diff --git a/src/HotChocolate/AspNetCore/src/AspNetCore/Serialization/DefaultHttpResponseFormatter.cs b/src/HotChocolate/AspNetCore/src/AspNetCore/Serialization/DefaultHttpResponseFormatter.cs
index 3c5f9cb9fa0..3d62656cc26 100644
--- a/src/HotChocolate/AspNetCore/src/AspNetCore/Serialization/DefaultHttpResponseFormatter.cs
+++ b/src/HotChocolate/AspNetCore/src/AspNetCore/Serialization/DefaultHttpResponseFormatter.cs
@@ -184,6 +184,13 @@ public async ValueTask FormatAsync(
response.GetTypedHeaders().CacheControl = cacheControlHeaderValue;
}
+ if (result.ContextData is not null
+ && result.ContextData.TryGetValue(VaryHeaderValue, out var varyValue)
+ && varyValue is string varyHeaderValue)
+ {
+ response.Headers.Vary = varyHeaderValue;
+ }
+
OnWriteResponseHeaders(operationResult, format, response.Headers);
await format.Formatter.FormatAsync(result, response.Body, cancellationToken);
diff --git a/src/HotChocolate/Caching/src/Caching/CacheControlAttribute.cs b/src/HotChocolate/Caching/src/Caching/CacheControlAttribute.cs
index 02d40783360..4af8061fc13 100644
--- a/src/HotChocolate/Caching/src/Caching/CacheControlAttribute.cs
+++ b/src/HotChocolate/Caching/src/Caching/CacheControlAttribute.cs
@@ -15,8 +15,10 @@ namespace HotChocolate.Caching;
public sealed class CacheControlAttribute : DescriptorAttribute
{
private int? _maxAge;
+ private int? _sharedMaxAge;
private CacheControlScope? _scope;
private bool? _inheritMaxAge;
+ private string[]? _vary;
public CacheControlAttribute()
{
@@ -41,16 +43,16 @@ protected internal override void TryConfigure(
switch (descriptor)
{
case IObjectFieldDescriptor objectField:
- objectField.CacheControl(_maxAge, _scope, _inheritMaxAge);
+ objectField.CacheControl(_maxAge, _scope, _inheritMaxAge, _sharedMaxAge, _vary);
break;
case IObjectTypeDescriptor objectType:
- objectType.CacheControl(_maxAge, _scope);
+ objectType.CacheControl(_maxAge, _scope, _sharedMaxAge, _vary);
break;
case IInterfaceTypeDescriptor interfaceType:
- interfaceType.CacheControl(_maxAge, _scope);
+ interfaceType.CacheControl(_maxAge, _scope, _sharedMaxAge, _vary);
break;
case IUnionTypeDescriptor unionType:
- unionType.CacheControl(_maxAge, _scope);
+ unionType.CacheControl(_maxAge, _scope, _sharedMaxAge, _vary);
break;
}
}
@@ -60,6 +62,12 @@ protected internal override void TryConfigure(
///
public int MaxAge { get => _maxAge ?? CacheControlDefaults.MaxAge; set => _maxAge = value; }
+ ///
+ /// The maximum time, in seconds, this resource can be cached on CDNs and other shared caches.
+ /// If not set, the value of MaxAge is used for shared caches too.
+ ///
+ public int SharedMaxAge { get => _sharedMaxAge ?? 0; set => _sharedMaxAge = value; }
+
///
/// The scope of this resource.
///
@@ -70,7 +78,7 @@ public CacheControlScope Scope
}
///
- /// Whether this resource should inherit the MaxAge
+ /// Whether this resource should inherit the MaxAge and SharedMaxAge
/// of its parent.
///
public bool InheritMaxAge
@@ -78,4 +86,14 @@ public bool InheritMaxAge
get => _inheritMaxAge ?? false;
set => _inheritMaxAge = value;
}
+
+ ///
+ /// List of headers that might affect the value of this resource. Typically, these headers becomes part
+ /// of the cache key.
+ ///
+ public string[]? Vary
+ {
+ get => _vary ?? Array.Empty();
+ set => _vary = value;
+ }
}
diff --git a/src/HotChocolate/Caching/src/Caching/CacheControlConstraintsOptimizer.cs b/src/HotChocolate/Caching/src/Caching/CacheControlConstraintsOptimizer.cs
index f5ec0c57eb2..588fbb82f2c 100644
--- a/src/HotChocolate/Caching/src/Caching/CacheControlConstraintsOptimizer.cs
+++ b/src/HotChocolate/Caching/src/Caching/CacheControlConstraintsOptimizer.cs
@@ -1,10 +1,13 @@
+using System.Linq;
using System.Runtime.CompilerServices;
+using System.Text;
using HotChocolate.Execution.Processing;
using HotChocolate.Language;
using HotChocolate.Types;
using HotChocolate.Types.Introspection;
using HotChocolate.Utilities;
using Microsoft.Net.Http.Headers;
+using Microsoft.Extensions.Primitives;
using IHasDirectives = HotChocolate.Types.IHasDirectives;
namespace HotChocolate.Caching;
@@ -26,23 +29,25 @@ public void OptimizeOperation(OperationOptimizerContext context)
var constraints = ComputeCacheControlConstraints(context.CreateOperation());
- if (constraints.MaxAge is not null)
+ if (constraints.MaxAge is not null || constraints.SharedMaxAge is not null)
{
var headerValue = new CacheControlHeaderValue
{
Private = constraints.Scope == CacheControlScope.Private,
- MaxAge = TimeSpan.FromSeconds(constraints.MaxAge.Value),
+ MaxAge = constraints.MaxAge is not null ? TimeSpan.FromSeconds(constraints.MaxAge.Value) : null,
+ SharedMaxAge = constraints.SharedMaxAge is not null ? TimeSpan.FromSeconds(constraints.SharedMaxAge.Value) : null,
};
context.ContextData.Add(
- WellKnownContextData.CacheControlConstraints,
- new ImmutableCacheConstraints(
- constraints.MaxAge.Value,
- constraints.Scope));
+ WellKnownContextData.CacheControlHeaderValue,
+ headerValue.ToString());
+ }
+ if (constraints.Vary is { Length: > 0 })
+ {
context.ContextData.Add(
- WellKnownContextData.CacheControlHeaderValue,
- headerValue);
+ WellKnownContextData.VaryHeaderValue,
+ string.Join(", ", constraints.Vary));
}
}
@@ -67,11 +72,25 @@ private static void ProcessSelection(
{
var field = selection.Field;
var maxAgeSet = false;
+ var sharedMaxAgeSet = false;
var scopeSet = false;
+ var varySet = false;
ExtractCacheControlDetailsFromDirectives(field.Directives);
- if (!maxAgeSet || !scopeSet)
+ if (!maxAgeSet || !sharedMaxAgeSet || !scopeSet || !varySet)
+ {
+ // Either maxAge or scope have not been specified by the @cacheControl
+ // directive on the field, so we try to infer these details
+ // from the type of the field.
+
+ if (field.Type is IHasDirectives type)
+ {
+ // The type of the field is complex and can therefore be
+ // annotated with a @cacheControl directive.
+ ExtractCacheControlDetailsFromDirectives(type.Directives);
+ }
+ }
{
// Either maxAge or scope have not been specified by the @cacheControl
// directive on the field, so we try to infer these details
@@ -127,6 +146,22 @@ void ExtractCacheControlDetailsFromDirectives(
maxAgeSet = true;
}
+ if (!sharedMaxAgeSet &&
+ directive.SharedMaxAge.HasValue &&
+ (!constraints.SharedMaxAge.HasValue || directive.SharedMaxAge < constraints.SharedMaxAge.Value))
+ {
+ // The maxAge of the @cacheControl directive is lower
+ // than the previously lowest maxAge value.
+ constraints.SharedMaxAge = directive.SharedMaxAge.Value;
+ sharedMaxAgeSet = true;
+ }
+ else if (directive.InheritMaxAge == true)
+ {
+ // If inheritMaxAge is set, we keep the
+ // computed maxAge value as is.
+ sharedMaxAgeSet = true;
+ }
+
if (directive.Scope.HasValue &&
directive.Scope < constraints.Scope)
{
@@ -135,6 +170,19 @@ void ExtractCacheControlDetailsFromDirectives(
constraints.Scope = directive.Scope.Value;
scopeSet = true;
}
+
+ if (directive.Vary is { Length: > 0 })
+ {
+ if (constraints.Vary != null)
+ {
+ constraints.Vary = constraints.Vary.Concat(directive.Vary.Select(x => x.ToLowerInvariant())).Distinct().OrderBy(x => x).ToArray();
+ }
+ else
+ {
+ constraints.Vary = directive.Vary.Select(x => x.ToLowerInvariant()).Distinct().OrderBy(x => x).ToArray();
+ }
+ varySet = true;
+ }
}
}
}
@@ -163,5 +211,9 @@ private sealed class CacheControlConstraints
public CacheControlScope Scope { get; set; } = CacheControlScope.Public;
internal int? MaxAge { get; set; }
+
+ internal int? SharedMaxAge { get; set; }
+
+ internal string[]? Vary { get; set; }
}
}
diff --git a/src/HotChocolate/Caching/src/Caching/CacheControlValidationTypeInterceptor.cs b/src/HotChocolate/Caching/src/Caching/CacheControlValidationTypeInterceptor.cs
index 6330ca96756..3bcda49a375 100644
--- a/src/HotChocolate/Caching/src/Caching/CacheControlValidationTypeInterceptor.cs
+++ b/src/HotChocolate/Caching/src/Caching/CacheControlValidationTypeInterceptor.cs
@@ -133,5 +133,20 @@ private static void ValidateCacheControlOnField(
validationContext.ReportError(error);
}
}
+
+ if (directive.SharedMaxAge.HasValue)
+ {
+ if (directive.SharedMaxAge.Value < 0)
+ {
+ var error = ErrorHelper.CacheControlNegativeSharedMaxAge(obj, field);
+ validationContext.ReportError(error);
+ }
+
+ if (inheritMaxAge)
+ {
+ var error = ErrorHelper.CacheControlBothSharedMaxAgeAndInheritMaxAge(obj, field);
+ validationContext.ReportError(error);
+ }
+ }
}
}
diff --git a/src/HotChocolate/Caching/src/Caching/ErrorHelper.cs b/src/HotChocolate/Caching/src/Caching/ErrorHelper.cs
index 1ee858ef48c..20bade2acb2 100644
--- a/src/HotChocolate/Caching/src/Caching/ErrorHelper.cs
+++ b/src/HotChocolate/Caching/src/Caching/ErrorHelper.cs
@@ -44,6 +44,17 @@ public static ISchemaError CacheControlNegativeMaxAge(
.SetTypeSystemObject(type)
.Build();
+ public static ISchemaError CacheControlNegativeSharedMaxAge(
+ ITypeSystemObject type,
+ IField field)
+ => SchemaErrorBuilder.New()
+ .SetMessage(
+ ErrorHelper_CacheControlNegativeSharedMaxAge,
+ field.Coordinate.ToString())
+ .SetTypeSystemObject(type)
+ .AddSyntaxNode(field.SyntaxNode)
+ .Build();
+
public static ISchemaError CacheControlBothMaxAgeAndInheritMaxAge(
ITypeSystemObject type,
IField field)
@@ -53,4 +64,15 @@ public static ISchemaError CacheControlBothMaxAgeAndInheritMaxAge(
field.Coordinate.ToString())
.SetTypeSystemObject(type)
.Build();
+
+ public static ISchemaError CacheControlBothSharedMaxAgeAndInheritMaxAge(
+ ITypeSystemObject type,
+ IField field)
+ => SchemaErrorBuilder.New()
+ .SetMessage(
+ ErrorHelper_CacheControlBothSharedMaxAgeAndInheritMaxAge,
+ field.Coordinate.ToString())
+ .SetTypeSystemObject(type)
+ .AddSyntaxNode(field.SyntaxNode)
+ .Build();
}
diff --git a/src/HotChocolate/Caching/src/Caching/Extensions/CacheControlInterfaceTypeDescriptorExtensions.cs b/src/HotChocolate/Caching/src/Caching/Extensions/CacheControlInterfaceTypeDescriptorExtensions.cs
index 4895ad79d11..f6f89729657 100644
--- a/src/HotChocolate/Caching/src/Caching/Extensions/CacheControlInterfaceTypeDescriptorExtensions.cs
+++ b/src/HotChocolate/Caching/src/Caching/Extensions/CacheControlInterfaceTypeDescriptorExtensions.cs
@@ -17,9 +17,17 @@ public static class CacheControlInterfaceTypeDescriptorExtensions
///
/// The scope of fields of this type.
///
+ ///
+ /// The maximum time, in seconds, fields of this
+ /// type should be cached in a shared cache.
+ ///
+ ///
+ /// List of headers that might affect the value of this resource.
+ ///
public static IInterfaceTypeDescriptor CacheControl(
this IInterfaceTypeDescriptor descriptor,
- int? maxAge = null, CacheControlScope? scope = null)
+ int? maxAge = null, CacheControlScope? scope = null,
+ int? sharedMaxAge = null, string[]? vary = null)
{
if (descriptor is null)
{
@@ -27,7 +35,7 @@ public static IInterfaceTypeDescriptor CacheControl(
}
return descriptor.Directive(
- new CacheControlDirective(maxAge, scope));
+ new CacheControlDirective(maxAge, scope, null, sharedMaxAge, vary));
}
///
/// Specifies the caching rules for this interface type.
diff --git a/src/HotChocolate/Caching/src/Caching/Extensions/CacheControlObjectFieldDescriptorExtensions.cs b/src/HotChocolate/Caching/src/Caching/Extensions/CacheControlObjectFieldDescriptorExtensions.cs
index 2778e53e477..873e428aa8a 100644
--- a/src/HotChocolate/Caching/src/Caching/Extensions/CacheControlObjectFieldDescriptorExtensions.cs
+++ b/src/HotChocolate/Caching/src/Caching/Extensions/CacheControlObjectFieldDescriptorExtensions.cs
@@ -21,10 +21,18 @@ public static class CacheControlObjectFieldDescriptorExtensions
/// Whether this field should inherit the MaxAge
/// from its parent.
///
+ ///
+ /// The maximum time, in seconds, fields of this
+ /// type should be cached in a shared cache.
+ ///
+ ///
+ /// List of headers that might affect the value of this resource.
+ ///
+ ///
public static IObjectFieldDescriptor CacheControl(
this IObjectFieldDescriptor descriptor,
int? maxAge = null, CacheControlScope? scope = null,
- bool? inheritMaxAge = null)
+ bool? inheritMaxAge = null, int? sharedMaxAge = null, string[]? vary = null)
{
if (descriptor is null)
{
@@ -32,6 +40,6 @@ public static IObjectFieldDescriptor CacheControl(
}
return descriptor.Directive(
- new CacheControlDirective(maxAge, scope, inheritMaxAge));
+ new CacheControlDirective(maxAge, scope, inheritMaxAge, sharedMaxAge, vary));
}
}
diff --git a/src/HotChocolate/Caching/src/Caching/Extensions/CacheControlObjectTypeDescriptorExtensions.cs b/src/HotChocolate/Caching/src/Caching/Extensions/CacheControlObjectTypeDescriptorExtensions.cs
index c67e39ec02f..0f8fdec3a64 100644
--- a/src/HotChocolate/Caching/src/Caching/Extensions/CacheControlObjectTypeDescriptorExtensions.cs
+++ b/src/HotChocolate/Caching/src/Caching/Extensions/CacheControlObjectTypeDescriptorExtensions.cs
@@ -17,9 +17,17 @@ public static class CacheControlObjectTypeDescriptorExtensions
///
/// The scope of fields of this type.
///
+ ///
+ /// The maximum time, in seconds, fields of this
+ /// type should be cached in a shared cache.
+ ///
+ ///
+ /// List of headers that might affect the value of this resource.
+ ///
public static IObjectTypeDescriptor CacheControl(
this IObjectTypeDescriptor descriptor,
- int? maxAge = null, CacheControlScope? scope = null)
+ int? maxAge = null, CacheControlScope? scope = null,
+ int? sharedMaxAge = null, string[]? vary = null)
{
if (descriptor is null)
{
@@ -27,7 +35,7 @@ public static IObjectTypeDescriptor CacheControl(
}
return descriptor.Directive(
- new CacheControlDirective(maxAge, scope));
+ new CacheControlDirective(maxAge, scope, null, sharedMaxAge, vary));
}
///
diff --git a/src/HotChocolate/Caching/src/Caching/Extensions/CacheControlUnionTypeDescriptorExtensions.cs b/src/HotChocolate/Caching/src/Caching/Extensions/CacheControlUnionTypeDescriptorExtensions.cs
index 97a44a25a7d..f8e3c575c6a 100644
--- a/src/HotChocolate/Caching/src/Caching/Extensions/CacheControlUnionTypeDescriptorExtensions.cs
+++ b/src/HotChocolate/Caching/src/Caching/Extensions/CacheControlUnionTypeDescriptorExtensions.cs
@@ -17,9 +17,17 @@ public static class CacheControlUnionTypeDescriptorExtensions
///
/// The scope of fields of this type.
///
+ ///
+ /// The maximum time, in seconds, fields of this
+ /// type should be cached in a shared cache.
+ ///
+ ///
+ /// List of headers that might affect the value of this resource.
+ ///
public static IUnionTypeDescriptor CacheControl(
this IUnionTypeDescriptor descriptor,
- int? maxAge = null, CacheControlScope? scope = null)
+ int? maxAge = null, CacheControlScope? scope = null,
+ int? sharedMaxAge = null, string[]? vary = null)
{
if (descriptor is null)
{
@@ -27,6 +35,6 @@ public static IUnionTypeDescriptor CacheControl(
}
return descriptor.Directive(
- new CacheControlDirective(maxAge, scope));
+ new CacheControlDirective(maxAge, scope, null, sharedMaxAge, vary));
}
}
diff --git a/src/HotChocolate/Caching/src/Caching/Properties/CacheControlResources.Designer.cs b/src/HotChocolate/Caching/src/Caching/Properties/CacheControlResources.Designer.cs
index f2e76dbc38d..12fe2355bc0 100644
--- a/src/HotChocolate/Caching/src/Caching/Properties/CacheControlResources.Designer.cs
+++ b/src/HotChocolate/Caching/src/Caching/Properties/CacheControlResources.Designer.cs
@@ -63,6 +63,18 @@ internal static string CacheControlDirectiveType_MaxAge {
}
}
+ internal static string CacheControlDirectiveType_SharedMaxAge {
+ get {
+ return ResourceManager.GetString("CacheControlDirectiveType_SharedMaxAge", resourceCulture);
+ }
+ }
+
+ internal static string CacheControlDirectiveType_Vary {
+ get {
+ return ResourceManager.GetString("CacheControlDirectiveType_Vary", resourceCulture);
+ }
+ }
+
internal static string CacheControlDirectiveType_Scope {
get {
return ResourceManager.GetString("CacheControlDirectiveType_Scope", resourceCulture);
@@ -93,6 +105,12 @@ internal static string ErrorHelper_CacheControlBothMaxAgeAndInheritMaxAge {
}
}
+ internal static string ErrorHelper_CacheControlBothSharedMaxAgeAndInheritMaxAge {
+ get {
+ return ResourceManager.GetString("ErrorHelper_CacheControlBothSharedMaxAgeAndInheritMaxAge", resourceCulture);
+ }
+ }
+
internal static string ErrorHelper_CacheControlInheritMaxAgeOnQueryTypeField {
get {
return ResourceManager.GetString("ErrorHelper_CacheControlInheritMaxAgeOnQueryTypeField", resourceCulture);
@@ -111,6 +129,12 @@ internal static string ErrorHelper_CacheControlNegativeMaxAge {
}
}
+ internal static string ErrorHelper_CacheControlNegativeSharedMaxAge {
+ get {
+ return ResourceManager.GetString("ErrorHelper_CacheControlNegativeSharedMaxAge", resourceCulture);
+ }
+ }
+
internal static string ErrorHelper_CacheControlOnInterfaceField {
get {
return ResourceManager.GetString("ErrorHelper_CacheControlOnInterfaceField", resourceCulture);
diff --git a/src/HotChocolate/Caching/src/Caching/Properties/CacheControlResources.resx b/src/HotChocolate/Caching/src/Caching/Properties/CacheControlResources.resx
index 4a91dd711a2..7c45d38c338 100644
--- a/src/HotChocolate/Caching/src/Caching/Properties/CacheControlResources.resx
+++ b/src/HotChocolate/Caching/src/Caching/Properties/CacheControlResources.resx
@@ -126,6 +126,12 @@
The maximum amount of time this field's cached value is valid, in seconds.
+
+ The maximum amount of time this field's cached value is valid in shared caches like CDNs, in seconds.
+
+
+ List of headers that might affect the value of this field's value.
+
If `PRIVATE`, the field's value is specific to a single user. The default value is `PUBLIC`, which means the field's value is not tied to a single user.
@@ -141,6 +147,9 @@
Can not specify `inheritMaxAge: true` and a value for `maxAge` for the @cacheControl directive on the field {0}.
+
+ Can not specify `inheritMaxAge: true` and a value for `sharedMaxAge` for the @cacheControl directive on the field {0}.
+
Can not specify `inheritMaxAge: true` for the @cacheControl directive on the field {0}, since it's a field of the Query root type {1}.
@@ -150,6 +159,9 @@
The `maxAge` value for the @cacheControl directive on the field {0} can not be negative.
+
+ The `sharedMaxAge` value for the @cacheControl directive on the field {0} can not be negative.
+
Can not specify @cacheControl directive on interface fields, such as {0}.
diff --git a/src/HotChocolate/Caching/src/Caching/QueryCacheMiddleware.cs b/src/HotChocolate/Caching/src/Caching/QueryCacheMiddleware.cs
index c3796c94d5f..fdac88fcae0 100644
--- a/src/HotChocolate/Caching/src/Caching/QueryCacheMiddleware.cs
+++ b/src/HotChocolate/Caching/src/Caching/QueryCacheMiddleware.cs
@@ -47,6 +47,14 @@ operationResult.ContextData is not null
: new ExtensionData();
contextData.Add(WellKnownContextData.CacheControlHeaderValue, cacheControlHeaderValue);
+
+ if (context.Operation.ContextData.TryGetValue(VaryHeaderValue, out var varyValue)
+ && varyValue is string varyHeaderValue
+ && !string.IsNullOrEmpty(varyHeaderValue))
+ {
+ contextData.Add(VaryHeaderValue, varyHeaderValue);
+ }
+
context.Result = operationResult.WithContextData(contextData);
}
}
diff --git a/src/HotChocolate/Caching/src/Caching/Types/CacheControlDirective.cs b/src/HotChocolate/Caching/src/Caching/Types/CacheControlDirective.cs
index 77a2d7516ae..fb32adbdc4a 100644
--- a/src/HotChocolate/Caching/src/Caching/Types/CacheControlDirective.cs
+++ b/src/HotChocolate/Caching/src/Caching/Types/CacheControlDirective.cs
@@ -5,16 +5,24 @@ public sealed class CacheControlDirective
public CacheControlDirective(
int? maxAge = null,
CacheControlScope? scope = null,
- bool? inheritMaxAge = null)
+ bool? inheritMaxAge = null,
+ int? sharedMaxAge = null,
+ string[]? vary = null)
{
MaxAge = maxAge;
Scope = scope;
InheritMaxAge = inheritMaxAge;
+ SharedMaxAge = sharedMaxAge;
+ Vary = vary;
}
public int? MaxAge { get; }
+ public int? SharedMaxAge { get; }
+
public CacheControlScope? Scope { get; }
public bool? InheritMaxAge { get; }
+
+ public string[]? Vary { get; }
}
diff --git a/src/HotChocolate/Caching/src/Caching/Types/CacheControlDirectiveType.cs b/src/HotChocolate/Caching/src/Caching/Types/CacheControlDirectiveType.cs
index cb142e8ba2a..391357bad04 100644
--- a/src/HotChocolate/Caching/src/Caching/Types/CacheControlDirectiveType.cs
+++ b/src/HotChocolate/Caching/src/Caching/Types/CacheControlDirectiveType.cs
@@ -34,13 +34,33 @@ protected override void Configure(
.Name(Names.InheritMaxAgeArgName)
.Description(CacheControlDirectiveType_InheritMaxAge)
.Type();
+
+ descriptor
+ .Argument(a => a.SharedMaxAge)
+ .Name(Names.SharedMaxAgeArgName)
+ .Description(CacheControlDirectiveType_SharedMaxAge)
+ .Type();
+
+ descriptor
+ .Argument(a => a.Vary)
+ .Name(Names.VaryArgName)
+ .Description(CacheControlDirectiveType_Vary)
+ .Type>();
+
+ descriptor
+ .Argument(a => a.InheritMaxAge)
+ .Name(Names.InheritMaxAgeArgName)
+ .Description(CacheControlDirectiveType_InheritMaxAge)
+ .Type();
}
public static class Names
{
public const string DirectiveName = "cacheControl";
public const string MaxAgeArgName = "maxAge";
+ public const string SharedMaxAgeArgName = "sharedMaxAge";
public const string ScopeArgName = "scope";
public const string InheritMaxAgeArgName = "inheritMaxAge";
+ public const string VaryArgName = "vary";
}
}
diff --git a/src/HotChocolate/Caching/test/Caching.Tests/CacheControlDirectiveTests.cs b/src/HotChocolate/Caching/test/Caching.Tests/CacheControlDirectiveTests.cs
index d494ed846e5..bbda7afd64c 100644
--- a/src/HotChocolate/Caching/test/Caching.Tests/CacheControlDirectiveTests.cs
+++ b/src/HotChocolate/Caching/test/Caching.Tests/CacheControlDirectiveTests.cs
@@ -14,6 +14,20 @@ public void ValidMaxAge(int? maxAge)
var cacheControl = new CacheControlDirective(maxAge);
Assert.Equal(maxAge, cacheControl.MaxAge);
+ Assert.False(cacheControl.SharedMaxAge.HasValue);
+ }
+
+ [Theory]
+ [InlineData(null)]
+ [InlineData(0)]
+ [InlineData(10)]
+ [InlineData(1000)]
+ public void ValidSharedMaxAge(int? maxAge)
+ {
+ var cacheControl = new CacheControlDirective(sharedMaxAge: maxAge);
+
+ Assert.Equal(maxAge, cacheControl.SharedMaxAge);
+ Assert.False(cacheControl.MaxAge.HasValue);
}
[Theory]
@@ -27,6 +41,17 @@ public void ValidScope(CacheControlScope? scope)
Assert.Equal(scope, cacheControl.Scope);
}
+ [Theory]
+ [InlineData(null)]
+ [InlineData(new object[] {new string[0]})]
+ [InlineData(new object[] {new[] {"a", "b"}})]
+ public void ValidVary(string[]? vary)
+ {
+ var cacheControl = new CacheControlDirective(0, vary: vary);
+
+ Assert.Equal(vary, cacheControl.Vary);
+ }
+
[Theory]
[InlineData(null)]
[InlineData(true)]
diff --git a/src/HotChocolate/Caching/test/Caching.Tests/CacheControlDirectiveTypeTests.cs b/src/HotChocolate/Caching/test/Caching.Tests/CacheControlDirectiveTypeTests.cs
index ef0ad45d7fc..e3cfb7c29bf 100644
--- a/src/HotChocolate/Caching/test/Caching.Tests/CacheControlDirectiveTypeTests.cs
+++ b/src/HotChocolate/Caching/test/Caching.Tests/CacheControlDirectiveTypeTests.cs
@@ -40,6 +40,17 @@ public void CreateCacheControlDirective()
{
Assert.Equal("inheritMaxAge", t.Name);
Assert.IsType(t.Type);
+ },
+ t =>
+ {
+ Assert.Equal("sharedMaxAge", t.Name);
+ Assert.IsType(t.Type);
+ },
+ t =>
+ {
+ Assert.Equal("vary", t.Name);
+ Assert.IsType(t.Type);
+ Assert.IsType(t.Type.ElementType());
});
Assert.Collection(
directive.Locations.AsEnumerable(),
diff --git a/src/HotChocolate/Caching/test/Caching.Tests/CacheControlTypeInterceptorTests.cs b/src/HotChocolate/Caching/test/Caching.Tests/CacheControlTypeInterceptorTests.cs
index 7de9d1f2204..4d11034aed8 100644
--- a/src/HotChocolate/Caching/test/Caching.Tests/CacheControlTypeInterceptorTests.cs
+++ b/src/HotChocolate/Caching/test/Caching.Tests/CacheControlTypeInterceptorTests.cs
@@ -344,9 +344,18 @@ public class NestedType
[CacheControl(200)]
public IExecutable ExecutableFieldWithCacheControl() => default!;
- [CacheControl(200)]
+ [CacheControl(MaxAge = 200)]
public IQueryable QueryableFieldWithCacheControl() => default!;
+ [CacheControl(SharedMaxAge=200)]
+ public IQueryable QueryableFieldWithCacheControlSharedMaxAge() => default!;
+
+ [CacheControl(500, SharedMaxAge = 200)]
+ public IQueryable QueryableFieldWithCacheControlMaxAgeAndSharedMaxAge() => default!;
+
+ [CacheControl(500, SharedMaxAge = 200, Vary = new [] {"accept-language", "x-timezoneoffset"})]
+ public IQueryable QueryableFieldWithCacheControlMaxAgeAndSharedMaxAgeAndVary() => default!;
+
[CacheControl(200)]
[UsePaging]
public IQueryable
diff --git a/src/HotChocolate/Caching/test/Caching.Tests/HttpCachingTests.cs b/src/HotChocolate/Caching/test/Caching.Tests/HttpCachingTests.cs
index 4d0b4c66ea8..d6b886f14d8 100644
--- a/src/HotChocolate/Caching/test/Caching.Tests/HttpCachingTests.cs
+++ b/src/HotChocolate/Caching/test/Caching.Tests/HttpCachingTests.cs
@@ -58,6 +58,37 @@ public async Task MaxAge_Zero_Should_Cache()
result.MatchSnapshot();
}
+ [Theory]
+ [InlineData(60, 30)]
+ [InlineData(30, 60)]
+ [InlineData(30, 30)]
+ [InlineData(30, 3000)]
+ public async Task MaxAge_Multiple_Should_Cache_Shortest_Time(int time1, int time2)
+ {
+ var server = CreateServer(services =>
+ {
+ services.AddGraphQLServer()
+ .UseQueryCachePipeline()
+ .AddCacheControl()
+ .ModifyCacheControlOptions(o => o.ApplyDefaults = false)
+ .AddQueryType(d =>
+ {
+ var o = d.Name("Query");
+ o.Field("field1")
+ .Resolve("")
+ .CacheControl(time1);
+ o.Field("field2")
+ .Resolve("")
+ .CacheControl(time2);
+ });
+ });
+
+ var client = server.CreateClient();
+ var result = await client.PostQueryAsync("{ field1, field2 }");
+
+ result.MatchSnapshot();
+ }
+
[Fact]
public async Task Just_Defaults_Should_Cache()
{
@@ -205,6 +236,77 @@ public async Task QueryError_Should_Not_Cache()
result.MatchSnapshot();
}
+
+ [Fact]
+ public async Task SharedMaxAgeAndScope_Should_Cache()
+ {
+ var server = CreateServer(services =>
+ {
+ services.AddGraphQLServer()
+ .UseQueryCachePipeline()
+ .AddCacheControl()
+ .ModifyCacheControlOptions(o => o.ApplyDefaults = false)
+ .AddQueryType(d =>
+ d.Name("Query")
+ .Field("field")
+ .Resolve("")
+ .CacheControl(sharedMaxAge: 2000, scope: CacheControlScope.Public));
+ });
+
+ var client = server.CreateClient();
+ var result = await client.PostQueryAsync("{ field }");
+
+ result.MatchSnapshot();
+ }
+
+ [Fact]
+ public async Task SharedMaxAgeAndVary_Should_Cache()
+ {
+ var server = CreateServer(services =>
+ {
+ services.AddGraphQLServer()
+ .UseQueryCachePipeline()
+ .AddCacheControl()
+ .ModifyCacheControlOptions(o => o.ApplyDefaults = false)
+ .AddQueryType(d =>
+ d.Name("Query")
+ .Field("field")
+ .Resolve("")
+ .CacheControl(sharedMaxAge: 2000, vary: new[] { "X-foo", "X-BaR" }));
+ });
+
+ var client = server.CreateClient();
+ var result = await client.PostQueryAsync("{ field }");
+
+ result.MatchSnapshot();
+ }
+
+ [Fact]
+ public async Task SharedMaxAgeAndVary_Multiple_Should_Cache_And_Combine()
+ {
+ var server = CreateServer(services =>
+ {
+ services.AddGraphQLServer()
+ .UseQueryCachePipeline()
+ .AddCacheControl()
+ .ModifyCacheControlOptions(o => o.ApplyDefaults = false)
+ .AddQueryType(d =>
+ {
+ var o = d.Name("Query");
+ o.Field("field1")
+ .Resolve("")
+ .CacheControl(sharedMaxAge: 2000, vary: new[] {"X-foo", "X-BaR"});
+ o.Field("field2")
+ .Resolve("")
+ .CacheControl(sharedMaxAge: 1000, vary: new[] {"X-FAR", "X-BaR"});
+ });
+ });
+
+ var client = server.CreateClient();
+ var result = await client.PostQueryAsync("{ field1, field2 }");
+
+ result.MatchSnapshot();
+ }
}
public class GraphQLResult
diff --git a/src/HotChocolate/Caching/test/Caching.Tests/SchemaTests.cs b/src/HotChocolate/Caching/test/Caching.Tests/SchemaTests.cs
index c31cd4072a8..51e9d0a9d00 100644
--- a/src/HotChocolate/Caching/test/Caching.Tests/SchemaTests.cs
+++ b/src/HotChocolate/Caching/test/Caching.Tests/SchemaTests.cs
@@ -48,7 +48,8 @@ enum CacheControlScope {
}
"The `@cacheControl` directive may be provided for individual fields or entire object, interface or union types to provide caching hints to the executor."
- directive @cacheControl("The maximum amount of time this field's cached value is valid, in seconds." maxAge: Int "If `PRIVATE`, the field's value is specific to a single user. The default value is `PUBLIC`, which means the field's value is not tied to a single user." scope: CacheControlScope "If `true`, the field inherits the `maxAge` of its parent field." inheritMaxAge: Boolean) on OBJECT | FIELD_DEFINITION | INTERFACE | UNION
+ directive @cacheControl("The maximum amount of time this field's cached value is valid, in seconds." maxAge: Int "If `PRIVATE`, the field's value is specific to a single user. The default value is `PUBLIC`, which means the field's value is not tied to a single user." scope: CacheControlScope "If `true`, the field inherits the `maxAge` of its parent field." inheritMaxAge: Boolean "The maximum amount of time this field's cached value is valid in shared caches like CDNs, in seconds." sharedMaxAge: Int "List of headers that might affect the value of this field's value." vary: [String]) on OBJECT | FIELD_DEFINITION | INTERFACE | UNION
+ """);
"""
The @tag directive is used to apply arbitrary string
diff --git a/src/HotChocolate/Caching/test/Caching.Tests/__snapshots__/CacheControlTypeInterceptorTests.DataResolvers_ApplyDefaults.snap b/src/HotChocolate/Caching/test/Caching.Tests/__snapshots__/CacheControlTypeInterceptorTests.DataResolvers_ApplyDefaults.snap
index 75ca5c1d8da..aa3d69ea963 100644
--- a/src/HotChocolate/Caching/test/Caching.Tests/__snapshots__/CacheControlTypeInterceptorTests.DataResolvers_ApplyDefaults.snap
+++ b/src/HotChocolate/Caching/test/Caching.Tests/__snapshots__/CacheControlTypeInterceptorTests.DataResolvers_ApplyDefaults.snap
@@ -21,6 +21,9 @@ type NestedType {
valueTaskFieldWithCacheControl: String! @cacheControl(maxAge: 200)
executableFieldWithCacheControl: [String!]! @cacheControl(maxAge: 200)
queryableFieldWithCacheControl: [String!]! @cacheControl(maxAge: 200)
+ queryableFieldWithCacheControlSharedMaxAge: [String!]! @cacheControl(sharedMaxAge: 200)
+ queryableFieldWithCacheControlMaxAgeAndSharedMaxAge: [String!]! @cacheControl(maxAge: 500, sharedMaxAge: 200)
+ queryableFieldWithCacheControlMaxAgeAndSharedMaxAgeAndVary: [String!]! @cacheControl(maxAge: 500, sharedMaxAge: 200, vary: [ "accept-language", "x-timezoneoffset" ])
queryableFieldWithConnectionWithCacheControl("Returns the first _n_ elements from the list." first: Int "Returns the elements in the list that come after the specified cursor." after: String "Returns the last _n_ elements from the list." last: Int "Returns the elements in the list that come before the specified cursor." before: String): QueryableFieldWithConnectionWithCacheControlConnection @cacheControl(maxAge: 200)
queryableFieldWithCollectionSegmentWithCacheControl(skip: Int take: Int): QueryableFieldWithCollectionSegmentWithCacheControlCollectionSegment @cacheControl(maxAge: 200)
pureField: String!
@@ -54,6 +57,9 @@ type Query {
valueTaskFieldWithCacheControl: String! @cacheControl(maxAge: 200)
executableFieldWithCacheControl: [String!]! @cacheControl(maxAge: 200)
queryableFieldWithCacheControl: [String!]! @cacheControl(maxAge: 200)
+ queryableFieldWithCacheControlSharedMaxAge: [String!]! @cacheControl(sharedMaxAge: 200)
+ queryableFieldWithCacheControlMaxAgeAndSharedMaxAge: [String!]! @cacheControl(maxAge: 500, sharedMaxAge: 200)
+ queryableFieldWithCacheControlMaxAgeAndSharedMaxAgeAndVary: [String!]! @cacheControl(maxAge: 500, sharedMaxAge: 200, vary: [ "accept-language", "x-timezoneoffset" ])
queryableFieldWithConnectionWithCacheControl("Returns the first _n_ elements from the list." first: Int "Returns the elements in the list that come after the specified cursor." after: String "Returns the last _n_ elements from the list." last: Int "Returns the elements in the list that come before the specified cursor." before: String): QueryableFieldWithConnectionWithCacheControlConnection @cacheControl(maxAge: 200)
queryableFieldWithCollectionSegmentWithCacheControl(skip: Int take: Int): QueryableFieldWithCollectionSegmentWithCacheControlCollectionSegment @cacheControl(maxAge: 200)
nested: NestedType! @cacheControl(maxAge: 0)
@@ -123,4 +129,4 @@ enum CacheControlScope {
}
"The `@cacheControl` directive may be provided for individual fields or entire object, interface or union types to provide caching hints to the executor."
-directive @cacheControl("The maximum amount of time this field's cached value is valid, in seconds." maxAge: Int "If `PRIVATE`, the field's value is specific to a single user. The default value is `PUBLIC`, which means the field's value is not tied to a single user." scope: CacheControlScope "If `true`, the field inherits the `maxAge` of its parent field." inheritMaxAge: Boolean) on OBJECT | FIELD_DEFINITION | INTERFACE | UNION
+directive @cacheControl("The maximum amount of time this field's cached value is valid, in seconds." maxAge: Int "If `PRIVATE`, the field's value is specific to a single user. The default value is `PUBLIC`, which means the field's value is not tied to a single user." scope: CacheControlScope "If `true`, the field inherits the `maxAge` of its parent field." inheritMaxAge: Boolean "The maximum amount of time this field's cached value is valid in shared caches like CDNs, in seconds." sharedMaxAge: Int "List of headers that might affect the value of this field's value." vary: [String]) on OBJECT | FIELD_DEFINITION | INTERFACE | UNION
diff --git a/src/HotChocolate/Caching/test/Caching.Tests/__snapshots__/CacheControlTypeInterceptorTests.DataResolvers_ApplyDefaults_DifferentDefaultMaxAge.snap b/src/HotChocolate/Caching/test/Caching.Tests/__snapshots__/CacheControlTypeInterceptorTests.DataResolvers_ApplyDefaults_DifferentDefaultMaxAge.snap
index 00ec924eafa..67a240206f8 100644
--- a/src/HotChocolate/Caching/test/Caching.Tests/__snapshots__/CacheControlTypeInterceptorTests.DataResolvers_ApplyDefaults_DifferentDefaultMaxAge.snap
+++ b/src/HotChocolate/Caching/test/Caching.Tests/__snapshots__/CacheControlTypeInterceptorTests.DataResolvers_ApplyDefaults_DifferentDefaultMaxAge.snap
@@ -21,6 +21,9 @@ type NestedType {
valueTaskFieldWithCacheControl: String! @cacheControl(maxAge: 200)
executableFieldWithCacheControl: [String!]! @cacheControl(maxAge: 200)
queryableFieldWithCacheControl: [String!]! @cacheControl(maxAge: 200)
+ queryableFieldWithCacheControlSharedMaxAge: [String!]! @cacheControl(sharedMaxAge: 200)
+ queryableFieldWithCacheControlMaxAgeAndSharedMaxAge: [String!]! @cacheControl(maxAge: 500, sharedMaxAge: 200)
+ queryableFieldWithCacheControlMaxAgeAndSharedMaxAgeAndVary: [String!]! @cacheControl(maxAge: 500, sharedMaxAge: 200, vary: [ "accept-language", "x-timezoneoffset" ])
queryableFieldWithConnectionWithCacheControl("Returns the first _n_ elements from the list." first: Int "Returns the elements in the list that come after the specified cursor." after: String "Returns the last _n_ elements from the list." last: Int "Returns the elements in the list that come before the specified cursor." before: String): QueryableFieldWithConnectionWithCacheControlConnection @cacheControl(maxAge: 200)
queryableFieldWithCollectionSegmentWithCacheControl(skip: Int take: Int): QueryableFieldWithCollectionSegmentWithCacheControlCollectionSegment @cacheControl(maxAge: 200)
pureField: String!
@@ -54,6 +57,9 @@ type Query {
valueTaskFieldWithCacheControl: String! @cacheControl(maxAge: 200)
executableFieldWithCacheControl: [String!]! @cacheControl(maxAge: 200)
queryableFieldWithCacheControl: [String!]! @cacheControl(maxAge: 200)
+ queryableFieldWithCacheControlSharedMaxAge: [String!]! @cacheControl(sharedMaxAge: 200)
+ queryableFieldWithCacheControlMaxAgeAndSharedMaxAge: [String!]! @cacheControl(maxAge: 500, sharedMaxAge: 200)
+ queryableFieldWithCacheControlMaxAgeAndSharedMaxAgeAndVary: [String!]! @cacheControl(maxAge: 500, sharedMaxAge: 200, vary: [ "accept-language", "x-timezoneoffset" ])
queryableFieldWithConnectionWithCacheControl("Returns the first _n_ elements from the list." first: Int "Returns the elements in the list that come after the specified cursor." after: String "Returns the last _n_ elements from the list." last: Int "Returns the elements in the list that come before the specified cursor." before: String): QueryableFieldWithConnectionWithCacheControlConnection @cacheControl(maxAge: 200)
queryableFieldWithCollectionSegmentWithCacheControl(skip: Int take: Int): QueryableFieldWithCollectionSegmentWithCacheControlCollectionSegment @cacheControl(maxAge: 200)
nested: NestedType! @cacheControl(maxAge: 100)
@@ -123,4 +129,4 @@ enum CacheControlScope {
}
"The `@cacheControl` directive may be provided for individual fields or entire object, interface or union types to provide caching hints to the executor."
-directive @cacheControl("The maximum amount of time this field's cached value is valid, in seconds." maxAge: Int "If `PRIVATE`, the field's value is specific to a single user. The default value is `PUBLIC`, which means the field's value is not tied to a single user." scope: CacheControlScope "If `true`, the field inherits the `maxAge` of its parent field." inheritMaxAge: Boolean) on OBJECT | FIELD_DEFINITION | INTERFACE | UNION
+directive @cacheControl("The maximum amount of time this field's cached value is valid, in seconds." maxAge: Int "If `PRIVATE`, the field's value is specific to a single user. The default value is `PUBLIC`, which means the field's value is not tied to a single user." scope: CacheControlScope "If `true`, the field inherits the `maxAge` of its parent field." inheritMaxAge: Boolean "The maximum amount of time this field's cached value is valid in shared caches like CDNs, in seconds." sharedMaxAge: Int "List of headers that might affect the value of this field's value." vary: [String]) on OBJECT | FIELD_DEFINITION | INTERFACE | UNION
diff --git a/src/HotChocolate/Caching/test/Caching.Tests/__snapshots__/CacheControlTypeInterceptorTests.DataResolvers_ApplyDefaults_False.snap b/src/HotChocolate/Caching/test/Caching.Tests/__snapshots__/CacheControlTypeInterceptorTests.DataResolvers_ApplyDefaults_False.snap
index 4a0f7dad4a9..ca413242dfb 100644
--- a/src/HotChocolate/Caching/test/Caching.Tests/__snapshots__/CacheControlTypeInterceptorTests.DataResolvers_ApplyDefaults_False.snap
+++ b/src/HotChocolate/Caching/test/Caching.Tests/__snapshots__/CacheControlTypeInterceptorTests.DataResolvers_ApplyDefaults_False.snap
@@ -21,6 +21,9 @@ type NestedType {
valueTaskFieldWithCacheControl: String! @cacheControl(maxAge: 200)
executableFieldWithCacheControl: [String!]! @cacheControl(maxAge: 200)
queryableFieldWithCacheControl: [String!]! @cacheControl(maxAge: 200)
+ queryableFieldWithCacheControlSharedMaxAge: [String!]! @cacheControl(sharedMaxAge: 200)
+ queryableFieldWithCacheControlMaxAgeAndSharedMaxAge: [String!]! @cacheControl(maxAge: 500, sharedMaxAge: 200)
+ queryableFieldWithCacheControlMaxAgeAndSharedMaxAgeAndVary: [String!]! @cacheControl(maxAge: 500, sharedMaxAge: 200, vary: [ "accept-language", "x-timezoneoffset" ])
queryableFieldWithConnectionWithCacheControl("Returns the first _n_ elements from the list." first: Int "Returns the elements in the list that come after the specified cursor." after: String "Returns the last _n_ elements from the list." last: Int "Returns the elements in the list that come before the specified cursor." before: String): QueryableFieldWithConnectionWithCacheControlConnection @cacheControl(maxAge: 200)
queryableFieldWithCollectionSegmentWithCacheControl(skip: Int take: Int): QueryableFieldWithCollectionSegmentWithCacheControlCollectionSegment @cacheControl(maxAge: 200)
pureField: String!
@@ -54,6 +57,9 @@ type Query {
valueTaskFieldWithCacheControl: String! @cacheControl(maxAge: 200)
executableFieldWithCacheControl: [String!]! @cacheControl(maxAge: 200)
queryableFieldWithCacheControl: [String!]! @cacheControl(maxAge: 200)
+ queryableFieldWithCacheControlSharedMaxAge: [String!]! @cacheControl(sharedMaxAge: 200)
+ queryableFieldWithCacheControlMaxAgeAndSharedMaxAge: [String!]! @cacheControl(maxAge: 500, sharedMaxAge: 200)
+ queryableFieldWithCacheControlMaxAgeAndSharedMaxAgeAndVary: [String!]! @cacheControl(maxAge: 500, sharedMaxAge: 200, vary: [ "accept-language", "x-timezoneoffset" ])
queryableFieldWithConnectionWithCacheControl("Returns the first _n_ elements from the list." first: Int "Returns the elements in the list that come after the specified cursor." after: String "Returns the last _n_ elements from the list." last: Int "Returns the elements in the list that come before the specified cursor." before: String): QueryableFieldWithConnectionWithCacheControlConnection @cacheControl(maxAge: 200)
queryableFieldWithCollectionSegmentWithCacheControl(skip: Int take: Int): QueryableFieldWithCollectionSegmentWithCacheControlCollectionSegment @cacheControl(maxAge: 200)
nested: NestedType!
@@ -123,4 +129,4 @@ enum CacheControlScope {
}
"The `@cacheControl` directive may be provided for individual fields or entire object, interface or union types to provide caching hints to the executor."
-directive @cacheControl("The maximum amount of time this field's cached value is valid, in seconds." maxAge: Int "If `PRIVATE`, the field's value is specific to a single user. The default value is `PUBLIC`, which means the field's value is not tied to a single user." scope: CacheControlScope "If `true`, the field inherits the `maxAge` of its parent field." inheritMaxAge: Boolean) on OBJECT | FIELD_DEFINITION | INTERFACE | UNION
+directive @cacheControl("The maximum amount of time this field's cached value is valid, in seconds." maxAge: Int "If `PRIVATE`, the field's value is specific to a single user. The default value is `PUBLIC`, which means the field's value is not tied to a single user." scope: CacheControlScope "If `true`, the field inherits the `maxAge` of its parent field." inheritMaxAge: Boolean "The maximum amount of time this field's cached value is valid in shared caches like CDNs, in seconds." sharedMaxAge: Int "List of headers that might affect the value of this field's value." vary: [String]) on OBJECT | FIELD_DEFINITION | INTERFACE | UNION
diff --git a/src/HotChocolate/Caching/test/Caching.Tests/__snapshots__/CacheControlTypeInterceptorTests.DataResolvers_CacheControl_Disabled.snap b/src/HotChocolate/Caching/test/Caching.Tests/__snapshots__/CacheControlTypeInterceptorTests.DataResolvers_CacheControl_Disabled.snap
index 4a0f7dad4a9..ca413242dfb 100644
--- a/src/HotChocolate/Caching/test/Caching.Tests/__snapshots__/CacheControlTypeInterceptorTests.DataResolvers_CacheControl_Disabled.snap
+++ b/src/HotChocolate/Caching/test/Caching.Tests/__snapshots__/CacheControlTypeInterceptorTests.DataResolvers_CacheControl_Disabled.snap
@@ -21,6 +21,9 @@ type NestedType {
valueTaskFieldWithCacheControl: String! @cacheControl(maxAge: 200)
executableFieldWithCacheControl: [String!]! @cacheControl(maxAge: 200)
queryableFieldWithCacheControl: [String!]! @cacheControl(maxAge: 200)
+ queryableFieldWithCacheControlSharedMaxAge: [String!]! @cacheControl(sharedMaxAge: 200)
+ queryableFieldWithCacheControlMaxAgeAndSharedMaxAge: [String!]! @cacheControl(maxAge: 500, sharedMaxAge: 200)
+ queryableFieldWithCacheControlMaxAgeAndSharedMaxAgeAndVary: [String!]! @cacheControl(maxAge: 500, sharedMaxAge: 200, vary: [ "accept-language", "x-timezoneoffset" ])
queryableFieldWithConnectionWithCacheControl("Returns the first _n_ elements from the list." first: Int "Returns the elements in the list that come after the specified cursor." after: String "Returns the last _n_ elements from the list." last: Int "Returns the elements in the list that come before the specified cursor." before: String): QueryableFieldWithConnectionWithCacheControlConnection @cacheControl(maxAge: 200)
queryableFieldWithCollectionSegmentWithCacheControl(skip: Int take: Int): QueryableFieldWithCollectionSegmentWithCacheControlCollectionSegment @cacheControl(maxAge: 200)
pureField: String!
@@ -54,6 +57,9 @@ type Query {
valueTaskFieldWithCacheControl: String! @cacheControl(maxAge: 200)
executableFieldWithCacheControl: [String!]! @cacheControl(maxAge: 200)
queryableFieldWithCacheControl: [String!]! @cacheControl(maxAge: 200)
+ queryableFieldWithCacheControlSharedMaxAge: [String!]! @cacheControl(sharedMaxAge: 200)
+ queryableFieldWithCacheControlMaxAgeAndSharedMaxAge: [String!]! @cacheControl(maxAge: 500, sharedMaxAge: 200)
+ queryableFieldWithCacheControlMaxAgeAndSharedMaxAgeAndVary: [String!]! @cacheControl(maxAge: 500, sharedMaxAge: 200, vary: [ "accept-language", "x-timezoneoffset" ])
queryableFieldWithConnectionWithCacheControl("Returns the first _n_ elements from the list." first: Int "Returns the elements in the list that come after the specified cursor." after: String "Returns the last _n_ elements from the list." last: Int "Returns the elements in the list that come before the specified cursor." before: String): QueryableFieldWithConnectionWithCacheControlConnection @cacheControl(maxAge: 200)
queryableFieldWithCollectionSegmentWithCacheControl(skip: Int take: Int): QueryableFieldWithCollectionSegmentWithCacheControlCollectionSegment @cacheControl(maxAge: 200)
nested: NestedType!
@@ -123,4 +129,4 @@ enum CacheControlScope {
}
"The `@cacheControl` directive may be provided for individual fields or entire object, interface or union types to provide caching hints to the executor."
-directive @cacheControl("The maximum amount of time this field's cached value is valid, in seconds." maxAge: Int "If `PRIVATE`, the field's value is specific to a single user. The default value is `PUBLIC`, which means the field's value is not tied to a single user." scope: CacheControlScope "If `true`, the field inherits the `maxAge` of its parent field." inheritMaxAge: Boolean) on OBJECT | FIELD_DEFINITION | INTERFACE | UNION
+directive @cacheControl("The maximum amount of time this field's cached value is valid, in seconds." maxAge: Int "If `PRIVATE`, the field's value is specific to a single user. The default value is `PUBLIC`, which means the field's value is not tied to a single user." scope: CacheControlScope "If `true`, the field inherits the `maxAge` of its parent field." inheritMaxAge: Boolean "The maximum amount of time this field's cached value is valid in shared caches like CDNs, in seconds." sharedMaxAge: Int "List of headers that might affect the value of this field's value." vary: [String]) on OBJECT | FIELD_DEFINITION | INTERFACE | UNION
diff --git a/src/HotChocolate/Caching/test/Caching.Tests/__snapshots__/CacheControlTypeInterceptorTests.InheritMaxAgeOnQueryTypeField.snap b/src/HotChocolate/Caching/test/Caching.Tests/__snapshots__/CacheControlTypeInterceptorTests.InheritMaxAgeOnQueryTypeField.snap
index a0db67a45b2..f41f0dfd3ed 100644
--- a/src/HotChocolate/Caching/test/Caching.Tests/__snapshots__/CacheControlTypeInterceptorTests.InheritMaxAgeOnQueryTypeField.snap
+++ b/src/HotChocolate/Caching/test/Caching.Tests/__snapshots__/CacheControlTypeInterceptorTests.InheritMaxAgeOnQueryTypeField.snap
@@ -4,3 +4,15 @@
"extensions": {}
}
+{
+ "message": "Can not specify `inheritMaxAge: true` and a value for `maxAge` for the @cacheControl directive on the field Query.field.",
+ "type": "Query",
+ "extensions": {}
+}
+
+{
+ "message": "Can not specify `inheritMaxAge: true` and a value for `sharedMaxAge` for the @cacheControl directive on the field Query.field.",
+ "type": "Query",
+ "extensions": {}
+}
+
diff --git a/src/HotChocolate/Caching/test/Caching.Tests/__snapshots__/CacheControlTypeInterceptorTests.MaxAgeAndInheritMaxAgeOnSameField.snap b/src/HotChocolate/Caching/test/Caching.Tests/__snapshots__/CacheControlTypeInterceptorTests.MaxAgeAndInheritMaxAgeOnSameField.snap
index 37792340d6f..967c5bc0e1e 100644
--- a/src/HotChocolate/Caching/test/Caching.Tests/__snapshots__/CacheControlTypeInterceptorTests.MaxAgeAndInheritMaxAgeOnSameField.snap
+++ b/src/HotChocolate/Caching/test/Caching.Tests/__snapshots__/CacheControlTypeInterceptorTests.MaxAgeAndInheritMaxAgeOnSameField.snap
@@ -4,3 +4,9 @@
"extensions": {}
}
+{
+ "message": "Can not specify `inheritMaxAge: true` and a value for `sharedMaxAge` for the @cacheControl directive on the field NestedType.field.",
+ "type": "NestedType",
+ "extensions": {}
+}
+
diff --git a/src/HotChocolate/Caching/test/Caching.Tests/__snapshots__/CacheControlTypeInterceptorTests.QueryFields_ApplyDefaults.snap b/src/HotChocolate/Caching/test/Caching.Tests/__snapshots__/CacheControlTypeInterceptorTests.QueryFields_ApplyDefaults.snap
index f67539a1b83..6288af3354d 100644
--- a/src/HotChocolate/Caching/test/Caching.Tests/__snapshots__/CacheControlTypeInterceptorTests.QueryFields_ApplyDefaults.snap
+++ b/src/HotChocolate/Caching/test/Caching.Tests/__snapshots__/CacheControlTypeInterceptorTests.QueryFields_ApplyDefaults.snap
@@ -16,4 +16,4 @@ enum CacheControlScope {
}
"The `@cacheControl` directive may be provided for individual fields or entire object, interface or union types to provide caching hints to the executor."
-directive @cacheControl("The maximum amount of time this field's cached value is valid, in seconds." maxAge: Int "If `PRIVATE`, the field's value is specific to a single user. The default value is `PUBLIC`, which means the field's value is not tied to a single user." scope: CacheControlScope "If `true`, the field inherits the `maxAge` of its parent field." inheritMaxAge: Boolean) on OBJECT | FIELD_DEFINITION | INTERFACE | UNION
+directive @cacheControl("The maximum amount of time this field's cached value is valid, in seconds." maxAge: Int "If `PRIVATE`, the field's value is specific to a single user. The default value is `PUBLIC`, which means the field's value is not tied to a single user." scope: CacheControlScope "If `true`, the field inherits the `maxAge` of its parent field." inheritMaxAge: Boolean "The maximum amount of time this field's cached value is valid in shared caches like CDNs, in seconds." sharedMaxAge: Int "List of headers that might affect the value of this field's value." vary: [String]) on OBJECT | FIELD_DEFINITION | INTERFACE | UNION
diff --git a/src/HotChocolate/Caching/test/Caching.Tests/__snapshots__/CacheControlTypeInterceptorTests.QueryFields_ApplyDefaults_DifferentDefaultMaxAge.snap b/src/HotChocolate/Caching/test/Caching.Tests/__snapshots__/CacheControlTypeInterceptorTests.QueryFields_ApplyDefaults_DifferentDefaultMaxAge.snap
index afc00558328..5f52316056e 100644
--- a/src/HotChocolate/Caching/test/Caching.Tests/__snapshots__/CacheControlTypeInterceptorTests.QueryFields_ApplyDefaults_DifferentDefaultMaxAge.snap
+++ b/src/HotChocolate/Caching/test/Caching.Tests/__snapshots__/CacheControlTypeInterceptorTests.QueryFields_ApplyDefaults_DifferentDefaultMaxAge.snap
@@ -16,4 +16,4 @@ enum CacheControlScope {
}
"The `@cacheControl` directive may be provided for individual fields or entire object, interface or union types to provide caching hints to the executor."
-directive @cacheControl("The maximum amount of time this field's cached value is valid, in seconds." maxAge: Int "If `PRIVATE`, the field's value is specific to a single user. The default value is `PUBLIC`, which means the field's value is not tied to a single user." scope: CacheControlScope "If `true`, the field inherits the `maxAge` of its parent field." inheritMaxAge: Boolean) on OBJECT | FIELD_DEFINITION | INTERFACE | UNION
+directive @cacheControl("The maximum amount of time this field's cached value is valid, in seconds." maxAge: Int "If `PRIVATE`, the field's value is specific to a single user. The default value is `PUBLIC`, which means the field's value is not tied to a single user." scope: CacheControlScope "If `true`, the field inherits the `maxAge` of its parent field." inheritMaxAge: Boolean "The maximum amount of time this field's cached value is valid in shared caches like CDNs, in seconds." sharedMaxAge: Int "List of headers that might affect the value of this field's value." vary: [String]) on OBJECT | FIELD_DEFINITION | INTERFACE | UNION
diff --git a/src/HotChocolate/Caching/test/Caching.Tests/__snapshots__/CacheControlTypeInterceptorTests.QueryFields_ApplyDefaults_False.snap b/src/HotChocolate/Caching/test/Caching.Tests/__snapshots__/CacheControlTypeInterceptorTests.QueryFields_ApplyDefaults_False.snap
index e96747cd13b..ea1c8802e58 100644
--- a/src/HotChocolate/Caching/test/Caching.Tests/__snapshots__/CacheControlTypeInterceptorTests.QueryFields_ApplyDefaults_False.snap
+++ b/src/HotChocolate/Caching/test/Caching.Tests/__snapshots__/CacheControlTypeInterceptorTests.QueryFields_ApplyDefaults_False.snap
@@ -16,4 +16,4 @@ enum CacheControlScope {
}
"The `@cacheControl` directive may be provided for individual fields or entire object, interface or union types to provide caching hints to the executor."
-directive @cacheControl("The maximum amount of time this field's cached value is valid, in seconds." maxAge: Int "If `PRIVATE`, the field's value is specific to a single user. The default value is `PUBLIC`, which means the field's value is not tied to a single user." scope: CacheControlScope "If `true`, the field inherits the `maxAge` of its parent field." inheritMaxAge: Boolean) on OBJECT | FIELD_DEFINITION | INTERFACE | UNION
+directive @cacheControl("The maximum amount of time this field's cached value is valid, in seconds." maxAge: Int "If `PRIVATE`, the field's value is specific to a single user. The default value is `PUBLIC`, which means the field's value is not tied to a single user." scope: CacheControlScope "If `true`, the field inherits the `maxAge` of its parent field." inheritMaxAge: Boolean "The maximum amount of time this field's cached value is valid in shared caches like CDNs, in seconds." sharedMaxAge: Int "List of headers that might affect the value of this field's value." vary: [String]) on OBJECT | FIELD_DEFINITION | INTERFACE | UNION
diff --git a/src/HotChocolate/Caching/test/Caching.Tests/__snapshots__/CacheControlTypeInterceptorTests.QueryFields_CacheControl_Disabled.snap b/src/HotChocolate/Caching/test/Caching.Tests/__snapshots__/CacheControlTypeInterceptorTests.QueryFields_CacheControl_Disabled.snap
index e96747cd13b..ea1c8802e58 100644
--- a/src/HotChocolate/Caching/test/Caching.Tests/__snapshots__/CacheControlTypeInterceptorTests.QueryFields_CacheControl_Disabled.snap
+++ b/src/HotChocolate/Caching/test/Caching.Tests/__snapshots__/CacheControlTypeInterceptorTests.QueryFields_CacheControl_Disabled.snap
@@ -16,4 +16,4 @@ enum CacheControlScope {
}
"The `@cacheControl` directive may be provided for individual fields or entire object, interface or union types to provide caching hints to the executor."
-directive @cacheControl("The maximum amount of time this field's cached value is valid, in seconds." maxAge: Int "If `PRIVATE`, the field's value is specific to a single user. The default value is `PUBLIC`, which means the field's value is not tied to a single user." scope: CacheControlScope "If `true`, the field inherits the `maxAge` of its parent field." inheritMaxAge: Boolean) on OBJECT | FIELD_DEFINITION | INTERFACE | UNION
+directive @cacheControl("The maximum amount of time this field's cached value is valid, in seconds." maxAge: Int "If `PRIVATE`, the field's value is specific to a single user. The default value is `PUBLIC`, which means the field's value is not tied to a single user." scope: CacheControlScope "If `true`, the field inherits the `maxAge` of its parent field." inheritMaxAge: Boolean "The maximum amount of time this field's cached value is valid in shared caches like CDNs, in seconds." sharedMaxAge: Int "List of headers that might affect the value of this field's value." vary: [String]) on OBJECT | FIELD_DEFINITION | INTERFACE | UNION
diff --git a/src/HotChocolate/Caching/test/Caching.Tests/__snapshots__/HttpCachingTests.MaxAge_Multiple_Should_Cache_Shortest_Time.snap b/src/HotChocolate/Caching/test/Caching.Tests/__snapshots__/HttpCachingTests.MaxAge_Multiple_Should_Cache_Shortest_Time.snap
new file mode 100644
index 00000000000..77e33ba4e4b
--- /dev/null
+++ b/src/HotChocolate/Caching/test/Caching.Tests/__snapshots__/HttpCachingTests.MaxAge_Multiple_Should_Cache_Shortest_Time.snap
@@ -0,0 +1,19 @@
+{
+ "Headers": [
+ {
+ "Key": "Cache-Control",
+ "Value": [
+ "public, max-age=30"
+ ]
+ }
+ ],
+ "ContentHeaders": [
+ {
+ "Key": "Content-Type",
+ "Value": [
+ "application/graphql-response+json; charset=utf-8"
+ ]
+ }
+ ],
+ "Body": "{\"data\":{\"field1\":\"\",\"field2\":\"\"}}"
+}
diff --git a/src/HotChocolate/Caching/test/Caching.Tests/__snapshots__/HttpCachingTests.SharedMaxAgeAndScope_Should_Cache.snap b/src/HotChocolate/Caching/test/Caching.Tests/__snapshots__/HttpCachingTests.SharedMaxAgeAndScope_Should_Cache.snap
new file mode 100644
index 00000000000..c9428bf981b
--- /dev/null
+++ b/src/HotChocolate/Caching/test/Caching.Tests/__snapshots__/HttpCachingTests.SharedMaxAgeAndScope_Should_Cache.snap
@@ -0,0 +1,19 @@
+{
+ "Headers": [
+ {
+ "Key": "Cache-Control",
+ "Value": [
+ "public, s-maxage=2000"
+ ]
+ }
+ ],
+ "ContentHeaders": [
+ {
+ "Key": "Content-Type",
+ "Value": [
+ "application/graphql-response+json; charset=utf-8"
+ ]
+ }
+ ],
+ "Body": "{\"data\":{\"field\":\"\"}}"
+}
diff --git a/src/HotChocolate/Caching/test/Caching.Tests/__snapshots__/HttpCachingTests.SharedMaxAgeAndVary_Multiple_Should_Cache_And_Combine.snap b/src/HotChocolate/Caching/test/Caching.Tests/__snapshots__/HttpCachingTests.SharedMaxAgeAndVary_Multiple_Should_Cache_And_Combine.snap
new file mode 100644
index 00000000000..6d5c5846f1c
--- /dev/null
+++ b/src/HotChocolate/Caching/test/Caching.Tests/__snapshots__/HttpCachingTests.SharedMaxAgeAndVary_Multiple_Should_Cache_And_Combine.snap
@@ -0,0 +1,27 @@
+{
+ "Headers": [
+ {
+ "Key": "Cache-Control",
+ "Value": [
+ "public, s-maxage=1000"
+ ]
+ },
+ {
+ "Key": "Vary",
+ "Value": [
+ "x-bar",
+ "x-far",
+ "x-foo"
+ ]
+ }
+ ],
+ "ContentHeaders": [
+ {
+ "Key": "Content-Type",
+ "Value": [
+ "application/graphql-response+json; charset=utf-8"
+ ]
+ }
+ ],
+ "Body": "{\"data\":{\"field1\":\"\",\"field2\":\"\"}}"
+}
diff --git a/src/HotChocolate/Caching/test/Caching.Tests/__snapshots__/HttpCachingTests.SharedMaxAgeAndVary_Should_Cache.snap b/src/HotChocolate/Caching/test/Caching.Tests/__snapshots__/HttpCachingTests.SharedMaxAgeAndVary_Should_Cache.snap
new file mode 100644
index 00000000000..7354014392b
--- /dev/null
+++ b/src/HotChocolate/Caching/test/Caching.Tests/__snapshots__/HttpCachingTests.SharedMaxAgeAndVary_Should_Cache.snap
@@ -0,0 +1,26 @@
+{
+ "Headers": [
+ {
+ "Key": "Cache-Control",
+ "Value": [
+ "public, s-maxage=2000"
+ ]
+ },
+ {
+ "Key": "Vary",
+ "Value": [
+ "x-bar",
+ "x-foo"
+ ]
+ }
+ ],
+ "ContentHeaders": [
+ {
+ "Key": "Content-Type",
+ "Value": [
+ "application/graphql-response+json; charset=utf-8"
+ ]
+ }
+ ],
+ "Body": "{\"data\":{\"field\":\"\"}}"
+}
diff --git a/src/HotChocolate/Core/src/Abstractions/WellKnownContextData.cs b/src/HotChocolate/Core/src/Abstractions/WellKnownContextData.cs
index 21da2c72034..9b5119d214b 100644
--- a/src/HotChocolate/Core/src/Abstractions/WellKnownContextData.cs
+++ b/src/HotChocolate/Core/src/Abstractions/WellKnownContextData.cs
@@ -219,6 +219,11 @@ public static class WellKnownContextData
///
public const string CacheControlHeaderValue = "HotChocolate.Caching.CacheControlHeaderValue";
+ ///
+ /// The key to get the Vary header value from the context data.
+ ///
+ public const string VaryHeaderValue = "HotChocolate.Caching.VaryHeaderValue";
+
///
/// The key to to ski caching a query result.
///