diff --git a/src/HotChocolate/Caching/src/Caching/CacheControlTypeInterceptor.cs b/src/HotChocolate/Caching/src/Caching/CacheControlTypeInterceptor.cs index 6a6468585c4..23bf4c29595 100644 --- a/src/HotChocolate/Caching/src/Caching/CacheControlTypeInterceptor.cs +++ b/src/HotChocolate/Caching/src/Caching/CacheControlTypeInterceptor.cs @@ -1,3 +1,5 @@ +using System; +using System.Collections.Generic; using System.Linq; using HotChocolate.Configuration; using HotChocolate.Language; @@ -10,14 +12,16 @@ namespace HotChocolate.Caching; internal sealed class CacheControlTypeInterceptor : TypeInterceptor { + private readonly List<(RegisteredType Type, ObjectTypeDefinition TypeDef)> _types = new(); private readonly ICacheControlOptions _cacheControlOptions; + private TypeDependency? _cacheControlDependency; public CacheControlTypeInterceptor(ICacheControlOptionsAccessor accessor) { _cacheControlOptions = accessor.CacheControl; } - public override void OnBeforeCompleteType( + public override void OnBeforeCompleteName( ITypeCompletionContext completionContext, DefinitionBase definition) { @@ -26,19 +30,42 @@ public CacheControlTypeInterceptor(ICacheControlOptionsAccessor accessor) return; } - if (completionContext.IsIntrospectionType || - completionContext.IsSubscriptionType == true || - completionContext.IsMutationType == true) + if (completionContext.Type is ObjectType && definition is ObjectTypeDefinition typeDef) + { + _types.Add(((RegisteredType)completionContext, typeDef)); + } + } + + public override void OnAfterMergeTypeExtensions() + { + foreach (var item in _types) + { + TryApplyDefaults(item.Type, item.TypeDef); + } + } + + private void TryApplyDefaults(RegisteredType type, ObjectTypeDefinition objectDef) + { + if (!_cacheControlOptions.Enable || !_cacheControlOptions.ApplyDefaults) { return; } - if (definition is not ObjectTypeDefinition objectDef) + if (type.IsIntrospectionType || + type.IsSubscriptionType == true || + type.IsMutationType == true) { return; } + _cacheControlDependency ??= new TypeDependency( + new ExtendedTypeDirectiveReference( + type.TypeInspector.GetType( + typeof(CacheControlDirectiveType))), + TypeDependencyFulfilled.Completed); + var length = objectDef.Fields.Count; + var appliedDefaults = false; #if NET6_0_OR_GREATER var fields = ((BindableList)objectDef.Fields).AsSpan(); @@ -63,17 +90,24 @@ public CacheControlTypeInterceptor(ICacheControlOptionsAccessor accessor) continue; } - if (completionContext.IsQueryType == true || + if (type.IsQueryType == true || CostTypeInterceptor.IsDataResolver(field)) { // Each field on the query type or data resolver fields // are treated as fields that need to be explicitly cached. ApplyCacheControlWithDefaultMaxAge(field); + appliedDefaults = true; } } + + if (appliedDefaults) + { + type.Dependencies.Add(_cacheControlDependency); + } } - private void ApplyCacheControlWithDefaultMaxAge(OutputFieldDefinitionBase field) + private void ApplyCacheControlWithDefaultMaxAge( + OutputFieldDefinitionBase field) { field.Directives.Add( new DirectiveDefinition( @@ -85,9 +119,7 @@ private void ApplyCacheControlWithDefaultMaxAge(OutputFieldDefinitionBase field) } private static bool HasCacheControlDirective(ObjectFieldDefinition field) - { - return field.Directives.Any(IsCacheControlDirective); - } + => field.Directives.Any(static d => IsCacheControlDirective(d)); private static bool IsCacheControlDirective(DirectiveDefinition directive) { diff --git a/src/HotChocolate/Caching/src/Caching/Types/CacheControlScope.cs b/src/HotChocolate/Caching/src/Caching/Types/CacheControlScope.cs index 064e30de3cd..58890295e95 100644 --- a/src/HotChocolate/Caching/src/Caching/Types/CacheControlScope.cs +++ b/src/HotChocolate/Caching/src/Caching/Types/CacheControlScope.cs @@ -11,6 +11,7 @@ public enum CacheControlScope /// be only cached for and accessible to that user. /// Private, + /// /// Public query results contain data /// that is not scoped to a particular user and can diff --git a/src/HotChocolate/Caching/test/Caching.Tests/SchemaTests.cs b/src/HotChocolate/Caching/test/Caching.Tests/SchemaTests.cs new file mode 100644 index 00000000000..6b402aa65d7 --- /dev/null +++ b/src/HotChocolate/Caching/test/Caching.Tests/SchemaTests.cs @@ -0,0 +1,66 @@ +using System.Threading.Tasks; +using CookieCrumble; +using HotChocolate.Execution; +using HotChocolate.Types; +using Microsoft.Extensions.DependencyInjection; +using Xunit; + +namespace HotChocolate.Caching.Tests; + +public class SchemaTests +{ + [Fact] + public async Task Allow_CacheControl_On_FieldDefinition() + { + var schema = + await new ServiceCollection() + .AddGraphQLServer() + .AddTypeExtension(typeof(Query)) + .ConfigureSchema( + b => b.TryAddRootType( + () => new ObjectType( + d => d.Name(OperationTypeNames.Query)), + Language.OperationType.Query)) + .AddCacheControl() + .BuildSchemaAsync(); + + schema.MatchInlineSnapshot( + """ + schema { + query: Query + } + + type Book { + title: String! @cacheControl(maxAge: 5000) + description: String! + } + + type Query { + book: Book! @cacheControl(maxAge: 0) + } + + "The scope of a cache hint." + enum CacheControlScope { + "The value to cache is not tied to a single user." + PUBLIC + "The value to cache is specific to a single user." + PRIVATE + } + + "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 + """); + } + + [QueryType] + public static class Query + { + public static Book GetBook() + => new Book("C# in depth.", "abc"); + +} + + public record Book( + [property: CacheControl(5000)] string Title, + string Description); +} diff --git a/src/HotChocolate/Core/src/Abstractions/ErrorCodes.cs b/src/HotChocolate/Core/src/Abstractions/ErrorCodes.cs index 910ec79217b..758c0a478b1 100644 --- a/src/HotChocolate/Core/src/Abstractions/ErrorCodes.cs +++ b/src/HotChocolate/Core/src/Abstractions/ErrorCodes.cs @@ -197,7 +197,6 @@ public static class Schema public const string UnresolvedTypes = "TS_UNRESOLVED_TYPES"; public const string NoName = "TS_NO_NAME_DEFINED"; public const string NoFieldType = "TS_NO_FIELD_TYPE"; - public const string ArgumentValueTypeWrong = "TS_ARG_VALUE_TYPE_WRONG"; public const string InvalidArgument = "TS_INVALID_ARG"; public const string InterfaceNotImplemented = "SCHEMA_INTERFACE_NO_IMPL"; public const string DuplicateTypeName = "HC0065"; diff --git a/src/HotChocolate/Core/src/Types.Analyzers/Generators/ModuleGenerator.cs b/src/HotChocolate/Core/src/Types.Analyzers/Generators/ModuleGenerator.cs index 6bfaeb418f7..92653f92fdf 100644 --- a/src/HotChocolate/Core/src/Types.Analyzers/Generators/ModuleGenerator.cs +++ b/src/HotChocolate/Core/src/Types.Analyzers/Generators/ModuleGenerator.cs @@ -85,7 +85,7 @@ public bool Consume(ISyntaxInfo syntaxInfo) sourceText.Append(Indent) .Append(Indent) .Append(Indent) - .Append("builder.AddType<") + .Append("builder.AddType();"); } @@ -100,7 +100,7 @@ public bool Consume(ISyntaxInfo syntaxInfo) sourceText.Append(Indent) .Append(Indent) .Append(Indent) - .Append("builder.AddTypeExtension(typeof(") + .Append("builder.AddTypeExtension(typeof(global::") .Append(extension.Name) .AppendLine("));"); } @@ -109,7 +109,7 @@ public bool Consume(ISyntaxInfo syntaxInfo) sourceText.Append(Indent) .Append(Indent) .Append(Indent) - .Append("builder.AddTypeExtension<") + .Append("builder.AddTypeExtension();"); } @@ -129,7 +129,7 @@ public bool Consume(ISyntaxInfo syntaxInfo) sourceText.Append(Indent) .Append(Indent) .Append(Indent) - .Append("builder.AddDataLoader<") + .Append("builder.AddDataLoader();"); } diff --git a/src/HotChocolate/Core/src/Types/HotChocolate.Types.csproj b/src/HotChocolate/Core/src/Types/HotChocolate.Types.csproj index 125f9ce06e1..8204e7cced7 100644 --- a/src/HotChocolate/Core/src/Types/HotChocolate.Types.csproj +++ b/src/HotChocolate/Core/src/Types/HotChocolate.Types.csproj @@ -99,6 +99,9 @@ RegisteredType.cs + + ObjectTypeExtension.cs + diff --git a/src/HotChocolate/Core/src/Types/Internal/TypeDependencyHelper.cs b/src/HotChocolate/Core/src/Types/Internal/TypeDependencyHelper.cs index 58c55a704ef..74f22a2a976 100644 --- a/src/HotChocolate/Core/src/Types/Internal/TypeDependencyHelper.cs +++ b/src/HotChocolate/Core/src/Types/Internal/TypeDependencyHelper.cs @@ -185,7 +185,7 @@ public static class TypeDependencyHelper { foreach (var directive in definition.Directives) { - dependencies.Add(new(directive.Type, Completed)); + dependencies.Add(new TypeDependency(directive.Type, Completed)); } } } @@ -198,7 +198,7 @@ public static class TypeDependencyHelper { foreach (var directive in definition.Directives) { - dependencies.Add(new(directive.Type, Completed)); + dependencies.Add(new TypeDependency(directive.Type, Completed)); } } } diff --git a/src/HotChocolate/Core/src/Types/Internal/TypeDiscoveryHandler.cs b/src/HotChocolate/Core/src/Types/Internal/TypeDiscoveryHandler.cs index d28b2a8302f..09bfe6e6eaf 100644 --- a/src/HotChocolate/Core/src/Types/Internal/TypeDiscoveryHandler.cs +++ b/src/HotChocolate/Core/src/Types/Internal/TypeDiscoveryHandler.cs @@ -6,7 +6,7 @@ namespace HotChocolate.Internal; /// -/// A type discover handler allows to specify how types are inferred +/// A type discovery handler allows to specify how types are inferred /// from s. /// public abstract class TypeDiscoveryHandler diff --git a/src/HotChocolate/Core/src/Types/Types/Descriptors/ObjectTypeDescriptor.cs b/src/HotChocolate/Core/src/Types/Types/Descriptors/ObjectTypeDescriptor.cs index dda7fed0cdb..aea2ed75e9b 100644 --- a/src/HotChocolate/Core/src/Types/Types/Descriptors/ObjectTypeDescriptor.cs +++ b/src/HotChocolate/Core/src/Types/Types/Descriptors/ObjectTypeDescriptor.cs @@ -15,7 +15,7 @@ namespace HotChocolate.Types.Descriptors; public class ObjectTypeDescriptor : DescriptorBase - , IObjectTypeDescriptor + , IObjectTypeDescriptor { private readonly List _fields = new(); diff --git a/src/HotChocolate/Core/src/Types/Types/ObjectTypeExtension.cs b/src/HotChocolate/Core/src/Types/Types/ObjectTypeExtension.cs index a5f039450b2..1078a672313 100644 --- a/src/HotChocolate/Core/src/Types/Types/ObjectTypeExtension.cs +++ b/src/HotChocolate/Core/src/Types/Types/ObjectTypeExtension.cs @@ -158,41 +158,3 @@ protected override ObjectTypeDefinition CreateDefinition(ITypeDiscoveryContext c } } } - -/// -/// This helper class is used to allow static type extensions. -/// -internal sealed class StaticObjectTypeExtension : ObjectTypeExtension -{ - private readonly Type _staticExtType; - - public StaticObjectTypeExtension(Type staticExtType) - => _staticExtType = staticExtType; - - protected override void Configure(IObjectTypeDescriptor descriptor) - { - var context = descriptor.Extend().Context; - var definition = descriptor.Extend().Definition; - - // we are using the non-generic type extension class which would set nothing. - definition.Name = context.Naming.GetTypeName(_staticExtType, TypeKind.Object); - definition.Description = context.Naming.GetTypeDescription(_staticExtType, TypeKind.Object); - definition.Fields.BindingBehavior = context.Options.DefaultBindingBehavior; - definition.FieldBindingFlags = context.Options.DefaultFieldBindingFlags; - definition.FieldBindingType = _staticExtType; - definition.IsExtension = true; - - // we set the static type as runtime type. Since this is not the actual runtime - // type and is replaced by the actual runtime type of the GraphQL type - // we do not run into any conflicts here. - definition.RuntimeType = _staticExtType; - - // next we set the binding flags to only infer static members. - definition.FieldBindingFlags = FieldBindingFlags.Static; - - // last we use an internal helper to force infer the GraphQL fields from the - // field binding type which is at this moment the runtime type that we have - // set above. - ((ObjectTypeDescriptor)descriptor).InferFieldsFromFieldBindingType(); - } -} diff --git a/src/HotChocolate/Core/src/Types/Types/StaticObjectTypeExtension.cs b/src/HotChocolate/Core/src/Types/Types/StaticObjectTypeExtension.cs new file mode 100644 index 00000000000..3cc75075e09 --- /dev/null +++ b/src/HotChocolate/Core/src/Types/Types/StaticObjectTypeExtension.cs @@ -0,0 +1,43 @@ +#nullable enable +using System; +using HotChocolate.Types.Descriptors; + +namespace HotChocolate.Types; + +/// +/// This helper class is used to allow static type extensions. +/// +internal sealed class StaticObjectTypeExtension : ObjectTypeExtension +{ + private readonly Type _staticExtType; + + public StaticObjectTypeExtension(Type staticExtType) + => _staticExtType = staticExtType; + + protected override void Configure(IObjectTypeDescriptor descriptor) + { + var context = descriptor.Extend().Context; + var definition = descriptor.Extend().Definition; + + // we are using the non-generic type extension class which would set nothing. + definition.Name = context.Naming.GetTypeName(_staticExtType, TypeKind.Object); + definition.Description = context.Naming.GetTypeDescription(_staticExtType, TypeKind.Object); + definition.Fields.BindingBehavior = context.Options.DefaultBindingBehavior; + definition.FieldBindingFlags = context.Options.DefaultFieldBindingFlags; + definition.FieldBindingType = _staticExtType; + definition.IsExtension = true; + + // we set the static type as runtime type. Since this is not the actual runtime + // type and is replaced by the actual runtime type of the GraphQL type + // we do not run into any conflicts here. + definition.RuntimeType = _staticExtType; + + // next we set the binding flags to only infer static members. + definition.FieldBindingFlags = FieldBindingFlags.Static; + + // last we use an internal helper to force infer the GraphQL fields from the + // field binding type which is at this moment the runtime type that we have + // set above. + ((ObjectTypeDescriptor)descriptor).InferFieldsFromFieldBindingType(); + } +}