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
?
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.
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.
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.
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 :)
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With