Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added support for MutationConventions #80

Draft
wants to merge 1 commit into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 15 additions & 6 deletions src/FairyBread/DefaultValidationErrorsHandler.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
using System.Collections.Generic;
using System;
using System.Collections.Generic;
using System.Linq;
using FluentValidation;
using FluentValidation.Results;
using HotChocolate;
Expand All @@ -12,13 +14,20 @@ public virtual void Handle(
IMiddlewareContext context,
IEnumerable<ArgumentValidationResult> invalidResults)
{
foreach (var invalidResult in invalidResults)
if (context.ContextData.ContainsKey(WellKnownContextData.ValidatorDescriptorsParams))
{
foreach (var failure in invalidResult.Result.Errors)
throw new AggregateException(invalidResults.Select(x => new ValidationException(x.Result.Errors)));
}
else
{
foreach (var invalidResult in invalidResults)
{
var errorBuilder = CreateErrorBuilder(context, invalidResult.ArgumentName, invalidResult.Validator, failure);
var error = errorBuilder.Build();
context.ReportError(error);
foreach (var failure in invalidResult.Result.Errors)
{
var errorBuilder = CreateErrorBuilder(context, invalidResult.ArgumentName, invalidResult.Validator, failure);
var error = errorBuilder.Build();
context.ReportError(error);
}
}
}
}
Expand Down
107 changes: 100 additions & 7 deletions src/FairyBread/ValidationMiddlewareInjector.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using HotChocolate;
using HotChocolate.Configuration;
using HotChocolate.Internal;
Expand All @@ -15,6 +16,7 @@ namespace FairyBread
internal class ValidationMiddlewareInjector : TypeInterceptor
{
private FieldMiddlewareDefinition? _validationFieldMiddlewareDef;
private FieldMiddlewareDefinition? _validationFieldMiddlewareDefParams;

public override void OnBeforeCompleteType(
ITypeCompletionContext completionContext,
Expand All @@ -36,6 +38,7 @@ public override void OnBeforeCompleteType(
// Don't add validation middleware unless:
// 1. we have args
var needsValidationMiddleware = false;
var needsValidationMiddlewareParams = false;

foreach (var argDef in fieldDef.Arguments)
{
Expand All @@ -48,13 +51,28 @@ public override void OnBeforeCompleteType(
}

// 3. there's validators for it
List<ValidatorDescriptor> validatorDescs;
Dictionary<string, List<ValidatorDescriptor>> validatorDescs;
var usingArgs = true;
try
{
validatorDescs = DetermineValidatorsForArg(validatorRegistry, argDef);
if (validatorDescs.Count < 1)
var validatorDescsArgs = DetermineValidatorsForArg(validatorRegistry, argDef);
if (validatorDescsArgs.Any())
validatorDescs = new Dictionary<string, List<ValidatorDescriptor>>() { { "Args", validatorDescsArgs } };
else
{
continue;
if (fieldDef.Arguments.Count == 1) // MutationConventions always use one argument
{
var type = fieldDef.ResolverMember as MethodInfo;
var parameters = type?.GetParameters();
if (parameters is null)
continue;
validatorDescs = DetermineValidatorsForParameters(validatorRegistry, parameters);
if (!validatorDescs.Any())
continue;
usingArgs = false;
}
else
continue;
}
}
catch (Exception ex)
Expand All @@ -74,9 +92,16 @@ public override void OnBeforeCompleteType(
}
}

validatorDescs.TrimExcess();
needsValidationMiddleware = true;
argDef.ContextData[WellKnownContextData.ValidatorDescriptors] = validatorDescs.AsReadOnly();
if (usingArgs)
{
needsValidationMiddleware = true;
argDef.ContextData[WellKnownContextData.ValidatorDescriptors] = validatorDescs.First().Value.AsReadOnly();
}
else
{
needsValidationMiddlewareParams = true;
argDef.ContextData[WellKnownContextData.ValidatorDescriptorsParams] = validatorDescs;
}
}

if (needsValidationMiddleware)
Expand All @@ -89,9 +114,77 @@ public override void OnBeforeCompleteType(

fieldDef.MiddlewareDefinitions.Insert(0, _validationFieldMiddlewareDef);
}
else if (needsValidationMiddlewareParams)
{
if (_validationFieldMiddlewareDefParams is null)
{
_validationFieldMiddlewareDefParams = new FieldMiddlewareDefinition(
FieldClassMiddlewareFactory.Create<ValidationMiddlewareParams>());
}

fieldDef.MiddlewareDefinitions.Add(_validationFieldMiddlewareDefParams);
}
}
}

