From 841ed7cbd5ca8a79852e6716958352e25440a446 Mon Sep 17 00:00:00 2001 From: Ben McCallum Date: Wed, 30 Oct 2019 17:00:53 +0100 Subject: [PATCH 1/3] feat: ValidateInstanceAsync extension --- .../FluentValidationExtensions.cs | 12 ++++++ src/Tests/Arguments/AsyncComplexInput.cs | 4 ++ src/Tests/Arguments/AsyncComplexInputGraph.cs | 10 +++++ .../Arguments/AsyncComplexInputValidator.cs | 16 +++++++ ...tionTests.AsyncComplexInvalid.approved.txt | 13 ++++++ ...rationTests.AsyncComplexValid.approved.txt | 7 +++ src/Tests/IntegrationTests.cs | 43 +++++++++++++++++++ src/Tests/Query.cs | 16 +++++++ 8 files changed, 121 insertions(+) create mode 100644 src/Tests/Arguments/AsyncComplexInput.cs create mode 100644 src/Tests/Arguments/AsyncComplexInputGraph.cs create mode 100644 src/Tests/Arguments/AsyncComplexInputValidator.cs create mode 100644 src/Tests/IntegrationTests.AsyncComplexInvalid.approved.txt create mode 100644 src/Tests/IntegrationTests.AsyncComplexValid.approved.txt diff --git a/src/GraphQL.FluentValidation/FluentValidationExtensions.cs b/src/GraphQL.FluentValidation/FluentValidationExtensions.cs index e3b8b1ea..4b28a54c 100644 --- a/src/GraphQL.FluentValidation/FluentValidationExtensions.cs +++ b/src/GraphQL.FluentValidation/FluentValidationExtensions.cs @@ -1,6 +1,7 @@ using FluentValidation; using GraphQL.FluentValidation; using GraphQL.Types; +using System.Threading.Tasks; namespace GraphQL { @@ -20,6 +21,17 @@ public static void ValidateInstance(this ResolveFieldContext ArgumentValidation.Validate(ArgumentTypeCacheBag.GetCache(context), type, input, context.UserContext); } + /// + /// Validate an instance asynchronously against the current cached validators defined by . + /// + public static async Task ValidateInstanceAsync(this ResolveFieldContext context, TInstance input) + { + Guard.AgainstNull(context, nameof(context)); + Guard.AgainstNull(input, nameof(input)); + var type = input!.GetType(); + await ArgumentValidation.ValidateAsync(ArgumentTypeCacheBag.GetCache(context), type, input, context.UserContext); + } + /// /// Adds a FieldMiddleware to the GraphQL pipeline that converts a to s./> /// diff --git a/src/Tests/Arguments/AsyncComplexInput.cs b/src/Tests/Arguments/AsyncComplexInput.cs new file mode 100644 index 00000000..6ed24650 --- /dev/null +++ b/src/Tests/Arguments/AsyncComplexInput.cs @@ -0,0 +1,4 @@ +public class AsyncComplexInput +{ + public ComplexInputInner? Inner { get; set; } +} \ No newline at end of file diff --git a/src/Tests/Arguments/AsyncComplexInputGraph.cs b/src/Tests/Arguments/AsyncComplexInputGraph.cs new file mode 100644 index 00000000..be36a5ba --- /dev/null +++ b/src/Tests/Arguments/AsyncComplexInputGraph.cs @@ -0,0 +1,10 @@ +using GraphQL.Types; + +public class AsyncComplexInputGraph : + InputObjectGraphType +{ + public AsyncComplexInputGraph() + { + Field("inner"); + } +} \ No newline at end of file diff --git a/src/Tests/Arguments/AsyncComplexInputValidator.cs b/src/Tests/Arguments/AsyncComplexInputValidator.cs new file mode 100644 index 00000000..0c10b6ec --- /dev/null +++ b/src/Tests/Arguments/AsyncComplexInputValidator.cs @@ -0,0 +1,16 @@ +using FluentValidation; +using System.Threading.Tasks; + +public class AsyncComplexInputValidator : + AbstractValidator +{ + public AsyncComplexInputValidator() + { + RuleFor(_ => _.Inner!) + .NotEmpty() + .MustAsync((o, token) => { + return Task.FromResult(o != null && !string.IsNullOrWhiteSpace(o.Content)); + }).WithMessage("Inner async test failed msg.") + .SetValidator(new ComplexInputInnerValidator()); + } +} \ No newline at end of file diff --git a/src/Tests/IntegrationTests.AsyncComplexInvalid.approved.txt b/src/Tests/IntegrationTests.AsyncComplexInvalid.approved.txt new file mode 100644 index 00000000..17aa2d14 --- /dev/null +++ b/src/Tests/IntegrationTests.AsyncComplexInvalid.approved.txt @@ -0,0 +1,13 @@ +{ + data: { + asyncComplexInputQuery: null + }, + errors: [ + { + message: 'Inner: Inner async test failed msg.' + }, + { + message: 'Inner.Content: \'Content\' must not be empty.' + } + ] +} \ No newline at end of file diff --git a/src/Tests/IntegrationTests.AsyncComplexValid.approved.txt b/src/Tests/IntegrationTests.AsyncComplexValid.approved.txt new file mode 100644 index 00000000..5b85e1c9 --- /dev/null +++ b/src/Tests/IntegrationTests.AsyncComplexValid.approved.txt @@ -0,0 +1,7 @@ +{ + data: { + asyncComplexInputQuery: { + data: 'TheContent' + } + } +} \ No newline at end of file diff --git a/src/Tests/IntegrationTests.cs b/src/Tests/IntegrationTests.cs index 6fa50da1..a94c19f7 100644 --- a/src/Tests/IntegrationTests.cs +++ b/src/Tests/IntegrationTests.cs @@ -151,6 +151,49 @@ public async Task ComplexInvalid2() ObjectApprover.Verify(result); } + [Fact] + public async Task AsyncComplexValid() + { + var queryString = @" +{ + asyncComplexInputQuery + ( + input: { + inner: { + content: ""TheContent"" + } + } + ) + { + data + } +}"; + var result = await QueryExecutor.ExecuteQuery(queryString, null, typeCache); + ObjectApprover.Verify(result); + } + + [Fact] + public async Task AsyncComplexInvalid() + { + var queryString = @" +{ + asyncComplexInputQuery + ( + input: { + inner: { + content: """" + } + } + ) + { + data + } +}"; + var result = await QueryExecutor.ExecuteQuery(queryString, null, typeCache); + ObjectApprover.Verify(result); + } + + public IntegrationTests(ITestOutputHelper output) : base(output) { diff --git a/src/Tests/Query.cs b/src/Tests/Query.cs index 39d67a86..de68120f 100644 --- a/src/Tests/Query.cs +++ b/src/Tests/Query.cs @@ -52,6 +52,22 @@ public Query() }; } ); + + FieldAsync( + "asyncComplexInputQuery", + arguments: new QueryArguments( + new QueryArgument { Name = "input", } + ), + resolve: async context => + { + var input = JToken.FromObject(context.Arguments["input"]).ToObject(); + await context.ValidateInstanceAsync(input); + return new Result + { + Data = input.Inner!.Content + }; + } + ); } } \ No newline at end of file From fa48a6ccf4fe595b76924b4d45df088c1397cd98 Mon Sep 17 00:00:00 2001 From: Ben McCallum Date: Tue, 5 Nov 2019 14:36:47 +0100 Subject: [PATCH 2/3] Remove unnecessary extensions and mark internals as internals again Update tests to prove complex arguments with complex, nested inners are indeed deserialized correctly. --- .../ArgumentValidation.cs | 4 ++-- .../FluentValidationExtensions.cs | 24 ------------------- src/Tests/Arguments/ComplexInput.cs | 6 ++++- src/Tests/Arguments/ComplexInputGraph.cs | 2 ++ src/Tests/Arguments/ComplexInputListItem.cs | 5 ++++ .../Arguments/ComplexInputListItemGraph.cs | 16 +++++++++++++ .../ComplexInputListItemValidator.cs | 14 +++++++++++ src/Tests/Arguments/ComplexInputValidator.cs | 4 ++++ ...tegrationTests.ComplexInvalid.approved.txt | 3 +++ ...egrationTests.ComplexInvalid2.approved.txt | 3 +++ ...IntegrationTests.ComplexValid.approved.txt | 2 +- src/Tests/IntegrationTests.cs | 21 ++++++++++++---- src/Tests/Query.cs | 10 ++++---- 13 files changed, 75 insertions(+), 39 deletions(-) create mode 100644 src/Tests/Arguments/ComplexInputListItem.cs create mode 100644 src/Tests/Arguments/ComplexInputListItemGraph.cs create mode 100644 src/Tests/Arguments/ComplexInputListItemValidator.cs diff --git a/src/GraphQL.FluentValidation/ArgumentValidation.cs b/src/GraphQL.FluentValidation/ArgumentValidation.cs index 5db2d07d..50ff2d1e 100644 --- a/src/GraphQL.FluentValidation/ArgumentValidation.cs +++ b/src/GraphQL.FluentValidation/ArgumentValidation.cs @@ -8,7 +8,7 @@ #pragma warning disable 1591 static class ArgumentValidation { - public static async Task ValidateAsync(ValidatorTypeCache typeCache, Type type, object? instance, object userContext) + internal static async Task ValidateAsync(ValidatorTypeCache typeCache, Type type, object? instance, object userContext) { Guard.AgainstNull(typeCache, nameof(typeCache)); Guard.AgainstNull(userContext, nameof(userContext)); @@ -31,7 +31,7 @@ public static async Task ValidateAsync(ValidatorTypeCache typeCache, Type type, ThrowIfResults(results); } - public static void Validate(ValidatorTypeCache typeCache, Type type, object? instance, object userContext) + internal static void Validate(ValidatorTypeCache typeCache, Type type, object? instance, object userContext) { Guard.AgainstNull(typeCache, nameof(typeCache)); Guard.AgainstNull(userContext, nameof(userContext)); diff --git a/src/GraphQL.FluentValidation/FluentValidationExtensions.cs b/src/GraphQL.FluentValidation/FluentValidationExtensions.cs index 4b28a54c..88826ef6 100644 --- a/src/GraphQL.FluentValidation/FluentValidationExtensions.cs +++ b/src/GraphQL.FluentValidation/FluentValidationExtensions.cs @@ -1,7 +1,5 @@ using FluentValidation; using GraphQL.FluentValidation; -using GraphQL.Types; -using System.Threading.Tasks; namespace GraphQL { @@ -10,28 +8,6 @@ namespace GraphQL /// public static partial class FluentValidationExtensions { - /// - /// Validate an instance against the current cached validators defined by . - /// - public static void ValidateInstance(this ResolveFieldContext context, TInstance input) - { - Guard.AgainstNull(context, nameof(context)); - Guard.AgainstNull(input, nameof(input)); - var type = input!.GetType(); - ArgumentValidation.Validate(ArgumentTypeCacheBag.GetCache(context), type, input, context.UserContext); - } - - /// - /// Validate an instance asynchronously against the current cached validators defined by . - /// - public static async Task ValidateInstanceAsync(this ResolveFieldContext context, TInstance input) - { - Guard.AgainstNull(context, nameof(context)); - Guard.AgainstNull(input, nameof(input)); - var type = input!.GetType(); - await ArgumentValidation.ValidateAsync(ArgumentTypeCacheBag.GetCache(context), type, input, context.UserContext); - } - /// /// Adds a FieldMiddleware to the GraphQL pipeline that converts a to s./> /// diff --git a/src/Tests/Arguments/ComplexInput.cs b/src/Tests/Arguments/ComplexInput.cs index 27c01f11..2f0a5423 100644 --- a/src/Tests/Arguments/ComplexInput.cs +++ b/src/Tests/Arguments/ComplexInput.cs @@ -1,4 +1,8 @@ -public class ComplexInput +using System.Collections.Generic; + +public class ComplexInput { public ComplexInputInner? Inner { get; set; } + + public List? Items { get; set; } } \ No newline at end of file diff --git a/src/Tests/Arguments/ComplexInputGraph.cs b/src/Tests/Arguments/ComplexInputGraph.cs index 2c6e706e..68ea92fd 100644 --- a/src/Tests/Arguments/ComplexInputGraph.cs +++ b/src/Tests/Arguments/ComplexInputGraph.cs @@ -6,5 +6,7 @@ public class ComplexInputGraph : public ComplexInputGraph() { Field("inner"); + + Field>>("items"); } } \ No newline at end of file diff --git a/src/Tests/Arguments/ComplexInputListItem.cs b/src/Tests/Arguments/ComplexInputListItem.cs new file mode 100644 index 00000000..376739a7 --- /dev/null +++ b/src/Tests/Arguments/ComplexInputListItem.cs @@ -0,0 +1,5 @@ +public class ComplexInputListItem +{ + public int Id { get; set; } + public string? Content { get; set; } +} \ No newline at end of file diff --git a/src/Tests/Arguments/ComplexInputListItemGraph.cs b/src/Tests/Arguments/ComplexInputListItemGraph.cs new file mode 100644 index 00000000..878d5148 --- /dev/null +++ b/src/Tests/Arguments/ComplexInputListItemGraph.cs @@ -0,0 +1,16 @@ +using GraphQL.Types; + +public class ComplexInputListItemGraph : + InputObjectGraphType +{ + public ComplexInputListItemGraph() + { + Field>() + .Name("id") + .Resolve(ctx => ctx.Source.Id); + + Field() + .Name("content") + .Resolve(ctx => ctx.Source.Content); + } +} \ No newline at end of file diff --git a/src/Tests/Arguments/ComplexInputListItemValidator.cs b/src/Tests/Arguments/ComplexInputListItemValidator.cs new file mode 100644 index 00000000..08056e83 --- /dev/null +++ b/src/Tests/Arguments/ComplexInputListItemValidator.cs @@ -0,0 +1,14 @@ +using FluentValidation; + +public class ComplexInputListItemValidator : + AbstractValidator +{ + public ComplexInputListItemValidator() + { + RuleFor(_ => _.Id) + .NotEmpty(); + + RuleFor(_ => _.Content) + .NotEmpty(); + } +} \ No newline at end of file diff --git a/src/Tests/Arguments/ComplexInputValidator.cs b/src/Tests/Arguments/ComplexInputValidator.cs index 52831278..ef74fbf0 100644 --- a/src/Tests/Arguments/ComplexInputValidator.cs +++ b/src/Tests/Arguments/ComplexInputValidator.cs @@ -8,5 +8,9 @@ public ComplexInputValidator() RuleFor(_ => _.Inner!) .NotEmpty() .SetValidator(new ComplexInputInnerValidator()); + + RuleFor(_ => _.Items) + .NotEmpty() + .ForEach(i => i.SetValidator(new ComplexInputListItemValidator())); } } \ No newline at end of file diff --git a/src/Tests/IntegrationTests.ComplexInvalid.approved.txt b/src/Tests/IntegrationTests.ComplexInvalid.approved.txt index 7827fe82..a629e50e 100644 --- a/src/Tests/IntegrationTests.ComplexInvalid.approved.txt +++ b/src/Tests/IntegrationTests.ComplexInvalid.approved.txt @@ -5,6 +5,9 @@ errors: [ { message: 'Inner.Content: \'Content\' must not be empty.' + }, + { + message: 'Items: \'Items\' must not be empty.' } ] } \ No newline at end of file diff --git a/src/Tests/IntegrationTests.ComplexInvalid2.approved.txt b/src/Tests/IntegrationTests.ComplexInvalid2.approved.txt index 894a6e86..450ea24c 100644 --- a/src/Tests/IntegrationTests.ComplexInvalid2.approved.txt +++ b/src/Tests/IntegrationTests.ComplexInvalid2.approved.txt @@ -5,6 +5,9 @@ errors: [ { message: 'Inner: \'Inner\' must not be empty.' + }, + { + message: 'Items: \'Items\' must not be empty.' } ] } \ No newline at end of file diff --git a/src/Tests/IntegrationTests.ComplexValid.approved.txt b/src/Tests/IntegrationTests.ComplexValid.approved.txt index c1d5c696..b087116b 100644 --- a/src/Tests/IntegrationTests.ComplexValid.approved.txt +++ b/src/Tests/IntegrationTests.ComplexValid.approved.txt @@ -1,7 +1,7 @@ { data: { complexInputQuery: { - data: 'TheContent' + data: '{"Inner":{"Content":"TheContent"},"Items":[{"Id":1,"Content":"Some content 1"},{"Id":2,"Content":"Some content 2"}]}' } } } \ No newline at end of file diff --git a/src/Tests/IntegrationTests.cs b/src/Tests/IntegrationTests.cs index a94c19f7..c23bf11c 100644 --- a/src/Tests/IntegrationTests.cs +++ b/src/Tests/IntegrationTests.cs @@ -100,7 +100,11 @@ public async Task ComplexValid() input: { inner: { content: ""TheContent"" - } + }, + items: [ + { id: 1, content: ""Some content 1"" }, + { id: 2, content: ""Some content 2"" } + ] } ) { @@ -121,7 +125,8 @@ public async Task ComplexInvalid() input: { inner: { content: """" - } + }, + items: [] } ) { @@ -140,7 +145,8 @@ public async Task ComplexInvalid2() complexInputQuery ( input: { - inner: null + inner: null, + items: null } ) { @@ -161,7 +167,11 @@ public async Task AsyncComplexValid() input: { inner: { content: ""TheContent"" - } + }, + items: [ + { id: 1, content: ""Some content 1"" }, + { id: 2, content: ""Some content 2"" } + ] } ) { @@ -182,7 +192,8 @@ public async Task AsyncComplexInvalid() input: { inner: { content: """" - } + }, + items: null } ) { diff --git a/src/Tests/Query.cs b/src/Tests/Query.cs index de68120f..5df5f822 100644 --- a/src/Tests/Query.cs +++ b/src/Tests/Query.cs @@ -1,6 +1,6 @@ using GraphQL; using GraphQL.Types; -using Newtonsoft.Json.Linq; +using Newtonsoft.Json; public class Query : ObjectGraphType @@ -29,11 +29,10 @@ public Query() ), resolve: context => { - var input = JToken.FromObject(context.Arguments["input"]).ToObject(); - context.ValidateInstance(input); + var input = context.GetValidatedArgument("input"); return new Result { - Data = input.Inner!.Content + Data = JsonConvert.SerializeObject(input) }; } ); @@ -60,8 +59,7 @@ public Query() ), resolve: async context => { - var input = JToken.FromObject(context.Arguments["input"]).ToObject(); - await context.ValidateInstanceAsync(input); + var input = await context.GetValidatedArgumentAsync("input"); return new Result { Data = input.Inner!.Content From 849f2179ed64166f3115f590739dfb584b6719a3 Mon Sep 17 00:00:00 2001 From: Simon Cropp Date: Sat, 9 Nov 2019 10:52:49 +1100 Subject: [PATCH 3/3] Update ArgumentValidation.cs --- src/GraphQL.FluentValidation/ArgumentValidation.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/GraphQL.FluentValidation/ArgumentValidation.cs b/src/GraphQL.FluentValidation/ArgumentValidation.cs index 50ff2d1e..8f09a682 100644 --- a/src/GraphQL.FluentValidation/ArgumentValidation.cs +++ b/src/GraphQL.FluentValidation/ArgumentValidation.cs @@ -8,7 +8,7 @@ #pragma warning disable 1591 static class ArgumentValidation { - internal static async Task ValidateAsync(ValidatorTypeCache typeCache, Type type, object? instance, object userContext) + public static async Task ValidateAsync(ValidatorTypeCache typeCache, Type type, object? instance, object userContext) { Guard.AgainstNull(typeCache, nameof(typeCache)); Guard.AgainstNull(userContext, nameof(userContext)); @@ -31,7 +31,7 @@ internal static async Task ValidateAsync(ValidatorTypeCache typeCache, Type type ThrowIfResults(results); } - internal static void Validate(ValidatorTypeCache typeCache, Type type, object? instance, object userContext) + public static void Validate(ValidatorTypeCache typeCache, Type type, object? instance, object userContext) { Guard.AgainstNull(typeCache, nameof(typeCache)); Guard.AgainstNull(userContext, nameof(userContext)); @@ -64,4 +64,4 @@ static ValidationContext BuildValidationContext(object? instance, object userCon validationContext.RootContextData.Add("UserContext", userContext); return validationContext; } -} \ No newline at end of file +}