0

I am using Fluent API to validate the payload in my API. Street Address/MailingAddress/Lockbox Address are three different properties in the domain model. I want to make sure in my validation that Street address and mailing address is only passed once.

Correct Payload

{
"id" :123,
"name":"test",
"streetAddress": {
"city":"London",
"address":"q23"
},
"MailingAddress": {
"city":"NewYork",
"address":"q2453"
},
"LockBoxAddress": {
"city":"Miami",
"address":"q23888"
}
}

Domain Model

public string id{get;set;}
public string name{get;set;}
public Address streetAddress{get;set;}
public Address MailingAddress{get;set;}
public Address LockboxAddress{get;set;}

Incorrect Payload

{
"id" :123,
"name":"test",
"streetAddress": {
"city":"London",
"address":"q23"
},
"streetAddress": {
"city":"NewYork",
"address":"q2453"
}
}

I would like the above payload to error out by saying you cannot pass multiple street addresses and I am using Fluent API

Fluent API

RuleFor(x => x.streetAddress).Count(x =>x < 2).When(x => x.streetAddress!= null); 

There is no property to get the count for the model. Any Ideas?

Jota.Toledo
  • 27,293
  • 11
  • 59
  • 73
Learn AspNet
  • 1,192
  • 3
  • 34
  • 74

1 Answers1

0
  1. You can't make it via Fluent validation API. Because there'll be only one streetAddress property in your domain model after the payload deserialization. On the other hand, your code RuleFor(x => x.streetAddress) returns IRuleBuilderInitial<XModel, Address> instead of IRuleBuilderInitial<XModel, IList<Address>>. To achieve your goal, you should make sure your validation happens before FluentValidation, i.e., validate the payload when deserializing .
  2. Sometimes a client might send a json with duplicated keys, if you're using ASP.NET Core 2.1 or ASP.NET 2.2, it won't fail by default.

How to solve

To reject such a payload, make sure your version of Newtonsoft.Json is 12.0.1 or higher. If you're not sure, just add such a reference in your *.csproj:

<PackageReference Include="Newtonsoft.Json" Version="12.0.1" />

And then create a custom Model Binder to handle duplicated keys:

public class RejectDuplicatedKeysModelBinder : IModelBinder
{
    private JsonLoadSettings _loadSettings  = new JsonLoadSettings(){ DuplicatePropertyNameHandling = DuplicatePropertyNameHandling.Error };

    public Task BindModelAsync(ModelBindingContext bindingContext)
    {
        if (bindingContext == null) { throw new ArgumentNullException(nameof(bindingContext)); }
        var modelName = bindingContext.BinderModelName ?? bindingContext.OriginalModelName ?? bindingContext.FieldName ?? String.Empty;
        var modelType = bindingContext.ModelType;

        var req = bindingContext.HttpContext.Request;
        var raw = req.Body;
        if (raw == null) {
            bindingContext.ModelState.AddModelError(modelName, "invalid request body stream");
            return Task.CompletedTask;
        }
        JsonTextReader reader = new JsonTextReader(new StreamReader(raw));
        try {
            var json = (JObject)JToken.Load(reader, this._loadSettings);
            var o = json.ToObject(modelType);
            bindingContext.Result = ModelBindingResult.Success(o);
        }
        catch (JsonReaderException e) {
            var msg = $"wrong property with key='{e.Path}': {e.Message}";
            bindingContext.ModelState.AddModelError(modelName, msg); 
            bindingContext.Result = ModelBindingResult.Failed();
        } 
        catch(Exception e) {
            bindingContext.ModelState.AddModelError(modelName, e.ToString()); // you might want to custom the error info
            bindingContext.Result = ModelBindingResult.Failed();
        }
        return Task.CompletedTask;
    }
}

By this way, the duplicated error info will be added into ModelState.

Test Case

Let's create a action method for test:

[HttpPost]
public IActionResult Test([ModelBinder(typeof(RejectDuplicatedKeysModelBinder))]XModel model){
    if(! this.ModelState.IsValid){
        var problemDetails = new ValidationProblemDetails(this.ModelState)
        {
            Status = StatusCodes.Status400BadRequest,
        };
        return BadRequest(problemDetails);
    }
    return new JsonResult(model);
}

When there's a json with duplicated keys, we'll get an error like: enter image description here

itminus
  • 23,772
  • 2
  • 53
  • 88