From 3d7d6e3cf3447b23030968be0fae4798d81442a3 Mon Sep 17 00:00:00 2001 From: Michael Staib Date: Sun, 7 Mar 2021 21:16:01 +0100 Subject: [PATCH] Started reworking for better perf --- .vscode/tasks.json | 24 ++++++++ Graph.ArgumentValidator.sln | 48 ++++++++++++++++ src/Graph.ArgumentValidator.csproj | 3 +- src/Graph.ArgumentValidator.sln | 25 -------- ...cs => RequestExecutorBuilderExtensions.cs} | 4 +- src/ValidatableAttribute.cs | 2 +- src/ValidationMiddleware.cs | 28 ++------- src/ValidationTypeInterceptor.cs | 57 +++++++++++++++++++ test/Graph.ArgumentValidator.Tests.csproj | 27 +++++++++ test/UnitTest1.cs | 31 ++++++++++ ...onTests.Ensure_Validation_Is_Executed.snap | 5 ++ 11 files changed, 203 insertions(+), 51 deletions(-) create mode 100644 .vscode/tasks.json create mode 100644 Graph.ArgumentValidator.sln delete mode 100644 src/Graph.ArgumentValidator.sln rename src/{Extensions.cs => RequestExecutorBuilderExtensions.cs} (70%) create mode 100644 src/ValidationTypeInterceptor.cs create mode 100644 test/Graph.ArgumentValidator.Tests.csproj create mode 100644 test/UnitTest1.cs create mode 100644 test/__snapshots__/ValidationTests.Ensure_Validation_Is_Executed.snap diff --git a/.vscode/tasks.json b/.vscode/tasks.json new file mode 100644 index 0000000..31c32bd --- /dev/null +++ b/.vscode/tasks.json @@ -0,0 +1,24 @@ +{ + // See https://go.microsoft.com/fwlink/?LinkId=733558 + // for the documentation about the tasks.json format + "version": "2.0.0", + "tasks": [ + { + "label": "build", + "command": "dotnet", + "type": "shell", + "args": [ + "build", + // Ask dotnet build to generate full paths for file names. + "/property:GenerateFullPaths=true", + // Do not generate summary otherwise it leads to duplicate errors in Problems panel + "/consoleloggerparameters:NoSummary" + ], + "group": "build", + "presentation": { + "reveal": "silent" + }, + "problemMatcher": "$msCompile" + } + ] +} \ No newline at end of file diff --git a/Graph.ArgumentValidator.sln b/Graph.ArgumentValidator.sln new file mode 100644 index 0000000..fe60a34 --- /dev/null +++ b/Graph.ArgumentValidator.sln @@ -0,0 +1,48 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio 15 +VisualStudioVersion = 15.0.26124.0 +MinimumVisualStudioVersion = 15.0.26124.0 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Graph.ArgumentValidator", "src\Graph.ArgumentValidator.csproj", "{59435C05-1155-47A5-9361-2702B320EB83}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Graph.ArgumentValidator.Tests", "test\Graph.ArgumentValidator.Tests.csproj", "{F31CE4C8-0B88-492B-9AE2-92D731D51B88}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Debug|x64 = Debug|x64 + Debug|x86 = Debug|x86 + Release|Any CPU = Release|Any CPU + Release|x64 = Release|x64 + Release|x86 = Release|x86 + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {59435C05-1155-47A5-9361-2702B320EB83}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {59435C05-1155-47A5-9361-2702B320EB83}.Debug|Any CPU.Build.0 = Debug|Any CPU + {59435C05-1155-47A5-9361-2702B320EB83}.Debug|x64.ActiveCfg = Debug|Any CPU + {59435C05-1155-47A5-9361-2702B320EB83}.Debug|x64.Build.0 = Debug|Any CPU + {59435C05-1155-47A5-9361-2702B320EB83}.Debug|x86.ActiveCfg = Debug|Any CPU + {59435C05-1155-47A5-9361-2702B320EB83}.Debug|x86.Build.0 = Debug|Any CPU + {59435C05-1155-47A5-9361-2702B320EB83}.Release|Any CPU.ActiveCfg = Release|Any CPU + {59435C05-1155-47A5-9361-2702B320EB83}.Release|Any CPU.Build.0 = Release|Any CPU + {59435C05-1155-47A5-9361-2702B320EB83}.Release|x64.ActiveCfg = Release|Any CPU + {59435C05-1155-47A5-9361-2702B320EB83}.Release|x64.Build.0 = Release|Any CPU + {59435C05-1155-47A5-9361-2702B320EB83}.Release|x86.ActiveCfg = Release|Any CPU + {59435C05-1155-47A5-9361-2702B320EB83}.Release|x86.Build.0 = Release|Any CPU + {F31CE4C8-0B88-492B-9AE2-92D731D51B88}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {F31CE4C8-0B88-492B-9AE2-92D731D51B88}.Debug|Any CPU.Build.0 = Debug|Any CPU + {F31CE4C8-0B88-492B-9AE2-92D731D51B88}.Debug|x64.ActiveCfg = Debug|Any CPU + {F31CE4C8-0B88-492B-9AE2-92D731D51B88}.Debug|x64.Build.0 = Debug|Any CPU + {F31CE4C8-0B88-492B-9AE2-92D731D51B88}.Debug|x86.ActiveCfg = Debug|Any CPU + {F31CE4C8-0B88-492B-9AE2-92D731D51B88}.Debug|x86.Build.0 = Debug|Any CPU + {F31CE4C8-0B88-492B-9AE2-92D731D51B88}.Release|Any CPU.ActiveCfg = Release|Any CPU + {F31CE4C8-0B88-492B-9AE2-92D731D51B88}.Release|Any CPU.Build.0 = Release|Any CPU + {F31CE4C8-0B88-492B-9AE2-92D731D51B88}.Release|x64.ActiveCfg = Release|Any CPU + {F31CE4C8-0B88-492B-9AE2-92D731D51B88}.Release|x64.Build.0 = Release|Any CPU + {F31CE4C8-0B88-492B-9AE2-92D731D51B88}.Release|x86.ActiveCfg = Release|Any CPU + {F31CE4C8-0B88-492B-9AE2-92D731D51B88}.Release|x86.Build.0 = Release|Any CPU + EndGlobalSection +EndGlobal diff --git a/src/Graph.ArgumentValidator.csproj b/src/Graph.ArgumentValidator.csproj index 50dfe20..6242f04 100644 --- a/src/Graph.ArgumentValidator.csproj +++ b/src/Graph.ArgumentValidator.csproj @@ -1,4 +1,5 @@ - + + $(LibraryTargetFrameworks) diff --git a/src/Graph.ArgumentValidator.sln b/src/Graph.ArgumentValidator.sln deleted file mode 100644 index 23fc8bd..0000000 --- a/src/Graph.ArgumentValidator.sln +++ /dev/null @@ -1,25 +0,0 @@ - -Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio Version 16 -VisualStudioVersion = 16.0.31025.194 -MinimumVisualStudioVersion = 10.0.40219.1 -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Graph.ArgumentValidator", "Graph.ArgumentValidator.csproj", "{D40E08ED-B236-4AF0-AE97-AB4580BD9419}" -EndProject -Global - GlobalSection(SolutionConfigurationPlatforms) = preSolution - Debug|Any CPU = Debug|Any CPU - Release|Any CPU = Release|Any CPU - EndGlobalSection - GlobalSection(ProjectConfigurationPlatforms) = postSolution - {D40E08ED-B236-4AF0-AE97-AB4580BD9419}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {D40E08ED-B236-4AF0-AE97-AB4580BD9419}.Debug|Any CPU.Build.0 = Debug|Any CPU - {D40E08ED-B236-4AF0-AE97-AB4580BD9419}.Release|Any CPU.ActiveCfg = Release|Any CPU - {D40E08ED-B236-4AF0-AE97-AB4580BD9419}.Release|Any CPU.Build.0 = Release|Any CPU - EndGlobalSection - GlobalSection(SolutionProperties) = preSolution - HideSolutionNode = FALSE - EndGlobalSection - GlobalSection(ExtensibilityGlobals) = postSolution - SolutionGuid = {14B6055D-A6EA-4D84-AD7E-A397DE116F0B} - EndGlobalSection -EndGlobal diff --git a/src/Extensions.cs b/src/RequestExecutorBuilderExtensions.cs similarity index 70% rename from src/Extensions.cs rename to src/RequestExecutorBuilderExtensions.cs index 2887973..fc8c1a4 100644 --- a/src/Extensions.cs +++ b/src/RequestExecutorBuilderExtensions.cs @@ -3,12 +3,12 @@ namespace Graph.ArgumentValidator { - public static class Extensions + public static class RequestExecutorBuilderExtensions { public static IRequestExecutorBuilder AddArgumentValidator( this IRequestExecutorBuilder requestExecutorBuilder) { - requestExecutorBuilder.UseField(); + requestExecutorBuilder.TryAddTypeInterceptor(); return requestExecutorBuilder; } diff --git a/src/ValidatableAttribute.cs b/src/ValidatableAttribute.cs index bd8da8f..e8ab406 100644 --- a/src/ValidatableAttribute.cs +++ b/src/ValidatableAttribute.cs @@ -2,7 +2,7 @@ namespace Graph.ArgumentValidator { - [AttributeUsage(AttributeTargets.Class, Inherited = true, AllowMultiple = false)] + [AttributeUsage(AttributeTargets.Parameter, Inherited = true, AllowMultiple = false)] public class ValidatableAttribute : Attribute { public ValidatableAttribute() diff --git a/src/ValidationMiddleware.cs b/src/ValidationMiddleware.cs index c7e0ae4..aa0b06c 100644 --- a/src/ValidationMiddleware.cs +++ b/src/ValidationMiddleware.cs @@ -17,43 +17,27 @@ public ValidationMiddleware(FieldDelegate next) _next = next; } + // this middleware is ensured to only execute on fields that have arguments that need validation. public async Task InvokeAsync(IMiddlewareContext context) { - var arguments = context.Field.Arguments; - - if (arguments.Count is 0) - await _next(context); - var errors = new List(); - foreach (var argument in arguments) + // we could even further optimize and aggregate this list in the interceptor and inject it into the middleware + foreach (var argument in context.Field.Arguments.Where(t => t.ContextData.ContainsKey(WellKnownContextData.NeedValidation))) { - if (argument == null) - { - continue; - } - var input = context.ArgumentValue(argument.Name); - - if (input == null) - { - continue; - } - - if (!ValidatorSettings.ValidateAllInputs && - input.GetType().GetCustomAttribute(typeof(ValidatableAttribute)) == null) - continue; - var validationContext = new ValidationContext(input); Validator.TryValidateObject(input, validationContext, errors, true); } - if (errors.Any()) + if (errors.Count > 0) { foreach (var error in errors) { + // NOTE: it would be good to provide an error code. context.ReportError(ErrorBuilder.New() .SetMessage(error.ErrorMessage) + // .SetCode() .Build()); } } diff --git a/src/ValidationTypeInterceptor.cs b/src/ValidationTypeInterceptor.cs new file mode 100644 index 0000000..344b3c5 --- /dev/null +++ b/src/ValidationTypeInterceptor.cs @@ -0,0 +1,57 @@ +using System.Collections.Generic; +using HotChocolate.Configuration; +using HotChocolate.Resolvers; +using HotChocolate.Types.Descriptors.Definitions; + +namespace Graph.ArgumentValidator +{ + internal class ValidationTypeInterceptor : TypeInterceptor + { + private FieldMiddleware _middleware; + + public override void OnBeforeCompleteType( + ITypeCompletionContext completionContext, + DefinitionBase definition, + IDictionary contextData) + { + if (definition is ObjectTypeDefinition objectTypeDef) + { + foreach (var fieldDef in objectTypeDef.Fields) + { + // most fields do not need validation. + bool needValidation = false; + + foreach (var argumentDef in fieldDef.Arguments) + { + if (argumentDef.Parameter is not null && + argumentDef.Parameter.IsDefined(typeof(ValidatableAttribute), true)) + { + // we will set a marker for this argument to be validated. + argumentDef.ContextData[WellKnownContextData.NeedValidation] = true; + needValidation = true; + } + } + + if (needValidation) + { + // if validation is needed we will ensure that a validation middleware exists. + if (_middleware is null) + { + // if no middleware is yet created we will compile a middleware from our + // ValidationMiddleware class. + _middleware = FieldClassMiddlewareFactory.Create(); + } + + // we add the validation middleware to the first spot so that validation is executed first. + fieldDef.MiddlewareComponents.Insert(0, _middleware); + } + } + } + } + } + + internal static class WellKnownContextData + { + public const string NeedValidation = nameof(NeedValidation); + } +} diff --git a/test/Graph.ArgumentValidator.Tests.csproj b/test/Graph.ArgumentValidator.Tests.csproj new file mode 100644 index 0000000..1759264 --- /dev/null +++ b/test/Graph.ArgumentValidator.Tests.csproj @@ -0,0 +1,27 @@ + + + + net5.0 + + false + + + + + + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + diff --git a/test/UnitTest1.cs b/test/UnitTest1.cs new file mode 100644 index 0000000..c7166e6 --- /dev/null +++ b/test/UnitTest1.cs @@ -0,0 +1,31 @@ +using System; +using System.ComponentModel.DataAnnotations; +using System.Threading.Tasks; +using HotChocolate; +using HotChocolate.Execution; +using Microsoft.Extensions.DependencyInjection; +using Snapshooter.Xunit; +using Xunit; + +namespace Graph.ArgumentValidator +{ + public class ValidationTests + { + [Fact] + public async Task Ensure_Validation_Is_Executed() + { + var result = + await new ServiceCollection() + .AddGraphQL() + .AddQueryType() + .AddArgumentValidator() + .ExecuteRequestAsync("{ argIsEmail(email: \"abc\") }"); + + result.ToJson().MatchSnapshot(); + } + } + + public class Query { + public string ArgIsEmail([Validatable] [EmailAddress] string email) => email; + } +} diff --git a/test/__snapshots__/ValidationTests.Ensure_Validation_Is_Executed.snap b/test/__snapshots__/ValidationTests.Ensure_Validation_Is_Executed.snap new file mode 100644 index 0000000..9151026 --- /dev/null +++ b/test/__snapshots__/ValidationTests.Ensure_Validation_Is_Executed.snap @@ -0,0 +1,5 @@ +{ + "data": { + "argIsEmail": "abc" + } +}