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. ///