FluentValidator and JsonPatchDocument
Asked Answered
B

4

6

I have WebAPI (.NET Core) and use FluentValidator to validate model, including updating. I use PATCH verb and have the following method:

    public IActionResult Update(int id, [FromBody] JsonPatchDocument<TollUpdateAPI> jsonPatchDocument)
    {

also, I have a validator class:

public class TollUpdateFluentValidator : AbstractValidator<TollUpdateAPI>
{
    public TollUpdateFluentValidator ()
    {
        RuleFor(d => d.Date)
            .NotNull().WithMessage("Date is required");

        RuleFor(d => d.DriverId)
            .GreaterThan(0).WithMessage("Invalid DriverId");

        RuleFor(d => d.Amount)
            .NotNull().WithMessage("Amount is required");

        RuleFor(d => d.Amount)
            .GreaterThanOrEqualTo(0).WithMessage("Invalid Amount");
    }
}

and map this validator in Startup class:

        services.AddTransient<IValidator<TollUpdateAPI>, TollUpdateFluentValidator>();

but it does not work. How to write valid FluentValidator for my task?

Baronial answered 28/5, 2019 at 14:18 Comment(5)
What it is the problem? When do you call your validator?Wirehaired
this validation should be called automatically, because I added resolving string: services.AddTransient<IValidator<TollUpdateAPI>, TollUpdateFluentValidator>();Baronial
Have you called .AddFluentValidation(); in ConfigureServices after AddMvc?Wirehaired
yes, other validators work fineBaronial
Form github.com/aspnet/JsonPatch/issues/18 , It seems that the build-in FluentValidation method does not work for JsonPatch document.Interdisciplinary
L
2

You will need to trigger the validation manually. Your action method will be similar to this:

public IActionResult Update(int id, [FromBody] JsonPatchDocument<TollUpdateAPI> jsonPatchDocument)
{
    // Load your db entity
    var myDbEntity = myService.LoadEntityFromDb(id);

    // Copy/Map data to the entity to patch using AutoMapper for example
    var entityToPatch = myMapper.Map<TollUpdateAPI>(myDbEntity);

    // Apply the patch to the entity to patch
    jsonPatchDocument.ApplyTo(entityToPatch);

    // Trigger validation manually
    var validationResult = new TollUpdateFluentValidator().Validate(entityToPatch);
    if (!validationResult.IsValid)
    {
        // Add validation errors to ModelState
        foreach (var error in validationResult.Errors)
        {
            ModelState.AddModelError(error.PropertyName, error.ErrorMessage);
        }

        // Patch failed, return 422 result
        return UnprocessableEntity(ModelState);
    }

    // Map the patch to the dbEntity
    myMapper.Map(entityToPatch, myDbEntity);
    myService.SaveChangesToDb();

    // So far so good, patch done
    return NoContent();
}
Lysin answered 5/10, 2020 at 0:15 Comment(0)
S
2

You may try the below generic validator - it validates only updated properties:

    public class JsonPatchDocumentValidator<T> : AbstractValidator<JsonPatchDocument<T>> where T: class, new()
    {
        private readonly IValidator<T> _validator;

        public JsonPatchDocumentValidator(IValidator<T> validator)
        {
            _validator = validator;
        }

        private static string NormalizePropertyName(string propertyName)
        {
            if (propertyName[0] == '/')
            {
                propertyName = propertyName.Substring(1);
            }
            return char.ToUpper(propertyName[0]) + propertyName.Substring(1);
        }
        // apply path to the model
        private static T ApplyPath(JsonPatchDocument<T> patchDocument)
        {
            var model = new T();
            patchDocument.ApplyTo(model);
            return model;
        }
        // returns only updated properties
        private static string[] CollectUpdatedProperties(JsonPatchDocument<T> patchDocument)
            => patchDocument.Operations.Select(t => NormalizePropertyName(t.path)).Distinct().ToArray();
        public override ValidationResult Validate(ValidationContext<JsonPatchDocument<T>> context)
        {
            return _validator.Validate(ApplyPath(context.InstanceToValidate),
                o => o.IncludeProperties(CollectUpdatedProperties(context.InstanceToValidate)));
        }

        public override async Task<ValidationResult> ValidateAsync(ValidationContext<JsonPatchDocument<T>> context, CancellationToken cancellation = new CancellationToken())
        {
            return await _validator.ValidateAsync(ApplyPath(context.InstanceToValidate),
                o => o.IncludeProperties(CollectUpdatedProperties(context.InstanceToValidate)), cancellation);
        }
    }

it has to be registered manually:


builder.Services.AddScoped<IValidator<JsonPatchDocument<TollUpdateAPI>>, JsonPatchDocumentValidator<TollUpdateAPI>>();

Snowball answered 25/5, 2022 at 19:45 Comment(0)
C
1

You can utilize a custom rule builder for this. It might not be the most elegant way of handling it but at least the validation logic is where you expect it to be.

Say you have the following request model:

public class CarRequestModel
{
    public string Make { get; set; }
    public string Model { get; set; }
    public decimal EngineDisplacement { get; set; }
}

Your Validator class can inherit from the AbstractValidator of JsonPatchDocument instead of the concrete request model type.

The fluent validator, on the other hand, provides us with decent extension points such as the Custom rule.

Combining these two ideas you can create something like this:

public class Validator : AbstractValidator<JsonPatchDocument<CarRequestModel>>
{
    public Validator()
    {
        RuleForEach(x => x.Operations)
           .Custom(HandleInternalPropertyValidation);
    }

    private void HandleInternalPropertyValidation(JsonPatchOperation property, CustomContext context)
    {
        void AddFailureForPropertyIf<T>(
            Expression<Func<T, object>> propertySelector,
            JsonPatchOperationType operation,
            Func<JsonPatchOperation, bool> predicate, string errorMessage)
        {
            var propertyName = (propertySelector.Body as MemberExpression)?.Member.Name;
            if (propertyName is null)
                throw new ArgumentException("Property selector must be of type MemberExpression");

            if (!property.Path.ToLowerInvariant().Contains(propertyName.ToLowerInvariant()) ||
                property.Operation != operation) return;

            if (predicate(property)) context.AddFailure(propertyName, errorMessage);
        }

        AddFailureForPropertyIf<CarRequestModel>(x => x.Make, JsonPatchOperationType.remove,
            x => true, "Car Make cannot be removed.");
        AddFailureForPropertyIf<CarRequestModel>(x => x.EngineDisplacement, JsonPatchOperationType.replace,
            x => (decimal) x.Value < 12m, "Engine displacement must be less than 12l.");
    }
}

In some cases, it might be tedious to write down all the actions that are not allowed from the domain perspective but are defined in the JsonPatch RFC.

This problem could be eased by defining none but rules which would define the set of operations that are valid from the perspective of your domain.

Cruiserweight answered 5/11, 2020 at 7:41 Comment(0)
T
0

Realization bellow allow use IValidator<Model> inside IValidator<JsonPatchDocument<Model>>, but you need create model with valid properties values.

public class ModelValidator : AbstractValidator<JsonPatchDocument<Model>>
{
    public override ValidationResult Validate(ValidationContext<JsonPatchDocument<Model>> context)
    {
        return _validator.Validate(GetRequestToValidate(context));
    }

    public override Task<ValidationResult> ValidateAsync(ValidationContext<JsonPatchDocument<Model>> context, CancellationToken cancellation = default)
    {
        return _validator.ValidateAsync(GetRequestToValidate(context), cancellation);
    }

    private static Model GetRequestToValidate(ValidationContext<JsonPatchDocument<Model>> context)
    {
        var validModel = new Model()
                           {
                               Name = nameof(Model.Name),
                               Url = nameof(Model.Url)
                           };

        context.InstanceToValidate.ApplyTo(validModel);
        return validModel;
    }

    private class Validator : AbstractValidator<Model>
    {
        /// <inheritdoc />
        public Validator()
        {
            RuleFor(r => r.Name).NotEmpty();
            RuleFor(r => r.Url).NotEmpty();
        }
    }

    private static readonly Validator _validator = new();
}
Tradescantia answered 26/7, 2021 at 11:7 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.