Keith
Keith

Reputation: 1038

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?

Upvotes: 2

Views: 5333

Answers (3)

Julian Gr
Julian Gr

Reputation: 123

You could have 3 classes:

  1. GrapeColourValidator: This class will contain validations needed for Create and Update

  2. GrapeColourCreateValidator: This class inherits from GrapeColourValidator and contains only the validations for Creating the resource.

  3. GrapeColourUpdateValidator: This class inherits from GrapeColourValidator and contains only the validations for Updating the resource.

And then in your Controller you can inject the 2 validators and use them when needed.

public class GrapesController : Controller
{
    private readonly GrapeColourCreateValidator createValidator;
    private readonly GrapeColourUpdateValidator updateValidator;

    public GrapesController(GrapeColourCreateValidator createValidator, GrapeColourUpdateValidator updateValidator)
    {
        this.createValidator = createValidator;
        this.updateValidator = updateValidator;
    }

    public async Task<IActionResult> Create(AddOrUpdateGrapeRequest request)
    {
        var results = await createValidator.ValidateAsync(request);

        if (!results.IsValid)
        {
            results.AddToModelState(ModelState);

            return BadRequest();
        }

        // save grape color

        return // RedirectToAction() or Ok()
    }

    public async Task<IActionResult> Update(AddOrUpdateGrapeRequest request)
    {
        var results = await updateValidator.ValidateAsync(request);

        if (!results.IsValid)
        {
            results.AddToModelState(ModelState);

            return BadRequest();
        }

        // save grape color

        return // RedirectToAction() or Ok()
    }
}

Upvotes: 1

hooah
hooah

Reputation: 76

3 years too late, but for anyone wondering how to do this - you can use RootContextData https://docs.fluentvalidation.net/en/latest/advanced.html#root-context-data

var context = ValidationContext<yourTypeHere>(request);
context.RootContextData["IsUpdate"] = true;
validator.Validate(context);

and then in your validator you can get the IsUpdate value out of the context

context.RootContextData.TryGetValue("IsUpdate", out var IsUpdate)

Upvotes: 4

Greg Burghardt
Greg Burghardt

Reputation: 18928

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.

Upvotes: 2

Related Questions