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.
services.AddTransient<IValidator<TollUpdateAPI>, TollUpdateFluentValidator>();
– Baronial.AddFluentValidation();
inConfigureServices
afterAddMvc
? – Wirehaired