private static Dictionary<string, List<ValidatorDescriptor>> DetermineValidatorsForParameters(IValidatorRegistry validatorRegistry, ParameterInfo[] parameters)
{
var validators = new Dictionary<string, List<ValidatorDescriptor>>();


foreach (var parameter in parameters)
{
var paramVals = new List<ValidatorDescriptor>();
// If validation is explicitly disabled, return none so validation middleware won't be added
if (parameter.CustomAttributes.Any(x => x.AttributeType == typeof(DontValidateAttribute)))
{
continue;
}


// Include implicit validator/s first (if allowed)
if (!parameter.CustomAttributes.Any(x => x.AttributeType == typeof(DontValidateImplicitlyAttribute)))
{
// And if we can figure out the arg's runtime type
var argRuntimeType = parameter.ParameterType;
if (argRuntimeType is not null)
{
if (validatorRegistry.Cache.TryGetValue(argRuntimeType, out var implicitValidators) &&
implicitValidators is not null)
{
paramVals.AddRange(implicitValidators);
}
}
}

// Include explicit validator/s (that aren't already added implicitly)
var explicitValidators = parameter.GetCustomAttributes().Where(x => x.GetType() == typeof(ValidateAttribute)).Cast<ValidateAttribute>().ToList();
if (explicitValidators.Any())
{
var validatorTypes = explicitValidators.SelectMany(x => x.ValidatorTypes);
// TODO: Potentially check and throw if there's a validator being explicitly applied for the wrong runtime type

foreach (var validatorType in validatorTypes)
{
if (paramVals.Any(v => v.ValidatorType == validatorType))
{
continue;
}

paramVals.Add(new ValidatorDescriptor(validatorType));
}
}

if (paramVals.Any())
{
paramVals.TrimExcess();
validators[parameter.Name] = paramVals;
}
}
return validators;
}


private static List<ValidatorDescriptor> DetermineValidatorsForArg(
IValidatorRegistry validatorRegistry,
ArgumentDefinition argDef)
Expand Down
125 changes: 125 additions & 0 deletions src/FairyBread/ValidationMiddlewareParams.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Text;
using System.Threading.Tasks;
using FluentValidation;
using HotChocolate.Resolvers;
using HotChocolate.Types;
using Microsoft.Extensions.DependencyInjection;

namespace FairyBread
{
internal class ValidationMiddlewareParams
{
private readonly FieldDelegate _next;
private readonly IValidatorProvider _validatorProvider;
private readonly IValidationErrorsHandler _validationErrorsHandler;

public ValidationMiddlewareParams(
FieldDelegate next,
IValidatorProvider validatorProvider,
IValidationErrorsHandler validationErrorsHandler)
{
_next = next;
_validatorProvider = validatorProvider;
_validationErrorsHandler = validationErrorsHandler;
}

public async Task InvokeAsync(IMiddlewareContext context)
{
context.ContextData[WellKnownContextData.ValidatorDescriptorsParams] = true;
var arguments = context.Selection.Field.Arguments;

var invalidResults = new List<ArgumentValidationResult>();
var type = context.Selection.Field.ResolverMember as MethodInfo;
var parameters = type.GetParameters();



foreach (var argument in context.Selection.Field.Arguments)
{
if (argument == null)
{
continue;
}

var resolvedValidators = GetValidators(context, argument).GroupBy(x => x.param);
if (resolvedValidators.Count() > 0)
{
foreach (var resolvedValidatorGroup in resolvedValidators)
{
try
{
var value = context.ArgumentValue<object?>(resolvedValidatorGroup.Key);
if (value == null)
{
continue;
}

foreach (var resolvedValidator in resolvedValidatorGroup)
{
var validationContext = new ValidationContext<object?>(value);
var validationResult = await resolvedValidator.resolver.Validator.ValidateAsync(
validationContext,
context.RequestAborted);
if (validationResult != null &&
!validationResult.IsValid)
{
invalidResults.Add(
new ArgumentValidationResult(
resolvedValidatorGroup.Key,
resolvedValidator.resolver.Validator,
validationResult));
}
}
}
finally
{
foreach (var resolvedValidator in resolvedValidatorGroup)
{
resolvedValidator.resolver.Scope?.Dispose();
}
}
}
}
}

if (invalidResults.Any())
{
_validationErrorsHandler.Handle(context, invalidResults);
return;
}

await _next(context);
}

IEnumerable<(string param, ResolvedValidator resolver)> GetValidators(IMiddlewareContext context, IInputField argument)
{
if (!argument.ContextData.TryGetValue(WellKnownContextData.ValidatorDescriptorsParams, out var validatorDescriptorsRaw)
|| validatorDescriptorsRaw is not Dictionary<string, List<ValidatorDescriptor>> validatorDescriptors)
{
yield break;
}

foreach (var validatorDescriptor in validatorDescriptors)
{
foreach (var validatorDescriptorEntry in validatorDescriptor.Value)
{
if (validatorDescriptorEntry.RequiresOwnScope)
{
var scope = context.Services.CreateScope(); // disposed by middleware
var validator = (IValidator)scope.ServiceProvider.GetRequiredService(validatorDescriptorEntry.ValidatorType);
yield return (validatorDescriptor.Key, new ResolvedValidator(validator, scope));
}
else
{
var validator = (IValidator)context.Services.GetRequiredService(validatorDescriptorEntry.ValidatorType);
yield return (validatorDescriptor.Key, new ResolvedValidator(validator));
}
}
}
}
}
}
3 changes: 3 additions & 0 deletions src/FairyBread/WellKnownContextData.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,5 +15,8 @@ internal static class WellKnownContextData

public const string ValidatorDescriptors =
Prefix + ".Validators";

public const string ValidatorDescriptorsParams =
Prefix + ".Validators.Params";
}
}