diff --git a/src/HotChocolate/Caching/src/Caching/CacheControlAttribute.cs b/src/HotChocolate/Caching/src/Caching/CacheControlAttribute.cs index cd3cb657324..387dddcda0e 100644 --- a/src/HotChocolate/Caching/src/Caching/CacheControlAttribute.cs +++ b/src/HotChocolate/Caching/src/Caching/CacheControlAttribute.cs @@ -56,14 +56,14 @@ protected internal override void TryConfigure( /// /// The maximum time, in seconds, this resource can be cached. /// - public int MaxAge { get => _maxAge ?? 0; set => _maxAge = value; } + public int MaxAge { get => _maxAge ?? CacheControlDefaults.MaxAge; set => _maxAge = value; } /// /// The scope of this resource. /// public CacheControlScope Scope { - get => _scope ?? CacheControlScope.Public; + get => _scope ?? CacheControlDefaults.Scope; set => _scope = value; } diff --git a/src/HotChocolate/Caching/src/Caching/CacheControlDefaults.cs b/src/HotChocolate/Caching/src/Caching/CacheControlDefaults.cs new file mode 100644 index 00000000000..57b2d426d75 --- /dev/null +++ b/src/HotChocolate/Caching/src/Caching/CacheControlDefaults.cs @@ -0,0 +1,7 @@ +namespace HotChocolate.Caching; + +internal static class CacheControlDefaults +{ + public const int MaxAge = 0; + public const CacheControlScope Scope = CacheControlScope.Public; +} diff --git a/src/HotChocolate/Caching/src/Caching/CacheControlTypeInterceptor.cs b/src/HotChocolate/Caching/src/Caching/CacheControlTypeInterceptor.cs index c448709ad85..d5f87b689bc 100644 --- a/src/HotChocolate/Caching/src/Caching/CacheControlTypeInterceptor.cs +++ b/src/HotChocolate/Caching/src/Caching/CacheControlTypeInterceptor.cs @@ -94,7 +94,7 @@ private void TryApplyDefaults(RegisteredType type, ObjectTypeDefinition objectDe { // Each field on the query type or data resolver fields // are treated as fields that need to be explicitly cached. - ApplyCacheControlWithDefaultMaxAge(field); + ApplyCacheControlWithDefaults(field); appliedDefaults = true; } } @@ -105,16 +105,28 @@ private void TryApplyDefaults(RegisteredType type, ObjectTypeDefinition objectDe } } - private void ApplyCacheControlWithDefaultMaxAge( + private void ApplyCacheControlWithDefaults( OutputFieldDefinitionBase field) { + var isNotDefaultScope = _cacheControlOptions.DefaultScope != CacheControlDefaults.Scope; + + var arguments = new ArgumentNode[isNotDefaultScope ? 2 : 1]; + arguments[0] = new ArgumentNode( + CacheControlDirectiveType.Names.MaxAgeArgName, + _cacheControlOptions.DefaultMaxAge); + + if (isNotDefaultScope) + { + arguments[1] = new ArgumentNode( + CacheControlDirectiveType.Names.ScopeArgName, + new EnumValueNode(_cacheControlOptions.DefaultScope)); + } + field.Directives.Add( new DirectiveDefinition( new DirectiveNode( CacheControlDirectiveType.Names.DirectiveName, - new ArgumentNode( - CacheControlDirectiveType.Names.MaxAgeArgName, - _cacheControlOptions.DefaultMaxAge)))); + arguments))); } private static bool HasCacheControlDirective(ObjectFieldDefinition field) diff --git a/src/HotChocolate/Caching/src/Caching/Options/CacheControlOptions.cs b/src/HotChocolate/Caching/src/Caching/Options/CacheControlOptions.cs index f5c296158d9..76efdd4edd1 100644 --- a/src/HotChocolate/Caching/src/Caching/Options/CacheControlOptions.cs +++ b/src/HotChocolate/Caching/src/Caching/Options/CacheControlOptions.cs @@ -7,7 +7,10 @@ public sealed class CacheControlOptions : ICacheControlOptions public bool Enable { get; set; } = true; /// - public int DefaultMaxAge { get; set; } = 0; + public int DefaultMaxAge { get; set; } = CacheControlDefaults.MaxAge; + + /// + public CacheControlScope DefaultScope { get; set; } = CacheControlDefaults.Scope; /// public bool ApplyDefaults { get; set; } = true; diff --git a/src/HotChocolate/Caching/src/Caching/Options/ICacheControlOptions.cs b/src/HotChocolate/Caching/src/Caching/Options/ICacheControlOptions.cs index b6a9d94cd7c..767eb780079 100644 --- a/src/HotChocolate/Caching/src/Caching/Options/ICacheControlOptions.cs +++ b/src/HotChocolate/Caching/src/Caching/Options/ICacheControlOptions.cs @@ -18,9 +18,17 @@ public interface ICacheControlOptions int DefaultMaxAge { get; } /// - /// Denotes whether the should be applied to all - /// fields that do not already specify a , - /// are fields on the Query root type or are responsible for fetching data. + /// The Scope that should be applied to fields, + /// if is true. + /// Defaults to Public. + /// + CacheControlScope DefaultScope { get; } + + /// + /// Denotes whether the and + /// should be applied to all fields that do not already specify a + /// , are fields on the Query root type + /// or are responsible for fetching data. /// bool ApplyDefaults { get; } } diff --git a/src/HotChocolate/Caching/test/Caching.Tests/CacheControlTypeInterceptorTests.cs b/src/HotChocolate/Caching/test/Caching.Tests/CacheControlTypeInterceptorTests.cs index 33e7c4cf7df..a83ad29f520 100644 --- a/src/HotChocolate/Caching/test/Caching.Tests/CacheControlTypeInterceptorTests.cs +++ b/src/HotChocolate/Caching/test/Caching.Tests/CacheControlTypeInterceptorTests.cs @@ -48,6 +48,24 @@ type Query { .MatchSnapshotAsync(); } + [Fact] + public async Task QueryFields_ApplyDefaults_DifferentDefaultScope() + { + await new ServiceCollection() + .AddGraphQL() + .AddDocumentFromString(@" + type Query { + field1: String + field2: String @cacheControl(maxAge: 200, scope: PUBLIC) + } + ") + .UseField(_ => _) + .AddCacheControl() + .ModifyCacheControlOptions(o => o.DefaultScope = CacheControlScope.Private) + .BuildSchemaAsync() + .MatchSnapshotAsync(); + } + [Fact] public async Task QueryFields_ApplyDefaults_False() { @@ -107,6 +125,18 @@ public async Task DataResolvers_ApplyDefaults_DifferentDefaultMaxAge() .MatchSnapshotAsync(); } + [Fact] + public async Task DataResolvers_ApplyDefaults_DifferentDefaultScope() + { + await new ServiceCollection() + .AddGraphQL() + .AddQueryType() + .AddCacheControl() + .ModifyCacheControlOptions(o => o.DefaultScope = CacheControlScope.Private) + .BuildSchemaAsync() + .MatchSnapshotAsync(); + } + [Fact] public async Task DataResolvers_ApplyDefaults_False() { diff --git a/src/HotChocolate/Caching/test/Caching.Tests/HttpCachingTests.cs b/src/HotChocolate/Caching/test/Caching.Tests/HttpCachingTests.cs index 9d011736be1..ff3500101b0 100644 --- a/src/HotChocolate/Caching/test/Caching.Tests/HttpCachingTests.cs +++ b/src/HotChocolate/Caching/test/Caching.Tests/HttpCachingTests.cs @@ -60,6 +60,89 @@ public async Task MaxAge_Zero_Should_Cache() result.MatchSnapshot(); } + [Fact] + public async Task Just_Defaults_Should_Cache() + { + var server = CreateServer(services => + { + services.AddGraphQLServer() + .UseQueryCachePipeline() + .AddCacheControl() + .AddQueryType(d => + d.Name("Query") + .Field("field") + .Resolve("")); + }); + + var client = server.CreateClient(); + var result = await client.PostQueryAsync("{ field }"); + + result.MatchSnapshot(); + } + + [Fact] + public async Task No_Applied_Defaults_Should_Not_Cache() + { + var server = CreateServer(services => + { + services.AddGraphQLServer() + .UseQueryCachePipeline() + .AddCacheControl() + .ModifyCacheControlOptions(o => o.ApplyDefaults = false) + .AddQueryType(d => + d.Name("Query") + .Field("field") + .Resolve("")); + }); + + var client = server.CreateClient(); + var result = await client.PostQueryAsync("{ field }"); + + result.MatchSnapshot(); + } + + [Fact] + public async Task Default_Max_Age_Should_Apply_And_Cache() + { + var server = CreateServer(services => + { + services.AddGraphQLServer() + .UseQueryCachePipeline() + .AddCacheControl() + .ModifyCacheControlOptions(o => o.DefaultMaxAge = 1000) + .AddQueryType(d => + d.Name("Query") + .Field("field") + .Resolve("")); + }); + + var client = server.CreateClient(); + var result = await client.PostQueryAsync("{ field }"); + + result.MatchSnapshot(); + } + + [Fact] + public async Task Default_Scope_Should_Apply_And_Cache() + { + var server = CreateServer(services => + { + services.AddGraphQLServer() + .UseQueryCachePipeline() + .AddCacheControl() + .ModifyCacheControlOptions(o => o.DefaultScope = CacheControlScope.Private) + .AddQueryType(d => + d.Name("Query") + .Field("field") + .Resolve("")); + }); + + var client = server.CreateClient(); + var result = await client.PostQueryAsync("{ field }"); + + result.MatchSnapshot(); + } + [Fact] public async Task JustScope_Should_Not_Cache() { diff --git a/src/HotChocolate/Caching/test/Caching.Tests/__snapshots__/CacheControlTypeInterceptorTests.DataResolvers_ApplyDefaults_DifferentDefaultScope.snap b/src/HotChocolate/Caching/test/Caching.Tests/__snapshots__/CacheControlTypeInterceptorTests.DataResolvers_ApplyDefaults_DifferentDefaultScope.snap new file mode 100644 index 00000000000..38b3984176e --- /dev/null +++ b/src/HotChocolate/Caching/test/Caching.Tests/__snapshots__/CacheControlTypeInterceptorTests.DataResolvers_ApplyDefaults_DifferentDefaultScope.snap @@ -0,0 +1,126 @@ +schema { + query: Query +} + +"Information about the offset pagination." +type CollectionSegmentInfo { + "Indicates whether more items exist following the set defined by the clients arguments." + hasNextPage: Boolean! + "Indicates whether more items exist prior the set defined by the clients arguments." + hasPreviousPage: Boolean! +} + +type NestedType { + taskField: String! @cacheControl(maxAge: 0, scope: PRIVATE) + valueTaskField: String! @cacheControl(maxAge: 0, scope: PRIVATE) + executableField: [String!]! @cacheControl(maxAge: 0, scope: PRIVATE) + queryableField: [String!]! @cacheControl(maxAge: 0, scope: PRIVATE) + queryableFieldWithConnection("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): QueryableFieldWithConnectionConnection @cacheControl(maxAge: 0, scope: PRIVATE) + queryableFieldWithCollectionSegment(skip: Int take: Int): QueryableFieldWithCollectionSegmentCollectionSegment @cacheControl(maxAge: 0, scope: PRIVATE) + taskFieldWithCacheControl: String! @cacheControl(maxAge: 200) + valueTaskFieldWithCacheControl: String! @cacheControl(maxAge: 200) + executableFieldWithCacheControl: [String!]! @cacheControl(maxAge: 200) + queryableFieldWithCacheControl: [String!]! @cacheControl(maxAge: 200) + 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! + pureFieldWithCacheControl: String! @cacheControl(maxAge: 200) +} + +type NestedType2 { + taskFieldWithInheritMaxAge: String! @cacheControl(inheritMaxAge: true) +} + +"Information about pagination in a connection." +type PageInfo { + "Indicates whether more edges exist following the set defined by the clients arguments." + hasNextPage: Boolean! + "Indicates whether more edges exist prior the set defined by the clients arguments." + hasPreviousPage: Boolean! + "When paginating backwards, the cursor to continue." + startCursor: String + "When paginating forwards, the cursor to continue." + endCursor: String +} + +type Query { + taskField: String! @cacheControl(maxAge: 0, scope: PRIVATE) + valueTaskField: String! @cacheControl(maxAge: 0, scope: PRIVATE) + executableField: [String!]! @cacheControl(maxAge: 0, scope: PRIVATE) + queryableField: [String!]! @cacheControl(maxAge: 0, scope: PRIVATE) + queryableFieldWithConnection("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): QueryableFieldWithConnectionConnection @cacheControl(maxAge: 0, scope: PRIVATE) + queryableFieldWithCollectionSegment(skip: Int take: Int): QueryableFieldWithCollectionSegmentCollectionSegment @cacheControl(maxAge: 0, scope: PRIVATE) + taskFieldWithCacheControl: String! @cacheControl(maxAge: 200) + valueTaskFieldWithCacheControl: String! @cacheControl(maxAge: 200) + executableFieldWithCacheControl: [String!]! @cacheControl(maxAge: 200) + queryableFieldWithCacheControl: [String!]! @cacheControl(maxAge: 200) + 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, scope: PRIVATE) + nested2: NestedType2! @cacheControl(maxAge: 0, scope: PRIVATE) + pureField: String! @cacheControl(maxAge: 0, scope: PRIVATE) + pureFieldWithCacheControl: String! @cacheControl(maxAge: 200) +} + +"A segment of a collection." +type QueryableFieldWithCollectionSegmentCollectionSegment { + "Information to aid in pagination." + pageInfo: CollectionSegmentInfo! + "A flattened list of the items." + items: [String!] +} + +"A segment of a collection." +type QueryableFieldWithCollectionSegmentWithCacheControlCollectionSegment { + "Information to aid in pagination." + pageInfo: CollectionSegmentInfo! + "A flattened list of the items." + items: [String!] +} + +"A connection to a list of items." +type QueryableFieldWithConnectionConnection { + "Information to aid in pagination." + pageInfo: PageInfo! + "A list of edges." + edges: [QueryableFieldWithConnectionEdge!] + "A flattened list of the nodes." + nodes: [String!] +} + +"An edge in a connection." +type QueryableFieldWithConnectionEdge { + "A cursor for use in pagination." + cursor: String! + "The item at the end of the edge." + node: String! +} + +"A connection to a list of items." +type QueryableFieldWithConnectionWithCacheControlConnection { + "Information to aid in pagination." + pageInfo: PageInfo! + "A list of edges." + edges: [QueryableFieldWithConnectionWithCacheControlEdge!] + "A flattened list of the nodes." + nodes: [String!] +} + +"An edge in a connection." +type QueryableFieldWithConnectionWithCacheControlEdge { + "A cursor for use in pagination." + cursor: String! + "The item at the end of the edge." + node: String! +} + +"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 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 1d68183a810..a0db67a45b2 100644 --- a/src/HotChocolate/Caching/test/Caching.Tests/__snapshots__/CacheControlTypeInterceptorTests.InheritMaxAgeOnQueryTypeField.snap +++ b/src/HotChocolate/Caching/test/Caching.Tests/__snapshots__/CacheControlTypeInterceptorTests.InheritMaxAgeOnQueryTypeField.snap @@ -4,9 +4,3 @@ "extensions": {} } -{ - "message": "Can not specify `inheritMaxAge: true` and a value for `maxAge` for the @cacheControl directive on the field Query.field.", - "type": "Query", - "extensions": {} -} - diff --git a/src/HotChocolate/Caching/test/Caching.Tests/__snapshots__/CacheControlTypeInterceptorTests.QueryFields_ApplyDefaults_DifferentDefaultScope.snap b/src/HotChocolate/Caching/test/Caching.Tests/__snapshots__/CacheControlTypeInterceptorTests.QueryFields_ApplyDefaults_DifferentDefaultScope.snap new file mode 100644 index 00000000000..7d993f37d18 --- /dev/null +++ b/src/HotChocolate/Caching/test/Caching.Tests/__snapshots__/CacheControlTypeInterceptorTests.QueryFields_ApplyDefaults_DifferentDefaultScope.snap @@ -0,0 +1,19 @@ +schema { + query: Query +} + +type Query { + field1: String @cacheControl(maxAge: 0, scope: PRIVATE) + field2: String @cacheControl(maxAge: 200, scope: PUBLIC) +} + +"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 diff --git a/src/HotChocolate/Caching/test/Caching.Tests/__snapshots__/HttpCachingTests.Default_Max_Age_Should_Apply_And_Cache.snap b/src/HotChocolate/Caching/test/Caching.Tests/__snapshots__/HttpCachingTests.Default_Max_Age_Should_Apply_And_Cache.snap new file mode 100644 index 00000000000..d21e9c75222 --- /dev/null +++ b/src/HotChocolate/Caching/test/Caching.Tests/__snapshots__/HttpCachingTests.Default_Max_Age_Should_Apply_And_Cache.snap @@ -0,0 +1,19 @@ +{ + "Headers": [ + { + "Key": "Cache-Control", + "Value": [ + "public, max-age=1000" + ] + } + ], + "ContentHeaders": [ + { + "Key": "Content-Type", + "Value": [ + "application/graphql-response+json; charset=utf-8" + ] + } + ], + "Body": "{}" +} \ No newline at end of file diff --git a/src/HotChocolate/Caching/test/Caching.Tests/__snapshots__/HttpCachingTests.Default_Scope_Should_Apply_And_Cache.snap b/src/HotChocolate/Caching/test/Caching.Tests/__snapshots__/HttpCachingTests.Default_Scope_Should_Apply_And_Cache.snap new file mode 100644 index 00000000000..d0d552ebc17 --- /dev/null +++ b/src/HotChocolate/Caching/test/Caching.Tests/__snapshots__/HttpCachingTests.Default_Scope_Should_Apply_And_Cache.snap @@ -0,0 +1,19 @@ +{ + "Headers": [ + { + "Key": "Cache-Control", + "Value": [ + "max-age=0, private" + ] + } + ], + "ContentHeaders": [ + { + "Key": "Content-Type", + "Value": [ + "application/graphql-response+json; charset=utf-8" + ] + } + ], + "Body": "{}" +} \ No newline at end of file diff --git a/src/HotChocolate/Caching/test/Caching.Tests/__snapshots__/HttpCachingTests.Just_Defaults_Should_Cache.snap b/src/HotChocolate/Caching/test/Caching.Tests/__snapshots__/HttpCachingTests.Just_Defaults_Should_Cache.snap new file mode 100644 index 00000000000..0a3c8d90948 --- /dev/null +++ b/src/HotChocolate/Caching/test/Caching.Tests/__snapshots__/HttpCachingTests.Just_Defaults_Should_Cache.snap @@ -0,0 +1,19 @@ +{ + "Headers": [ + { + "Key": "Cache-Control", + "Value": [ + "public, max-age=0" + ] + } + ], + "ContentHeaders": [ + { + "Key": "Content-Type", + "Value": [ + "application/graphql-response+json; charset=utf-8" + ] + } + ], + "Body": "{}" +} \ No newline at end of file diff --git a/src/HotChocolate/Caching/test/Caching.Tests/__snapshots__/HttpCachingTests.No_Applied_Defaults_Should_Not_Cache.snap b/src/HotChocolate/Caching/test/Caching.Tests/__snapshots__/HttpCachingTests.No_Applied_Defaults_Should_Not_Cache.snap new file mode 100644 index 00000000000..140ac8d2711 --- /dev/null +++ b/src/HotChocolate/Caching/test/Caching.Tests/__snapshots__/HttpCachingTests.No_Applied_Defaults_Should_Not_Cache.snap @@ -0,0 +1,12 @@ +{ + "Headers": [], + "ContentHeaders": [ + { + "Key": "Content-Type", + "Value": [ + "application/graphql-response+json; charset=utf-8" + ] + } + ], + "Body": "{\"data\":{\"field\":\"\"}}" +} \ No newline at end of file diff --git a/src/HotChocolate/Core/src/Types/Types/Argument.cs b/src/HotChocolate/Core/src/Types/Types/Argument.cs index 960d1676cfc..ef59601657f 100644 --- a/src/HotChocolate/Core/src/Types/Types/Argument.cs +++ b/src/HotChocolate/Core/src/Types/Types/Argument.cs @@ -82,7 +82,7 @@ protected sealed override void OnCompleteField( ITypeSystemMember declaringMember, FieldDefinitionBase definition) => OnCompleteField(context, declaringMember, (ArgumentDefinition)definition); - + protected virtual void OnCompleteField( ITypeCompletionContext context, ITypeSystemMember declaringMember, @@ -94,7 +94,7 @@ protected virtual void OnCompleteField( .SetMessage(TypeResources.Argument_TypeIsNull, definition.Name) .SetTypeSystemObject(context.Type) .SetExtension("declaringMember", declaringMember) - .SetExtension("name", definition.Name.ToString()) + .SetExtension("name", definition.Name) .Build()); return; } @@ -102,7 +102,7 @@ protected virtual void OnCompleteField( base.OnCompleteField(context, declaringMember, definition); Type = context.GetType(definition.Type!).EnsureInputType(); - _runtimeType = definition.RuntimeType ?? definition.Parameter?.ParameterType!; + _runtimeType = definition.GetRuntimeType()!; _runtimeType = CompleteRuntimeType(Type, _runtimeType, out var isOptional); DefaultValue = CompleteDefaultValue(context, definition, Type, Coordinate); IsOptional = isOptional; diff --git a/src/HotChocolate/Core/src/Types/Types/Descriptors/Definitions/ArgumentDefinition.cs b/src/HotChocolate/Core/src/Types/Types/Descriptors/Definitions/ArgumentDefinition.cs index 0db6692e639..db6d0f1ad71 100644 --- a/src/HotChocolate/Core/src/Types/Types/Descriptors/Definitions/ArgumentDefinition.cs +++ b/src/HotChocolate/Core/src/Types/Types/Descriptors/Definitions/ArgumentDefinition.cs @@ -48,6 +48,8 @@ public ArgumentDefinition( public IList Formatters => _formatters ??= []; + public virtual Type? GetRuntimeType() => RuntimeType ?? Parameter?.ParameterType; + public IReadOnlyList GetFormatters() { if (_formatters is null) diff --git a/src/HotChocolate/Core/src/Types/Types/Descriptors/Definitions/DirectiveArgumentDefinition.cs b/src/HotChocolate/Core/src/Types/Types/Descriptors/Definitions/DirectiveArgumentDefinition.cs index 5add37e6681..b821d693953 100644 --- a/src/HotChocolate/Core/src/Types/Types/Descriptors/Definitions/DirectiveArgumentDefinition.cs +++ b/src/HotChocolate/Core/src/Types/Types/Descriptors/Definitions/DirectiveArgumentDefinition.cs @@ -1,3 +1,4 @@ +using System; using System.Reflection; using HotChocolate.Language; using HotChocolate.Utilities; @@ -37,4 +38,6 @@ public DirectiveArgumentDefinition( /// The property to which this argument binds to. /// public PropertyInfo? Property { get; set; } + + public override Type? GetRuntimeType() => RuntimeType ?? Property?.PropertyType; } diff --git a/src/HotChocolate/Core/test/Types.Tests/Types/Directives/TagDirectiveTests.cs b/src/HotChocolate/Core/test/Types.Tests/Types/Directives/TagDirectiveTests.cs index 277ee776a18..c3ee4841bc2 100644 --- a/src/HotChocolate/Core/test/Types.Tests/Types/Directives/TagDirectiveTests.cs +++ b/src/HotChocolate/Core/test/Types.Tests/Types/Directives/TagDirectiveTests.cs @@ -21,7 +21,7 @@ public async Task EnsureAllLocationsAreApplied() schema.MatchSnapshot(); } - + [Fact] public async Task SchemaFirst_Tag() { @@ -33,10 +33,10 @@ public async Task SchemaFirst_Tag() type Query { field: String @tag(name: "abc") } - - directive @tag("The name of the tag." name: String!) - repeatable on SCHEMA | SCALAR | OBJECT | FIELD_DEFINITION | - ARGUMENT_DEFINITION | INTERFACE | UNION | ENUM | ENUM_VALUE | + + directive @tag("The name of the tag." name: String!) + repeatable on SCHEMA | SCALAR | OBJECT | FIELD_DEFINITION | + ARGUMENT_DEFINITION | INTERFACE | UNION | ENUM | ENUM_VALUE | INPUT_OBJECT | INPUT_FIELD_DEFINITION """) .UseField(_ => _ => default)