ASP.NET Web API Updates – May 14

We have been on a roll with respect to changes over the last few weeks – here’s some of the highlights. If you want to see the full list then you can either track the commit page or follow the project which will then show you the updates on your codeplex home page. As in the previous update this is a sneak-peak of code checked into our CodePlex source code repository but not yet released as part of our official installer. See Using Nightly ASP.NET Web Stack NuGet Packages for how to try out these features.

Per Controller Type Configuration

ASP.NET Web API has a HttpConfiguration object that provides configuration for features including routing, dependency resolution, media type formatters, and message handlers. However, while the HttpConfiguration allows you to configure most aspects of ASP.NET Web API, it does not allow different configurations per controller type.  This feature changes that in that we have added per-controller-type configuration. In essence, a controller type can have its own “shadow copy” of the global HttpConfiguration object but override specific settings that only applies to that controller type. This is automatically applied to all controller instances of the given controller-type.

Mike Stall has a great blog about the feature in detail including examples of how to use it with an ApiController so check it out – this is an exciting feature!

Content Negotiated Error Messages

We have unified the generation of error messages regardless of whether they are generated as part of a normal message flow, as part of an exception path, or as part of model state validation. This means that regardless of how you generate a 4xx or 5xx status code, the result will be a uniform, content negotiated message, making it easier to handle by clients.

For example, you can generate a 409 Conflict response like this:

    1: public Contact Get()
    2: {
    3:     HttpResponseMessage response = Request.CreateErrorResponse(
    4:         HttpStatusCode.Conflict, "Check out this message!");
    5:     throw new HttpResponseException(response);
    6: }

or like this:

    1: public HttpResponseMessage Get(HttpRequestMessage request)
    2: {
    3:     HttpResponseMessage response = request.CreateErrorResponse(
    4:         HttpStatusCode.Conflict, "Check out this message!");
    5:     return response;
    6: }

In either case, the response will look like this in Fiddler if you ask for “text/json”:

409Conflict

Note: By default the HttpConfiguration.IncludeErrorDetailPolicy setting only exposes a generic message to external users so that we inadvertently leak internal information.

Content Negotiation Improvements

Lastly, we re-organized the content negotiation code to all be gathered in our DefaultContentNegotiator class. The content negotiation is fully pluggable so that if you want to roll your own then you can implement the IContentNegotiator interface and hook it in through the configuration. However, now you can also just derive from DefaultContentNegotiator and pretty much override any aspect that you like instead of starting from scratch. For a full description, please see this commit.

In addition we added support for not picking accept header values with a quality factor of 0.0. Before we treated this as a small but possible match while now we completely filter them out of the picture. That is, if you send something like “Accept: text/xml; q=0.0” then you won’t get it.

We also added a hook so that it is possible to create an instance of the DefaultContentNegotiator that returns null if none of the accept header values were matched. This makes it possible to inject your own DefaultContentNegotiator instance (in HttpConfiguration.Services) that is suited for generating 406 responses if no match on accept headers were found.

This sample shows a simple self host which hooks in a special DefaultContentNegotiator instance set to return null if not matching on any Accept header field in the request:

    1: string baseAddress = "https://localhost:8080/";
    2: HttpSelfHostConfiguration config = new HttpSelfHostConfiguration(baseAddress);
    3:  
    4: DefaultContentNegotiator negotiator = new DefaultContentNegotiator(excludeMatchOnTypeOnly: true);
    5: config.Services.Replace(typeof(IContentNegotiator), negotiator);
    6:  
    7: // Add a route
    8: config.Routes.MapHttpRoute(
    9:   name: "defaultAction",
   10:   routeTemplate: "api/{controller}/{action}/{id}",
   11:   defaults: new { controller = "Home", id = RouteParameter.Optional });
   12:  
   13: config.Routes.MapHttpRoute(
   14:   name: "default",
   15:   routeTemplate: "api/{controller}/{id}",
   16:   defaults: new { controller = "Home", id = RouteParameter.Optional });
   17:  
   18: HttpSelfHostServer server = new HttpSelfHostServer(config);
   19: server.OpenAsync().Wait();
   20:  
   21: Console.WriteLine("Listening on " + baseAddress);

We should now see a 406 Not Acceptable response if there is no matching Accept header field so let’s try it out in Fiddler with no Accept header at all:

ContentNegotiationNotAcceptable

And indeed there is a 406 response. If we add an Accept header then we get a 200 response like this:

ContentNegotiationAcceptable

Have fun!

Henrik

del.icio.us Tags: asp.net,webapi,httpclient