Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Create a custom Model Binder for a custom type

Tags:

asp.net-core

In an ASP.NET CORE 1.1 project I have the following model:

public class GetProductsModel {
  public OrderExpression OrderBy { get; set; }
}

OrderExpression is a class which has the following method:

Boolean TryParse(String value, out OrderExpression expression)

The method creates a OrderExpression instance from a String and can be used:

OrderExpression expression;

Boolean parsed = OrderExpression.TryParse(value, out expression);

How can I create a custom Model Binder to properties of type OrderExpression?

like image 971
Miguel Moura Avatar asked Feb 19 '17 14:02

Miguel Moura


People also ask

What is custom model binder?

In the MVC pattern, Model binding maps the HTTP request data to the parameters of a Controllers action method. The parameter can be of a simple type like integers, strings, double etc. or they may be complex types. MVC then binds the request data to the action parameter by using the parameter name.

What is model binder in Web API?

Model Binding is the most powerful mechanism in Web API 2. It enables the response to receive data as per requester choice. i.e. it may be from the URL either form of Query String or Route data OR even from Request Body. It's just the requester has to decorate the action method with [FromUri] and [FromBody] as desired.

How does model binding work in razor pages?

Razor Pages, by default, bind properties only with non- GET verbs. Binding to properties removes the need to writing code to convert HTTP data to the model type. Binding reduces code by using the same property to render form fields ( <input asp-for="Customer.Name"> ) and accept the input.


1 Answers

I assume that within your request data there is a property orderBy that you want to bind into an OrderExpression using OrderExpression.TryParse.

Let's assume your OrderExpression class looks like follows, where I have provided a very simple implementation of your TryParse method:

public class OrderExpression
{
    public string RawValue { get; set; }
    public static bool TryParse(string value, out OrderExpression expr)
    {
        expr = new OrderExpression { RawValue = value };
        return true;
    }
}

Then you could create a model binder which basically gets the raw string value and calls OrderExpression.TryParse:

public class OrderExpressionBinder : IModelBinder
{
    public Task BindModelAsync(ModelBindingContext bindingContext)
    {            
        var values = bindingContext.ValueProvider.GetValue(bindingContext.ModelName);
        if (values.Length == 0) return Task.CompletedTask;

        // Attempt to parse
        var stringValue = values.FirstValue;
        OrderExpression expression;
        if (OrderExpression.TryParse(stringValue, out expression))
        {
            bindingContext.ModelState.SetModelValue(bindingContext.ModelName, expression, stringValue);
            bindingContext.Result = ModelBindingResult.Success(expression);
        }

        return Task.CompletedTask;
    }
}

You will also need a new model binder provider, which returns your new binder just for the OrderExpression type:

public class OrderExpressionBinderProvider : IModelBinderProvider
{
    public IModelBinder GetBinder(ModelBinderProviderContext context)
    {
        return context.Metadata.ModelType == typeof(OrderExpression) ? new OrderExpressionBinder() : null;
    }
}

// It should be registered in your Startup class, adding it to the ModelBinderProviders collection:
services.AddMvc(opts => {
    opts.ModelBinderProviders.Insert(0, new OrderExpressionBinderProvider());
});

With this in place, you will be able to bind OrderExpression parameters of the controller actions. Something like in the following example:

[HttpPost]
public IActionResult Products([FromBody]OrderExpression orderBy) 
{
    return Ok();
}

$.ajax({
    method: 'POST', 
    dataType: 'json', 
    url: '/home/products', 
    data: {orderby: 'my orderby expression'}
});

However there is something else that needs to be done for you to be able to send a json and bind it to a complex model like GetProductsModel which internally contains an OrderExpression. I am talking about a scenario like this:

[HttpPost]
public IActionResult Products([FromBody]GetProductsModel model)
{
    return Ok();
}

public class GetProductsModel
{
    public OrderExpression OrderBy { get; set; }
}

$.ajax({
    method: 'POST', 
    dataType: 'json', 
    contentType: 'application/json; charset=utf-8', 
    url: '/home/products', 
    data: JSON.stringify({orderby: 'my orderby expression'})
});

In that scenario ASP.Net Core will just use Newtonsoft.Json as the InputFormatter and convert the received json into an instance of the GetProductsModel model, without trying to use the new OrderExpressionBinderProvider for the internal property.

Luckily, you can also tell Newtonsoft.Json how to format properties of OrderExpression type by creating your JsonConverter:

public class OrderExpressionJsonConverter : JsonConverter
{
    public override bool CanConvert(Type objectType)
    {
        return objectType == typeof(OrderExpression);
    }

    public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
    {
        var stringValue = reader.Value?.ToString();
        OrderExpression expression;
        if (OrderExpression.TryParse(stringValue, out expression))
        {
            return expression;
        }
        return null;
    }

    public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
    {
        throw new NotImplementedException();
    }
}

Which should be registered in your Startup class:

services.AddMvc(opts => {
    opts.ModelBinderProviders.Insert(0, new OrderExpressionBinderProvider());

}).AddJsonOptions(opts => {
    opts.SerializerSettings.Converters.Add(new OrderExpressionJsonConverter());
});

Now you will finally be able to handle both scenarios :)

like image 67
Daniel J.G. Avatar answered Sep 20 '22 21:09

Daniel J.G.