Skip to content

Latest commit

 

History

History
117 lines (96 loc) · 3.15 KB

root-validator-segregation.md

File metadata and controls

117 lines (96 loc) · 3.15 KB

Root validator segregation

Imagine you have a User

public record User(string FirstName, string LastName, string Address);

You want to validate address separately, to have a compact and readable validator, so you create UserValidator

public class UserValidator : AbstractValidator<User>
{
  public UserValidator()
  {
    ...

    RuleFor(x => x.Address)
      .SetValidator(new DefaultUserAddressValidator());
  }
}

And set address validator to DefaultUserAddressValidator

public class DefaultUserAddressValidator : AbstractValidator<string>
{
  public DefaultUserAddressValidator()
  {
    RuleFor(x => x)
      .Must(...)
      .WithMessage(...)
  }
}

But it is tightly coupling two validators. What if you have another way to validate the address? Or to validate address with the first name?

public class CustomUserAddressValidator : AbstractValidator<string>
{
  public DefaultUserAddressValidator()
  {
    RuleFor(x => x)
      .Must(...)
      .WithMessage(...);
  }
}

To override address validator you should pass it as a constructor argument (or maybe null?)

public class UserValidator : AbstractValidator<User>
{
  public UserValidator(IValidator<string> validator)
  {
    ...

    RuleFor(x => x.Address)
      // Some null checks and if statements omitted
      .SetValidator(validator);
  }
}

But how can you do that with DI? Which IValidator<string> should you resolve? There are some solutions (separate user validators, factories, manual DI, Validator.Include(), etc.), but this library supports multiple root validators and automatically calls them sequentially, converting failures to useful GraphQL errors

To use this feature just call multiple UseValidator/UseValidators with multiple User validators

public class DefaultAddressUserValidator : AbstractValidator<User>
{
  public DefaultAddressUserValidator()
  {
    RuleFor(x => x.Address)
      ...
      ;
  }
}

public class CustomAddressUserValidator : AbstractValidator<User>
{
  public CustomAddressUserValidator()
  {
    RuleFor(x => x.Address)
      ...
      ;
  }
}

# Usage
descriptor.Field(x => x.CreateUser(default!))
  .Argument("input", argument => argument.UseFluentValidation(options =>
  {
    options.UseValidator<UserValidator>()
      // Use required validator without UserValidator modification
      .UseValidator<DefaultAddressUserValidator>() or/and .UseValidator<CustomAddressUserValidator>();
  }));

# or
... CreateUser([UseFluentValidation(typeof(UserValidator), typeof(DefaultAddressUserValidator))] User user)

descriptor.Field(x => x.CreateExternalUser(default!))
  .Argument("input", argument => argument.UseFluentValidation(options =>
  {
    options.UseValidator<UserValidator>()
      // For external users we will use Custom address validator
      .UseValidator<CustomAddressUserValidator>();
  }));

# or
... CreateExternalUser([UseFluentValidation(typeof(UserValidator), typeof(CustomAddressUserValidator))] User user)

This is not a silver bullet, this library handles only root validators and only static validator sequences. If you want dynamically change validation rules (feature flags?) prefer another solution