Search code examples
c#fluentvalidation

Pass parameter to validator - fluent validation


I have a validator that I use for both an insert, and an update. One of the checks I do is to see if what is being inserted already exists. The code for the validator is:

public GrapeColourValidator(IGrapeRepository grapeRepository)
        {
            _grapeRepository = grapeRepository;

            RuleFor(x => x.Colour)
                .NotEmpty()
                .WithMessage("Colour is required")
                .MaximumLength(_maxLength)
                .WithMessage($"Colour cannot be more that {_maxLength} characters");

            RuleFor(x => x)
                .MustAsync(async (grapeColour, context, cancellation) =>
                {
                    return await GrapeColourExists(grapeColour.Colour).ConfigureAwait(false);
                })
                .WithMessage($"Grape colour already exists");
        }

        private async Task<bool> GrapeColourExists(string grapeColour)
        {
            var colourResult = await _grapeRepository.GetByColour(grapeColour).ConfigureAwait(false);
            return !colourResult.Any(x => x.Colour == grapeColour);
        }

The issue with this is that it runs for Update also, so the colour will definitely exists. What I want to do is pass a parameter, so I could do something like:

if(isInsert)
            {
                RuleFor(x => x)
                .MustAsync(async (grapeColour, context, cancellation) =>
                {
                    return await GrapeColourExists(grapeColour.Colour).ConfigureAwait(false);
                })
                .WithMessage($"Grape colour already exists");
            }

Is this possible?


Solution

  • I usually accomplish this with a property on the view model I am validating. Basically, you need a flag on the view model indicating whether it represents "new" or "existing" data.

    If you have a numeric identifier (which includes Guid's) just test for the default value of the Id:

    // For int identifiers:
    public class ViewModel
    {
        public int Id { get; set; }
    
        public bool IsNew => Id == default(int);
    }
    
    // For GUID identifiers:
    public class ViewModel
    {
        public Guid Id { get; set; }
    
        public bool IsNew => Id == default(Guid );
    }
    

    And then add a When(...) clause to the validation rule:

    RuleFor(x => x.Property)
        .Must(...)
        .When(x => x.IsNew);
    

    The downside with testing a view model property is that it could be vulnerable to request tampering. Someone can POST a non default Guid or int in the request and make the validator think it is validating a persisted object.

    Then again, you should be authenticating and authorizing every request, as well as checking anti-forgery tokens.