I am having a lot of trouble getting custom model binding to work when posting x-www-form-urlencoded
data. I've tried every way I can think of and nothing seems to produce the desired result. Note when posting JSON data, my JsonConverters and so forth all work just fine. It's when I post as x-www-form-urlencoded
that the system can't seem to figure out how to bind my model.
My test case is that I'd like to bind a TimeZoneInfo object as part of my model.
Here's my model binder:
public class TimeZoneModelBinder : SystemizerModelBinder
{
protected override object BindModel(string attemptedValue, Action<string> addModelError)
{
try
{
return TimeZoneInfo.FindSystemTimeZoneById(attemptedValue);
}
catch(TimeZoneNotFoundException)
{
addModelError("The value was not a valid time zone ID. See the GetSupportedTimeZones Api call for a list of valid time zone IDs.");
return null;
}
}
}
Here's the base class I'm using:
public abstract class SystemizerModelBinder : IModelBinder
{
public bool BindModel(HttpActionContext actionContext, ModelBindingContext bindingContext)
{
var name = GetModelName(bindingContext.ModelName);
var valueProviderResult = bindingContext.ValueProvider.GetValue(name);
if(valueProviderResult == null || string.IsNullOrWhiteSpace(valueProviderResult.AttemptedValue))
return false;
var success = true;
var value = BindModel(valueProviderResult.AttemptedValue, s =>
{
success = false;
bindingContext.ModelState.AddModelError(name, s);
});
bindingContext.Model = value;
bindingContext.ModelState.SetModelValue(name, new System.Web.Http.ValueProviders.ValueProviderResult(value, valueProviderResult.AttemptedValue, valueProviderResult.Culture));
return success;
}
private string GetModelName(string name)
{
var n = name.LastIndexOf(".", StringComparison.Ordinal);
return n < 0 || n >= name.Length - 1 ? name : name.Substring(n + 1);
}
protected abstract object BindModel(string attemptedValue, Action<string> addModelError);
}
I used a base class like this to make it simple to create additional custom model binders.
Here's my model binder provider. Note that this is getting invoked correctly from my IoC container, so I won't bother to show that aspect of my code.
public class SystemizerModelBinderProvider : ModelBinderProvider
{
public override IModelBinder GetBinder(HttpConfiguration configuration, Type modelType)
{
if(modelType == typeof(TimeZoneInfo))
return new TimeZoneModelBinder();
return null;
}
}
Finally, here's the action method and model class:
[DataContract)]
public class TestModel
{
[DataMember]
public TimeZoneInfo TimeZone { get; set; }
}
[HttpPost]
public HttpResponseMessage Test(TestModel model)
{
return Request.CreateResponse(HttpStatusCode.OK, model);
}
For the action method, I have tried:
public HttpResponseMessage Test([FromBody] TestModel model)
This invokes the FormUrlEncodedMediaFormatter
, which seems to ignore my custom model binder altogether.
public HttpResponseMessage Test([ModelBinder] TestModel model)
This calls into my custom model binder, as expected, but then it only provides ValueProviders for RouteData
and QueryString
and for some reason doesn't provide anything for body content. See below:
I've also tried decorating the class itself with ModelBinder(typeof(SystemizerModelBinderProvider))
Why does model binding ONLY occur when I use the [ModelBinder] attribute, and why does it ONLY try to read route and querystring values and ignore body content? Why does FromBody
ignore my custom model binder provider?
How do I create a scenario where I can receive POSTED x-www-form-urlencoded
data and successfully bind model properties using custom logic?
I would recommend you reading the following blog post
in which Mike Stall explains in details how model binding works in the Web API:
There are 2 techniques for binding parameters: Model Binding and Formatters. In practice, WebAPI uses model binding to read from the query string and Formatters to read from the body.
Here are the basic rules to determine whether a parameter is read with model binding or a formatter:
- If the parameter has no attribute on it, then the decision is made purely on the parameter’s .NET type. "Simple types" uses model binding. Complex types uses the formatters. A "simple type" includes: primitives, TimeSpan, DateTime, Guid, Decimal, String, or something with a TypeConverter that converts from strings.
- You can use a
[FromBody]
attribute to specify that a parameter should be read from the body.- You can use a
[ModelBinder]
attribute on the parameter or the parameter’s type to specify that a parameter should be model bound. This attribute also lets you configure the model binder.[FromUri]
is a derived instance of[ModelBinder]
that specifically configures a model binder to only look in the URI.- The body can only be read once. So if you have 2 complex types in the signature, at least one of them must have a [ModelBinder] attribute on it.
So if the source of your data is the request body then you can create a custom MediaTypeFormatter rather than a model binder.
ModelBinder seem relatively better to use than MediaTypeFormatter. You do not need to register it globally.
I found another alternative to use model binder to bind complex object types in Web API. In model binder, I am reading request body as string and then using JSON.NET to deserialize it to required object type. It can be used to map array of complex object types as well.
I added a model binder as follows:
public class PollRequestModelBinder : IModelBinder
{
public bool BindModel(HttpActionContext actionContext, ModelBindingContext bindingContext)
{
var body = actionContext.Request.Content.ReadAsStringAsync().Result;
var pollRequest = JsonConvert.DeserializeObject<PollRequest>(body);
bindingContext.Model = pollRequest;
return true;
}
}
And then I am using it in Web API controller as follows:
public async Task<PollResponse> Post(Guid instanceId, [ModelBinder(typeof(PollRequestModelBinder))]PollRequest request)
{
// api implementation
}
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