graphql fluent validation implementation with middleware.

This commit is contained in:
David Lebee 2021-02-03 20:28:56 -05:00
parent afb8b534bb
commit ffcfc60df1
19 changed files with 332 additions and 19 deletions

View File

@ -18,6 +18,7 @@
<ProjectReference Include="..\PoweredSoft.CQRS.DynamicQuery.Abstractions\PoweredSoft.CQRS.DynamicQuery.Abstractions.csproj" /> <ProjectReference Include="..\PoweredSoft.CQRS.DynamicQuery.Abstractions\PoweredSoft.CQRS.DynamicQuery.Abstractions.csproj" />
<ProjectReference Include="..\PoweredSoft.CQRS.DynamicQuery.AspNetCore\PoweredSoft.CQRS.DynamicQuery.AspNetCore.csproj" /> <ProjectReference Include="..\PoweredSoft.CQRS.DynamicQuery.AspNetCore\PoweredSoft.CQRS.DynamicQuery.AspNetCore.csproj" />
<ProjectReference Include="..\PoweredSoft.CQRS.DynamicQuery\PoweredSoft.CQRS.DynamicQuery.csproj" /> <ProjectReference Include="..\PoweredSoft.CQRS.DynamicQuery\PoweredSoft.CQRS.DynamicQuery.csproj" />
<ProjectReference Include="..\PoweredSoft.CQRS.GraphQL.FluentValidation\PoweredSoft.CQRS.GraphQL.FluentValidation.csproj" />
<ProjectReference Include="..\PoweredSoft.CQRS.GraphQL.HotChocolate\PoweredSoft.CQRS.GraphQL.HotChocolate.csproj" /> <ProjectReference Include="..\PoweredSoft.CQRS.GraphQL.HotChocolate\PoweredSoft.CQRS.GraphQL.HotChocolate.csproj" />
<ProjectReference Include="..\PoweredSoft.CQRS\PoweredSoft.CQRS.csproj" /> <ProjectReference Include="..\PoweredSoft.CQRS\PoweredSoft.CQRS.csproj" />
</ItemGroup> </ItemGroup>

View File

@ -19,6 +19,7 @@ using PoweredSoft.CQRS.AspNetCore.Mvc;
using PoweredSoft.CQRS.DynamicQuery; using PoweredSoft.CQRS.DynamicQuery;
using PoweredSoft.CQRS.DynamicQuery.Abstractions; using PoweredSoft.CQRS.DynamicQuery.Abstractions;
using PoweredSoft.CQRS.DynamicQuery.AspNetCore; using PoweredSoft.CQRS.DynamicQuery.AspNetCore;
using PoweredSoft.CQRS.GraphQL.FluentValidation;
using PoweredSoft.CQRS.GraphQL.HotChocolate; using PoweredSoft.CQRS.GraphQL.HotChocolate;
using PoweredSoft.Data; using PoweredSoft.Data;
using PoweredSoft.Data.Core; using PoweredSoft.Data.Core;
@ -50,7 +51,9 @@ namespace Demo
services.AddPoweredSoftDataServices(); services.AddPoweredSoftDataServices();
services.AddPoweredSoftDynamicQuery(); services.AddPoweredSoftDynamicQuery();
services.AddPoweredSoftCQRS(); services
.AddPoweredSoftCQRS();
services services
.AddControllers() .AddControllers()
.AddPoweredSoftQueries() .AddPoweredSoftQueries()
@ -65,8 +68,9 @@ namespace Demo
.AddMutationType(d => d.Name("Mutation")) .AddMutationType(d => d.Name("Mutation"))
.AddPoweredSoftMutations(); .AddPoweredSoftMutations();
services.AddPoweredSoftGraphQLFluentValidation();
//services.AddSwaggerGen(); services.AddSwaggerGen();
} }
private void AddDynamicQueries(IServiceCollection services) private void AddDynamicQueries(IServiceCollection services)

View File

@ -0,0 +1,10 @@
using System.Collections.Generic;
namespace PoweredSoft.CQRS.GraphQL.Abstractions
{
public interface IGraphQLFieldError
{
string Field { get; set; }
List<string> Errors { get; set; }
}
}

View File

@ -0,0 +1,11 @@
using System.Collections.Generic;
namespace PoweredSoft.CQRS.GraphQL.Abstractions
{
public interface IGraphQLValidationResult
{
bool IsValid { get; }
List<IGraphQLFieldError> Errors { get; }
}
}

View File

@ -0,0 +1,13 @@
using System;
using System.Threading;
using System.Threading.Tasks;
namespace PoweredSoft.CQRS.GraphQL.Abstractions
{
public interface IGraphQLValidationService
{
Task<IGraphQLValidationResult> ValidateObjectAsync(object subject, CancellationToken cancellationToken = default);
Task<IGraphQLValidationResult> ValidateAsync<T>(T subject, CancellationToken cancellationToken = default);
}
}

View File

@ -0,0 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
</PropertyGroup>
</Project>

View File

@ -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<string> Errors { get; set; } = new List<string>();
}
}

View File

@ -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<IGraphQLFieldError> Errors { get; } = new List<IGraphQLFieldError>();
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;
}
}
}

View File

