From ffcfc60df14904377cecb3cf8cc76603907e3eb0 Mon Sep 17 00:00:00 2001 From: David Lebee Date: Wed, 3 Feb 2021 20:28:56 -0500 Subject: [PATCH] graphql fluent validation implementation with middleware. --- Demo/Demo.csproj | 1 + Demo/Startup.cs | 8 +++- .../IGraphQLFieldError.cs | 10 ++++ .../IGraphQLValidationResult.cs | 11 +++++ .../IGraphQLValidationService.cs | 13 +++++ ...weredSoft.CQRS.GraphQL.Abstractions.csproj | 7 +++ .../GraphQLFieldError.cs | 11 +++++ .../GraphQLFluentValidationResult.cs | 28 +++++++++++ .../GraphQLFluentValidationService.cs | 47 +++++++++++++++++++ .../GraphQLValidResult.cs | 11 +++++ ...dSoft.CQRS.GraphQL.FluentValidation.csproj | 16 +++++++ .../ServiceCollectionExtensions.cs | 18 +++++++ .../MutationObjectType.cs | 21 ++++----- .../MutationParamRequiredMiddleware.cs | 37 +++++++++++++++ .../MutationValidationMiddleware.cs | 47 +++++++++++++++++++ ...weredSoft.CQRS.GraphQL.HotChocolate.csproj | 1 + .../QueryObjectType.cs | 5 +- .../QueryValidationMiddleware.cs | 47 +++++++++++++++++++ PoweredSoft.CQRS.sln | 12 +++++ 19 files changed, 332 insertions(+), 19 deletions(-) create mode 100644 PoweredSoft.CQRS.GraphQL.Abstractions/IGraphQLFieldError.cs create mode 100644 PoweredSoft.CQRS.GraphQL.Abstractions/IGraphQLValidationResult.cs create mode 100644 PoweredSoft.CQRS.GraphQL.Abstractions/IGraphQLValidationService.cs create mode 100644 PoweredSoft.CQRS.GraphQL.Abstractions/PoweredSoft.CQRS.GraphQL.Abstractions.csproj create mode 100644 PoweredSoft.CQRS.GraphQL.FluentValidation/GraphQLFieldError.cs create mode 100644 PoweredSoft.CQRS.GraphQL.FluentValidation/GraphQLFluentValidationResult.cs create mode 100644 PoweredSoft.CQRS.GraphQL.FluentValidation/GraphQLFluentValidationService.cs create mode 100644 PoweredSoft.CQRS.GraphQL.FluentValidation/GraphQLValidResult.cs create mode 100644 PoweredSoft.CQRS.GraphQL.FluentValidation/PoweredSoft.CQRS.GraphQL.FluentValidation.csproj create mode 100644 PoweredSoft.CQRS.GraphQL.FluentValidation/ServiceCollectionExtensions.cs create mode 100644 PoweredSoft.CQRS.GraphQL.HotChocolate/MutationParamRequiredMiddleware.cs create mode 100644 PoweredSoft.CQRS.GraphQL.HotChocolate/MutationValidationMiddleware.cs create mode 100644 PoweredSoft.CQRS.GraphQL.HotChocolate/QueryValidationMiddleware.cs diff --git a/Demo/Demo.csproj b/Demo/Demo.csproj index 7abba2b..9450b4e 100644 --- a/Demo/Demo.csproj +++ b/Demo/Demo.csproj @@ -18,6 +18,7 @@ + diff --git a/Demo/Startup.cs b/Demo/Startup.cs index ed21cbe..c26d25b 100644 --- a/Demo/Startup.cs +++ b/Demo/Startup.cs @@ -19,6 +19,7 @@ using PoweredSoft.CQRS.AspNetCore.Mvc; using PoweredSoft.CQRS.DynamicQuery; using PoweredSoft.CQRS.DynamicQuery.Abstractions; using PoweredSoft.CQRS.DynamicQuery.AspNetCore; +using PoweredSoft.CQRS.GraphQL.FluentValidation; using PoweredSoft.CQRS.GraphQL.HotChocolate; using PoweredSoft.Data; using PoweredSoft.Data.Core; @@ -50,7 +51,9 @@ namespace Demo services.AddPoweredSoftDataServices(); services.AddPoweredSoftDynamicQuery(); - services.AddPoweredSoftCQRS(); + services + .AddPoweredSoftCQRS(); + services .AddControllers() .AddPoweredSoftQueries() @@ -65,8 +68,9 @@ namespace Demo .AddMutationType(d => d.Name("Mutation")) .AddPoweredSoftMutations(); + services.AddPoweredSoftGraphQLFluentValidation(); - //services.AddSwaggerGen(); + services.AddSwaggerGen(); } private void AddDynamicQueries(IServiceCollection services) diff --git a/PoweredSoft.CQRS.GraphQL.Abstractions/IGraphQLFieldError.cs b/PoweredSoft.CQRS.GraphQL.Abstractions/IGraphQLFieldError.cs new file mode 100644 index 0000000..f4fb65a --- /dev/null +++ b/PoweredSoft.CQRS.GraphQL.Abstractions/IGraphQLFieldError.cs @@ -0,0 +1,10 @@ +using System.Collections.Generic; + +namespace PoweredSoft.CQRS.GraphQL.Abstractions +{ + public interface IGraphQLFieldError + { + string Field { get; set; } + List Errors { get; set; } + } +} diff --git a/PoweredSoft.CQRS.GraphQL.Abstractions/IGraphQLValidationResult.cs b/PoweredSoft.CQRS.GraphQL.Abstractions/IGraphQLValidationResult.cs new file mode 100644 index 0000000..1cc3d1d --- /dev/null +++ b/PoweredSoft.CQRS.GraphQL.Abstractions/IGraphQLValidationResult.cs @@ -0,0 +1,11 @@ +using System.Collections.Generic; + +namespace PoweredSoft.CQRS.GraphQL.Abstractions +{ + public interface IGraphQLValidationResult + { + bool IsValid { get; } + + List Errors { get; } + } +} diff --git a/PoweredSoft.CQRS.GraphQL.Abstractions/IGraphQLValidationService.cs b/PoweredSoft.CQRS.GraphQL.Abstractions/IGraphQLValidationService.cs new file mode 100644 index 0000000..1c5dd22 --- /dev/null +++ b/PoweredSoft.CQRS.GraphQL.Abstractions/IGraphQLValidationService.cs @@ -0,0 +1,13 @@ +using System; +using System.Threading; +using System.Threading.Tasks; + +namespace PoweredSoft.CQRS.GraphQL.Abstractions +{ + + public interface IGraphQLValidationService + { + Task ValidateObjectAsync(object subject, CancellationToken cancellationToken = default); + Task ValidateAsync(T subject, CancellationToken cancellationToken = default); + } +} diff --git a/PoweredSoft.CQRS.GraphQL.Abstractions/PoweredSoft.CQRS.GraphQL.Abstractions.csproj b/PoweredSoft.CQRS.GraphQL.Abstractions/PoweredSoft.CQRS.GraphQL.Abstractions.csproj new file mode 100644 index 0000000..9f5c4f4 --- /dev/null +++ b/PoweredSoft.CQRS.GraphQL.Abstractions/PoweredSoft.CQRS.GraphQL.Abstractions.csproj @@ -0,0 +1,7 @@ + + + + netstandard2.0 + + + diff --git a/PoweredSoft.CQRS.GraphQL.FluentValidation/GraphQLFieldError.cs b/PoweredSoft.CQRS.GraphQL.FluentValidation/GraphQLFieldError.cs new file mode 100644 index 0000000..3125d25 --- /dev/null +++ b/PoweredSoft.CQRS.GraphQL.FluentValidation/GraphQLFieldError.cs @@ -0,0 +1,11 @@ +using PoweredSoft.CQRS.GraphQL.Abstractions; +using System.Collections.Generic; + +namespace PoweredSoft.CQRS.GraphQL.FluentValidation +{ + public class GraphQLFieldError : IGraphQLFieldError + { + public string Field { get; set; } + public List Errors { get; set; } = new List(); + } +} diff --git a/PoweredSoft.CQRS.GraphQL.FluentValidation/GraphQLFluentValidationResult.cs b/PoweredSoft.CQRS.GraphQL.FluentValidation/GraphQLFluentValidationResult.cs new file mode 100644 index 0000000..c5f1ddb --- /dev/null +++ b/PoweredSoft.CQRS.GraphQL.FluentValidation/GraphQLFluentValidationResult.cs @@ -0,0 +1,28 @@ +using FluentValidation.Results; +using PoweredSoft.CQRS.GraphQL.Abstractions; +using System.Collections.Generic; + +namespace PoweredSoft.CQRS.GraphQL.FluentValidation +{ + public class GraphQLFluentValidationResult : IGraphQLValidationResult + { + public bool IsValid => Errors.Count == 0; + public List Errors { get; } = new List(); + + public static GraphQLFluentValidationResult From(ValidationResult result) + { + var model = new GraphQLFluentValidationResult(); + foreach (var error in result.Errors) + { + var fieldError = new GraphQLFieldError + { + Field = error.PropertyName + }; + fieldError.Errors.Add(error.ErrorMessage); + model.Errors.Add(fieldError); + } + + return model; + } + } +} diff --git a/PoweredSoft.CQRS.GraphQL.FluentValidation/GraphQLFluentValidationService.cs b/PoweredSoft.CQRS.GraphQL.FluentValidation/GraphQLFluentValidationService.cs new file mode 100644 index 0000000..516b912 --- /dev/null +++ b/PoweredSoft.CQRS.GraphQL.FluentValidation/GraphQLFluentValidationService.cs @@ -0,0 +1,47 @@ +using FluentValidation; +using PoweredSoft.CQRS.GraphQL.Abstractions; +using System; +using System.Linq; +using System.Text; +using System.Threading; +using System.Threading.Tasks; + +namespace PoweredSoft.CQRS.GraphQL.FluentValidation +{ + public class GraphQLFluentValidationService : IGraphQLValidationService + { + private readonly IServiceProvider serviceProvider; + + public GraphQLFluentValidationService(IServiceProvider serviceProvider) + { + this.serviceProvider = serviceProvider; + } + + public async Task ValidateAsync(T subject, CancellationToken cancellationToken = default) + { + var validationService = serviceProvider.GetService(typeof(IValidator)) as IValidator; + if (validationService == null) + return new GraphQLValidResult(); + + var result = await validationService.ValidateAsync(subject, cancellationToken); + if (!result.IsValid) + return GraphQLFluentValidationResult.From(result); + + return new GraphQLValidResult(); + } + + public async Task ValidateObjectAsync(object subject, CancellationToken cancellationToken = default) + { + var validatorType = typeof(IValidator<>).MakeGenericType(subject.GetType()); + var validationService = serviceProvider.GetService(validatorType) as IValidator; + if (validationService == null) + return new GraphQLValidResult(); + + var result = await validationService.ValidateAsync(new ValidationContext(subject), cancellationToken); + if (!result.IsValid) + return GraphQLFluentValidationResult.From(result); + + return new GraphQLValidResult(); + } + } +} diff --git a/PoweredSoft.CQRS.GraphQL.FluentValidation/GraphQLValidResult.cs b/PoweredSoft.CQRS.GraphQL.FluentValidation/GraphQLValidResult.cs new file mode 100644 index 0000000..5f49a4e --- /dev/null +++ b/PoweredSoft.CQRS.GraphQL.FluentValidation/GraphQLValidResult.cs @@ -0,0 +1,11 @@ +using PoweredSoft.CQRS.GraphQL.Abstractions; +using System.Collections.Generic; + +namespace PoweredSoft.CQRS.GraphQL.FluentValidation +{ + public class GraphQLValidResult : IGraphQLValidationResult + { + public bool IsValid => true; + public List Errors { get; } = new List(); + } +} diff --git a/PoweredSoft.CQRS.GraphQL.FluentValidation/PoweredSoft.CQRS.GraphQL.FluentValidation.csproj b/PoweredSoft.CQRS.GraphQL.FluentValidation/PoweredSoft.CQRS.GraphQL.FluentValidation.csproj new file mode 100644 index 0000000..5340204 --- /dev/null +++ b/PoweredSoft.CQRS.GraphQL.FluentValidation/PoweredSoft.CQRS.GraphQL.FluentValidation.csproj @@ -0,0 +1,16 @@ + + + + netstandard2.0 + + + + + + + + + + + + diff --git a/PoweredSoft.CQRS.GraphQL.FluentValidation/ServiceCollectionExtensions.cs b/PoweredSoft.CQRS.GraphQL.FluentValidation/ServiceCollectionExtensions.cs new file mode 100644 index 0000000..a0dcb57 --- /dev/null +++ b/PoweredSoft.CQRS.GraphQL.FluentValidation/ServiceCollectionExtensions.cs @@ -0,0 +1,18 @@ +using System; +using System.Collections.Generic; +using System.Text; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; +using PoweredSoft.CQRS.GraphQL.Abstractions; + +namespace PoweredSoft.CQRS.GraphQL.FluentValidation +{ + public static class ServiceCollectionExtensions + { + public static IServiceCollection AddPoweredSoftGraphQLFluentValidation(this IServiceCollection services) + { + services.AddTransient(); + return services; + } + } +} diff --git a/PoweredSoft.CQRS.GraphQL.HotChocolate/MutationObjectType.cs b/PoweredSoft.CQRS.GraphQL.HotChocolate/MutationObjectType.cs index 9452676..2cc00d8 100644 --- a/PoweredSoft.CQRS.GraphQL.HotChocolate/MutationObjectType.cs +++ b/PoweredSoft.CQRS.GraphQL.HotChocolate/MutationObjectType.cs @@ -21,7 +21,7 @@ namespace PoweredSoft.CQRS.GraphQL.HotChocolate desc.Name("Mutation"); foreach (var m in commandDiscovery.GetCommands()) { - var queryField = desc.Field(m.LowerCamelCaseName); + var mutationField = desc.Field(m.LowerCamelCaseName); Type typeToGet; if (m.CommandResultType == null) @@ -30,15 +30,15 @@ namespace PoweredSoft.CQRS.GraphQL.HotChocolate typeToGet = typeof(ICommandHandler<,>).MakeGenericType(m.CommandType, m.CommandResultType); if (m.CommandResultType == null) - queryField.Type(typeof(int?)); + mutationField.Type(typeof(int?)); else - queryField.Type(m.CommandResultType); + mutationField.Type(m.CommandResultType); //queryField.Use((sp, d) => new MutationAuthorizationMiddleware(m.CommandType, d)); if (m.CommandType.GetProperties().Length == 0) { - queryField.Resolve(async ctx => + mutationField.Resolve(async ctx => { var queryArgument = Activator.CreateInstance(m.CommandType); return await HandleMutation(m.CommandResultType != null, ctx, typeToGet, queryArgument); @@ -47,21 +47,16 @@ namespace PoweredSoft.CQRS.GraphQL.HotChocolate continue; } - queryField.Argument("params", t => t.Type(m.CommandType)); + mutationField.Argument("params", t => t.Type(m.CommandType)); - queryField.Resolve(async ctx => + mutationField.Resolve(async ctx => { var queryArgument = ctx.ArgumentValue("params"); return await HandleMutation(m.CommandResultType != null, ctx, typeToGet, queryArgument); }); - // TODO. - //if (m.MutationObjectRequired) - // queryField.Use(); - - // TODO. - //if (m.ValidateMutationObject) - // queryField.Use(); + mutationField.Use(); + mutationField.Use(); } } diff --git a/PoweredSoft.CQRS.GraphQL.HotChocolate/MutationParamRequiredMiddleware.cs b/PoweredSoft.CQRS.GraphQL.HotChocolate/MutationParamRequiredMiddleware.cs new file mode 100644 index 0000000..ba5a697 --- /dev/null +++ b/PoweredSoft.CQRS.GraphQL.HotChocolate/MutationParamRequiredMiddleware.cs @@ -0,0 +1,37 @@ +using HotChocolate; +using HotChocolate.Resolvers; +using System; +using System.Collections.Generic; +using System.Text; +using System.Threading.Tasks; + +namespace PoweredSoft.CQRS.GraphQL.HotChocolate +{ + public class MutationParamRequiredMiddleware + { + private readonly FieldDelegate _next; + + public MutationParamRequiredMiddleware(FieldDelegate next) + { + _next = next; + } + + public async Task InvokeAsync(IMiddlewareContext context) + { + var queryArgument = context.ArgumentValue("params"); + if (queryArgument == null) + { + context.Result = ErrorBuilder.New() + .SetMessage("mutation argument is required") + .SetCode("400") + .SetPath(context.Path) + .AddLocation(context.Selection.SyntaxNode) + .Build(); + + return; + } + + await _next.Invoke(context); + } + } +} diff --git a/PoweredSoft.CQRS.GraphQL.HotChocolate/MutationValidationMiddleware.cs b/PoweredSoft.CQRS.GraphQL.HotChocolate/MutationValidationMiddleware.cs new file mode 100644 index 0000000..1c30efb --- /dev/null +++ b/PoweredSoft.CQRS.GraphQL.HotChocolate/MutationValidationMiddleware.cs @@ -0,0 +1,47 @@ +using HotChocolate; +using HotChocolate.Resolvers; +using PoweredSoft.CQRS.GraphQL.Abstractions; +using System; +using System.Collections.Generic; +using System.Text; +using System.Threading.Tasks; + +namespace PoweredSoft.CQRS.GraphQL.HotChocolate +{ + public class MutationValidationMiddleware + { + private readonly FieldDelegate _next; + + public MutationValidationMiddleware(FieldDelegate next) + { + _next = next; + } + + public async Task InvokeAsync(IMiddlewareContext context) + { + var queryArgument = context.ArgumentValue("params"); + if (queryArgument != null) + { + var service = context.Service(); + var result = await service.ValidateObjectAsync(queryArgument, context.RequestAborted); + if (!result.IsValid) + { + var eb = ErrorBuilder.New() + .SetMessage("There are some validations errors") + .SetCode("ValidationError") + .SetPath(context.Path) + .AddLocation(context.Selection.SyntaxNode); + + foreach (var error in result.Errors) + eb.SetExtension(error.Field, error.Errors); + + context.Result = eb.Build(); + + return; + } + } + + await _next.Invoke(context); + } + } +} diff --git a/PoweredSoft.CQRS.GraphQL.HotChocolate/PoweredSoft.CQRS.GraphQL.HotChocolate.csproj b/PoweredSoft.CQRS.GraphQL.HotChocolate/PoweredSoft.CQRS.GraphQL.HotChocolate.csproj index 245df3f..9f66a32 100644 --- a/PoweredSoft.CQRS.GraphQL.HotChocolate/PoweredSoft.CQRS.GraphQL.HotChocolate.csproj +++ b/PoweredSoft.CQRS.GraphQL.HotChocolate/PoweredSoft.CQRS.GraphQL.HotChocolate.csproj @@ -14,6 +14,7 @@ + diff --git a/PoweredSoft.CQRS.GraphQL.HotChocolate/QueryObjectType.cs b/PoweredSoft.CQRS.GraphQL.HotChocolate/QueryObjectType.cs index 24c3f5e..386e0c1 100644 --- a/PoweredSoft.CQRS.GraphQL.HotChocolate/QueryObjectType.cs +++ b/PoweredSoft.CQRS.GraphQL.HotChocolate/QueryObjectType.cs @@ -57,10 +57,7 @@ namespace PoweredSoft.CQRS.GraphQL.HotChocolate if (q.QueryObjectRequired) queryField.Use();*/ - /* TODO - if (q.ValidateQueryObject) - queryField.Use(); - */ + queryField.Use(); } } diff --git a/PoweredSoft.CQRS.GraphQL.HotChocolate/QueryValidationMiddleware.cs b/PoweredSoft.CQRS.GraphQL.HotChocolate/QueryValidationMiddleware.cs new file mode 100644 index 0000000..3415e88 --- /dev/null +++ b/PoweredSoft.CQRS.GraphQL.HotChocolate/QueryValidationMiddleware.cs @@ -0,0 +1,47 @@ +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using HotChocolate; +using HotChocolate.Resolvers; +using Newtonsoft.Json; +using PoweredSoft.CQRS.GraphQL.Abstractions; + +namespace PoweredSoft.CQRS.GraphQL.HotChocolate +{ + public class QueryValidationMiddleware + { + private readonly FieldDelegate _next; + + public QueryValidationMiddleware(FieldDelegate next) + { + _next = next; + } + + public async Task InvokeAsync(IMiddlewareContext context) + { + var queryArgument = context.ArgumentValue("params"); + if (queryArgument != null) + { + var service = context.Service(); + var result = await service.ValidateObjectAsync(queryArgument, context.RequestAborted); + if (!result.IsValid) + { + var eb = ErrorBuilder.New() + .SetMessage("There are some validations errors") + .SetCode("ValidationError") + .SetPath(context.Path) + .AddLocation(context.Selection.SyntaxNode); + + foreach (var error in result.Errors) + eb.SetExtension(error.Field, error.Errors); + + context.Result = eb.Build(); + + return; + } + } + + await _next.Invoke(context); + } + } +} \ No newline at end of file diff --git a/PoweredSoft.CQRS.sln b/PoweredSoft.CQRS.sln index f29c46d..4d09ba5 100644 --- a/PoweredSoft.CQRS.sln +++ b/PoweredSoft.CQRS.sln @@ -27,6 +27,10 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PoweredSoft.CQRS.DynamicQue EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PoweredSoft.CQRS.GraphQL.HotChocolate", "PoweredSoft.CQRS.GraphQL.HotChocolate\PoweredSoft.CQRS.GraphQL.HotChocolate.csproj", "{BF8E3B0D-8651-4541-892F-F607C5E80F9B}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PoweredSoft.CQRS.GraphQL.Abstractions", "PoweredSoft.CQRS.GraphQL.Abstractions\PoweredSoft.CQRS.GraphQL.Abstractions.csproj", "{C18DD3EB-56A8-4576-BB31-04AE724E6E25}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PoweredSoft.CQRS.GraphQL.FluentValidation", "PoweredSoft.CQRS.GraphQL.FluentValidation\PoweredSoft.CQRS.GraphQL.FluentValidation.csproj", "{BB134663-BAB0-45C4-A6E0-34F296FCA7AE}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -69,6 +73,14 @@ Global {BF8E3B0D-8651-4541-892F-F607C5E80F9B}.Debug|Any CPU.Build.0 = Debug|Any CPU {BF8E3B0D-8651-4541-892F-F607C5E80F9B}.Release|Any CPU.ActiveCfg = Release|Any CPU {BF8E3B0D-8651-4541-892F-F607C5E80F9B}.Release|Any CPU.Build.0 = Release|Any CPU + {C18DD3EB-56A8-4576-BB31-04AE724E6E25}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {C18DD3EB-56A8-4576-BB31-04AE724E6E25}.Debug|Any CPU.Build.0 = Debug|Any CPU + {C18DD3EB-56A8-4576-BB31-04AE724E6E25}.Release|Any CPU.ActiveCfg = Release|Any CPU + {C18DD3EB-56A8-4576-BB31-04AE724E6E25}.Release|Any CPU.Build.0 = Release|Any CPU + {BB134663-BAB0-45C4-A6E0-34F296FCA7AE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {BB134663-BAB0-45C4-A6E0-34F296FCA7AE}.Debug|Any CPU.Build.0 = Debug|Any CPU + {BB134663-BAB0-45C4-A6E0-34F296FCA7AE}.Release|Any CPU.ActiveCfg = Release|Any CPU + {BB134663-BAB0-45C4-A6E0-34F296FCA7AE}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE