Model normalization before model validation in Asp.Net Core 2.0+
Asked Answered
L

1

2

I'm using automatic model validation (see "Better Input Processing") to keep my controllers clean; so:

[HttpPost]
[ProducesResponseType(typeof(Product), 201)]
public IActionResult Post([FromBody] Product product)
{
    if (!ModelState.IsValid)
    {
        return BadRequest(ModelState);
    }
    product = _repository.AddProduct(product);
    return CreatedAtAction(nameof(Get), new { id = product.Id }, product);
}

becomes:

[HttpPost]
[ProducesResponseType(201)]
public ActionResult<Product> Post(Product product)
{
    _repository.AddProduct(product);
    return CreatedAtAction(nameof(Get), new { id = product.Id }, product);
}

However, I do have a few models that have a phonenumber property. I would like to 'normalize' these before the model validation is invoked. What I mean is that I want to normalize these properties (of type string) from all kinds of input like:

  • +31 23 456 7890
  • (023) 4567890
  • 023 - 4567 890
  • ...

To E.164 notation:

  • +31234567890

So in whatever form a user enters a phonenumber, before validation is invoked I want to be sure it's always in E.164 form ('normalized'). How this normalization is done is irrelevant (I use libphonenumber if you insist). As a second, maybe less convoluted, example I can imagine a string to be always upper-/lowercased before validation is invoked.

What would be the correct, or best, way to invoke my normalization process before the validation is invoked? Would I have to write some middleware?

Also relevant: my models contain attributes so the normalizer knows which properties to normalize (and how):

class ExampleModel {

    public int Id { get; set; }

    public string Name { get; set; }

    [NormalizedNumber(NumberFormat.E164)]
    public string Phonenumber { get; set; }
}

I guess the middleware(? or whatever the solution is going to be) can then take a model, figure out if any of the properties (recursively) have the attribute and invoke the normalizer if needed.

Lisbethlisbon answered 21/11, 2018 at 17:13 Comment(5)
I would think the earliest accessible point would be in a custom model binder.Trossachs
Custom model binder would be the best way to handle this. Take a look at this dotnetcoretutorials.com/2016/12/28/…Mandorla
Thanks both! I'll have a look into a custom model binder!Lisbethlisbon
@Mandorla It's looking very promising; however, my models can be quite complex and I'd like to invoke the original modelbinder binding and after that invoke my 'formatter'. The link doesn't show how to invoke the 'original' modelbinder from the custom binder correctly so I can, after it did it's work, change all 'tagged' properties. Do you happen to have a good resource for that?Lisbethlisbon
I have posted a follow up question since I can't get this to work the way I'd like to.Lisbethlisbon
M
0

Maybe you can use an approach like this using Formatter. I have used similar approach to convert all incoming dates to UTC format in my API

public class JsonModelFormatter : JsonMediaTypeFormatter
{
    public override System.Threading.Tasks.Task<Object> ReadFromStreamAsync(Type type, Stream readStream, HttpContent content, IFormatterLogger formatterLogger, CancellationToken cancellationToken)
    {

        System.Threading.Tasks.Task<Object> baseTask = base.ReadFromStreamAsync(type, readStream, content, formatterLogger, cancellationToken);

        if (baseTask.Result != null)
        {
            var properties = baseTask.Result.GetType().GetProperties();
            foreach (var property in properties)
            {
                //Check Property attribute and decide if you need to format it
                if (property.CustomAttributes.Where (x=> you condition here))
                {
                    if (property.CanWrite && property.GetValue(baseTask.Result, null) != null)
                    {
                        var propValue = ((string)property.GetValue(baseTask.Result, null));
                       //Update propValue here 
                       property.SetValue(baseTask.Result, newPropValue);
                    }
                }
            }

        }
        return baseTask;
    }

    public override bool CanReadType(Type type)
    {
        return true;
    }
}
Mandorla answered 26/11, 2018 at 22:47 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.