@ -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<IGraphQLValidationResult> ValidateAsync<T>(T subject, CancellationToken cancellationToken = default)
{
var validationService = serviceProvider.GetService(typeof(IValidator<T>)) as IValidator<T>;
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<IGraphQLValidationResult> 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<object>(subject), cancellationToken);
if (!result.IsValid)
return GraphQLFluentValidationResult.From(result);
return new GraphQLValidResult();
}
}
}

View File

@ -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<IGraphQLFieldError> Errors { get; } = new List<IGraphQLFieldError>();
}
}

View File

@ -0,0 +1,16 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="FluentValidation" Version="9.5.0" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="3.1.0" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\PoweredSoft.CQRS.GraphQL.Abstractions\PoweredSoft.CQRS.GraphQL.Abstractions.csproj" />
</ItemGroup>
</Project>

View File

@ -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<IGraphQLValidationService, GraphQLFluentValidationService>();
return services;
}
}
}

View File

@ -21,7 +21,7 @@ namespace PoweredSoft.CQRS.GraphQL.HotChocolate
desc.Name("Mutation"); desc.Name("Mutation");
foreach (var m in commandDiscovery.GetCommands()) foreach (var m in commandDiscovery.GetCommands())
{ {
var queryField = desc.Field(m.LowerCamelCaseName); var mutationField = desc.Field(m.LowerCamelCaseName);
Type typeToGet; Type typeToGet;
if (m.CommandResultType == null) if (m.CommandResultType == null)
@ -30,15 +30,15 @@ namespace PoweredSoft.CQRS.GraphQL.HotChocolate
typeToGet = typeof(ICommandHandler<,>).MakeGenericType(m.CommandType, m.CommandResultType); typeToGet = typeof(ICommandHandler<,>).MakeGenericType(m.CommandType, m.CommandResultType);
if (m.CommandResultType == null) if (m.CommandResultType == null)
queryField.Type(typeof(int?)); mutationField.Type(typeof(int?));
else else
queryField.Type(m.CommandResultType); mutationField.Type(m.CommandResultType);
//queryField.Use((sp, d) => new MutationAuthorizationMiddleware(m.CommandType, d)); //queryField.Use((sp, d) => new MutationAuthorizationMiddleware(m.CommandType, d));
if (m.CommandType.GetProperties().Length == 0) if (m.CommandType.GetProperties().Length == 0)
{ {
queryField.Resolve(async ctx => mutationField.Resolve(async ctx =>
{ {
var queryArgument = Activator.CreateInstance(m.CommandType); var queryArgument = Activator.CreateInstance(m.CommandType);
return await HandleMutation(m.CommandResultType != null, ctx, typeToGet, queryArgument); return await HandleMutation(m.CommandResultType != null, ctx, typeToGet, queryArgument);
@ -47,21 +47,16 @@ namespace PoweredSoft.CQRS.GraphQL.HotChocolate
continue; 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<object>("params"); var queryArgument = ctx.ArgumentValue<object>("params");
return await HandleMutation(m.CommandResultType != null, ctx, typeToGet, queryArgument); return await HandleMutation(m.CommandResultType != null, ctx, typeToGet, queryArgument);
}); });
// TODO. mutationField.Use<MutationParamRequiredMiddleware>();
//if (m.MutationObjectRequired) mutationField.Use<MutationValidationMiddleware>();
// queryField.Use<MutationParamRequiredMiddleware>();
// TODO.
//if (m.ValidateMutationObject)
// queryField.Use<MutationValidationMiddleware>();
} }
} }

View File

@ -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<object>("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);
}
}
}

View File

@ -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<object>("params");
if (queryArgument != null)
{
var service = context.Service<IGraphQLValidationService>();
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);
}
}
}

View File

@ -14,6 +14,7 @@
<ItemGroup> <ItemGroup>
<ProjectReference Include="..\PoweredSoft.CQRS.Abstractions\PoweredSoft.CQRS.Abstractions.csproj" /> <ProjectReference Include="..\PoweredSoft.CQRS.Abstractions\PoweredSoft.CQRS.Abstractions.csproj" />
<ProjectReference Include="..\PoweredSoft.CQRS.GraphQL.Abstractions\PoweredSoft.CQRS.GraphQL.Abstractions.csproj" />
</ItemGroup> </ItemGroup>
</Project> </Project>

View File

@ -57,10 +57,7 @@ namespace PoweredSoft.CQRS.GraphQL.HotChocolate
if (q.QueryObjectRequired) if (q.QueryObjectRequired)
queryField.Use<QueryParamRequiredMiddleware>();*/ queryField.Use<QueryParamRequiredMiddleware>();*/
/* TODO queryField.Use<QueryValidationMiddleware>();
if (q.ValidateQueryObject)
queryField.Use<QueryValidationMiddleware>();
*/
} }
} }

View File

@ -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<object>("params");
if (queryArgument != null)
{
var service = context.Service<IGraphQLValidationService>();
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);
}
}
}

View File

@ -27,6 +27,10 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PoweredSoft.CQRS.DynamicQue
EndProject 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}" 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 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 Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU 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}.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.ActiveCfg = Release|Any CPU
{BF8E3B0D-8651-4541-892F-F607C5E80F9B}.Release|Any CPU.Build.0 = 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 EndGlobalSection
GlobalSection(SolutionProperties) = preSolution GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE HideSolutionNode = FALSE