Disabling model binding on ASP.NET Web APIs Beta
The code for this post can be found in the MSDN Code Gallery.
As I mentioned in my first post about the ASP.NET Web APIs, people used to WCF services may get a little confused at first with the concept of model binding, which is the process by which the inputs to the actions (formerly known as operations) are retrieved from the request and bound to the parameters of the method which implements that action. Since I’ve already got three questions about how to disable the model binding, I decided to write a post about it.
In principle, the model binder performs the same role as a message formatter in WCF, but they’re different in many ways, including that the model binder can retrieve parameters from any part of the request (including the request body, URI, path, query string, headers and anything a value provider can return a value from), while in WCF the values will come from either a query string converter (query string parameters) or from a serializer (body parameters).
In most cases, both are equally valid and work just as well. There are cases, however, when one wants to take advantage of some of the serializer features (such as customizing attribute and element names for XML, for example), so disabling the model binding and choosing the serialization path is a good alternative. In the example below I’ll show one case where the default model binding doesn’t work as well as we want, and switching to the serializer fixes the issue.
Notice: this information is for the Beta release of the ASP.NET Web APIs. This can (and likely will) change on the RC version of the framework.
To disable the model binding, we need to use the service resolver, and the IRequestContentReadPolicy interface. If you want to change how the action parameters will be populated, you can use a custom implementation of that interface which will tell the runtime to bypass the model binding and go directly to the deserialization when consuming the request body, as shown in the example below. Notice that this selection can be made on a per-action basis, so you don’t need to have a global rule if only some actions need this setting.
- class MyRequestContentReadPolicy : IRequestContentReadPolicy
- {
- public RequestContentReadKind GetRequestContentReadKind(HttpActionDescriptor actionDescriptor)
- {
- if (actionDescriptor.ActionName.Contains("NoModelBinding"))
- {
- return RequestContentReadKind.AsSingleObject;
- }
- else
- {
- return RequestContentReadKind.AsKeyValuePairsOrSingleObject;
- }
- }
- }
The enumeration RequestContentReadKind specifies how the content should be read. To disable model binding (or to choose a serializer to read the body) we need to use the policy to read the body as a single object.
Example: round-tripping XML-annotated types
An example will help explaining this case. The default formatter used to convert between CLR objects and XML representation is the XmlMediaTypeFormatter. By default, it will use the XmlSerializer (this can be changed in the UseDataContractSerializer property of the formatter). If we want to control exactly how the XML will be produced for a certain type, we can use many of the attributes in the System.Xml.Serialization namespace. In the example below, the attributes will change the capitalization of the attributes and elements in the XML representation, while still allowing us to use the PascalCasing for our property names.
- public class Order
- {
- [XmlAttribute(AttributeName = "id")]
- public int Id { get; set; }
- [XmlElement(ElementName = "product")]
- public List<Product> Products { get; set; }
- }
- public class Product
- {
- [XmlAttribute(AttributeName = "id")]
- public int Id { get; set; }
- [XmlAttribute(AttributeName = "name")]
- public string Name { get; set; }
- [XmlAttribute(AttributeName = "unit")]
- public string Unit { get; set; }
- [XmlAttribute(AttributeName = "unitPrice")]
- public double UnitPrice { get; set; }
- }
Now, let’s create a controller which can expose orders. Nothing special about this controller, which will store the orders in memory.
- public class OrdersController : ApiController
- {
- static int nextId = 2;
- static List<Order> allOrders = new List<Order>
- {
- new Order
- {
- Id = 0,
- Products = new List<Product>
- {
- new Product { Id = 0, Name = "Bread", Unit = "loaf", UnitPrice = 2.50 },
- new Product { Id = 1, Name = "Milk", Unit = "gal", UnitPrice = 2.99 }
- }
- }
- };
- public List<Order> GetAll()
- {
- return allOrders;
- }
- public Order GetOne(int id)
- {
- Order result = allOrders.Where(o => o.Id == id).FirstOrDefault();
- if (result != null)
- {
- return result;
- }
- else
- {
- throw new HttpResponseException(HttpStatusCode.NotFound);
- }
- }
- public int Post(Order order)
- {
- order.Id = nextId++;
- allOrders.Add(order);
- return order.Id;
- }
- }
Let’s now add a route which can reach all the actions. Let’s add the action name as part of the route, since we’ll have later a new Post operation in the same controller.
- RouteTable.Routes.MapHttpRoute(
- name: "API",
- routeTemplate:"api/{controller}/{action}/{id}",
- defaults: new { id = RouteParameter.Optional });
We can now test the controller. Sending this request with Fiddler:
We get this response (indentation added for clarity)
<?xml version="1.0" encoding="utf-8"?>
<Order id="0"
xmlns:xsi="https://www.w3.org/2001/XMLSchema-instance"
xmlns:xsd="https://www.w3.org/2001/XMLSchema">
<product id="0" name="Bread" unit="loaf" unitPrice="2.5"/>
<product id="1" name="Milk" unit="gal" unitPrice="2.99"/>
</Order>
So this is what an order is supposed to be represented. Let’s try to send it to the controller:
Ok, the controller accepted the order, and it gave us the order id (2) so we can query it. So if we send a request to https://localhost:35967/api/orders/getone/2, we should get the order back. But this is what we get:
HTTP/1.1 200 OK
Server: ASP.NET Development Server/10.0.0.0
Date: Mon, 27 Feb 2012 20:51:51 GMT
X-AspNet-Version: 4.0.30319
Cache-Control: no-cache
Pragma: no-cache
Expires: -1
Content-Type: text/xml; charset=utf-8
Connection: Close
Content-Length: 152<?xml version="1.0" encoding="utf-8"?>
<Order id="2"
xmlns:xsi="https://www.w3.org/2001/XMLSchema-instance"
xmlns:xsd="https://www.w3.org/2001/XMLSchema"/>
All the product information was lost. That’s one of the limitations of the model binding – dealing with nested XML structures (notice that model binding was originally created for “classic” MVC applications, where data would come from a web page, likely either as JSON or from HTML forms, so this wasn’t an issue then). For this scenario, however, we need the full deserialization capability so we want to disable model binding for this operation.
To show the two behaviors side-by-side, let’s add a new action to the controller for which we’ll choose the “deserialization path”. Notice that its implementation will be exactly the same as the original action.
- public int Post(Order order)
- {
- order.Id = nextId++;
- allOrders.Add(order);
- return order.Id;
- }
- public int PostNoModelBinding(Order order)
- {
- order.Id = nextId++;
- allOrders.Add(order);
- return order.Id;
- }
Now we can hook up the request policy class we showed in the beginning of the post to the runtime, using the service resolver.
- protected void Application_Start(object sender, EventArgs e)
- {
- RouteTable.Routes.MapHttpRoute(
- name: "API",
- routeTemplate:"api/{controller}/{action}/{id}",
- defaults: new { id = RouteParameter.Optional });
- DependencyResolver serviceResolver = GlobalConfiguration.Configuration.ServiceResolver;
- serviceResolver.SetService(typeof(IRequestContentReadPolicy), new MyRequestContentReadPolicy());
- }
So let’s now post the exact same content, except that this time we’ll send it to the action which has the “deserialization path”.
Same response. Now let’s retrieve the order we just added by sending a request to https://localhost:35967/api/orders/getone/2 again.
HTTP/1.1 200 OK
Server: ASP.NET Development Server/10.0.0.0
Date: Mon, 27 Feb 2012 21:18:58 GMT
X-AspNet-Version: 4.0.30319
Cache-Control: no-cache
Pragma: no-cache
Expires: -1
Content-Type: text/xml; charset=utf-8
Connection: Close
Content-Length: 274<?xml version="1.0" encoding="utf-8"?>
<Order id="2"
xmlns:xsi="https://www.w3.org/2001/XMLSchema-instance"
xmlns:xsd="https://www.w3.org/2001/XMLSchema">
<product id="0" name="Bread" unit="loaf" unitPrice="2.5"/>
<product id="1" name="Milk" unit="gal" unitPrice="2.99"/>
</Order>
And we finally get what we expected.
One final note: we don’t think that this is an ideal solution, so we’re working to get to a better place by the RC date. Use this information as a temporary workaround, but this will likely be changed before the final version of ASP.NET MVC 4.
Comments
- Anonymous
March 27, 2012
Has this been resolved in the current asp.net web api? - Anonymous
March 27, 2012
Yes, it has been fixed. The model binding story changed quite a bit between the Beta release and the current bits. And you can even try them now before waiting for the next "official" release - the bits are now being developed live, in the open source project at aspnetwebstack.codeplex.com (see scottgu's post at weblogs.asp.net/.../asp-net-mvc-web-api-razor-and-open-source.aspx for more details)