A Usability Issue with ASP.NET Web Api with A Solution
I did some messing around with the ASP.NET Web Api this weekend and noticed what I think is a usability flaw in how they built the Api. Figured I would spell things out and see if anyone has any thoughts. And I do have a solution, though some will think the cure is worse than the disease.
Before we get started, a note. Big thanks to Tugberk Ugurlu (@tourismgeek) for talking through this with me. He doesn’t actually like this but he was a good conversation partner :)
Some Basics
So let’s talk some basics to set the groundwork of the discussion. First, you can return an HttpResponseMessage directly from a controller method, like so:
public HttpResponseMessage Get()
{
return new HttpResponseMessage(HttpStatusCode.BadGateway);
}
Alternatively, you can return a regular type and it will get serialized correctly in the normal content negotiation pipeline. Pretty handy.
public Thingy Get()
{
return new Thingy
{
Name = "mabob"
};
}
If I want to return an error message, I could do it like this:
public HttpResponseMessage Get()
{
var error = new HttpError("You have failed.");
return Request.CreateErrorResponse(HttpStatusCode.BadRequest, error);
}
Using the default json, serialization, I would look like this (with some extraneous headers removed).
HTTP/1.1 400 Bad Request
Content-Type: application/json; charset=utf-8
Date: Mon, 10 Sep 2012 03:16:18 GMT
Content-Length: 30
{"Message":"You have failed."}
If that’s what you want, that’s cool.
HttpResponseMessage and Payloads
But that’s not what I want. Otherwise, I wouldn’t be writing this post. So here are two things that I think are weird about how things work. First, you can’t do this (it won’t compile):
//Don’t do this
public HttpResponseMessage Get(string stuff)
{
if (String.IsNullOrEmpty(stuff))
{
var error = new HttpError("You have failed.");
return Request.CreateErrorResponse(HttpStatusCode.BadRequest, error);
}
return new Thingy
{
Name = "mabob"
};
}
But that’s logically what I need to do. So let’s solve this problem:
public HttpResponseMessage Get(string stuff)
{
if (String.IsNullOrEmpty(stuff))
{
var error = new HttpError("You have failed.");
return Request.CreateErrorResponse(HttpStatusCode.BadRequest, error);
}
var thingy = new Thingy
{
Name = "mabob"
};
return Request.CreateResponse(HttpStatusCode.Accepted, thingy);
}
It works! But I don’t like that, for two reasons. First, you are telling the request to create the response. This doesn’t make logical sense. You might say “but the request is the only thing that knows the requested Content-Type header.” That’s true, but in example #2 about I was able to return an object without doing a Request.CreateResponse call. The framework allows for that.
Second, to setup a unit test for this I will now I have to setup the Request object of the controller. Maybe not hard but it’s something I shouldn’t have to do. That’s just working around the framework and that’s just not ideal.
Here is what I would like to do but can’t.
//Don’t do this
public HttpResponseMessage Get(string stuff)
{
if (String.IsNullOrEmpty(stuff))
{
var error = new HttpError("You have failed.");
return new HttpResponseMessage(HttpStatusCode.BadRequest, error);
}
var thingy = new Thingy
{
Name = "mabob"
};
return new HttpResponseMessage(HttpStatusCode.Accepted, thingy);
}
There’s clearly nothing in the framework that would keep this from working. They just chose not to do that. I would have chosen differently.
A Bad Workaround
Okay, here’s a workaround but a bad one.
public HttpResponseMessage Get(string stuff)
{
if (String.IsNullOrEmpty(stuff))
{
var error = new HttpError("You have failed.");
return Request.CreateErrorResponse(HttpStatusCode.BadRequest, error);
}
var thingy = new Thingy
{
Name = "mabob"
};
return new HttpResponseMessage(HttpStatusCode.Accepted)
{
Content = new ObjectContent<Thingy>(thingy, new System.Net.Http.Formatting.JsonMediaTypeFormatter())
};
}
This is what the framework does under the covers to create your actual response so it works but if you do this you end up skipping content negotiation. Or, you could run content negotiation manually and then you have the following.
public HttpResponseMessage Get(string stuff)
{
if (String.IsNullOrEmpty(stuff))
{
var error = new HttpError("You have failed.");
return Request.CreateErrorResponse(HttpStatusCode.BadRequest, error);
}
var thingy = new Thingy
{
Name = "mabob"
};
var config = Request.GetConfiguration();
var contentNegotiator = config.Services.GetContentNegotiator();
var connegResult = contentNegotiator.Negotiate(
typeof(Thingy), Request, config.Formatters
);
return new HttpResponseMessage(HttpStatusCode.Accepted)
{
Content = new ObjectContent<Thingy>(thingy, connegResult.Formatter)
};
}
But all that just to return a custom model is just heinous if you have to do this more than once. So this is not an option.
Another Thing I Want to Do: Custom Error Response
Another thing I want to do is return a custom error model. This is not as straightforward as I thought it would be. Here’s a good discussion on your options, so have a read. Out of the box, it appears you can return an error code with a basic message response or a bunch of gobbledygook (not sure how to spell that) if you use the ModelState stuff.
Anyway, let’s say that I want to return a custom error. This is what I’d like to do.
//Don’t do this. It won’t work.
public HttpResponseMessage Get(string stuff)
{
if (String.IsNullOrEmpty(stuff))
{
return new HttpResponseMessage(HttpStatusCode.BadRequest,
new Error { Code = 76543, Description = "Please supply stuff" });
}
//Other stuff left out
}
This way I can control exactly how my errors get serialized out but that won’t work because you can’t pass in a custom model. And I don’t want to use Request.CreateErrorResponse because I don’t want to have to setup the Request object just to run a test.
A Solution
The basic idea for this solution was Tugberk’s idea (though, like I said, he didn’t even think this was a real problem) but I tweaked a few things to make them my own. To get the api I want for response messages, I created…
public class ExtendedHttpResponseMessage: HttpResponseMessage
{
public ExtendedHttpResponseMessage(HttpStatusCode statusCode)
: base(statusCode) { }
public object Payload { get; set; }
}
public class ExtendedHttpResponseMessage<T> : ExtendedHttpResponseMessage
{
public ExtendedHttpResponseMessage(T payload)
: this(payload, HttpStatusCode.Accepted) { }
public ExtendedHttpResponseMessage(T payload, HttpStatusCode statusCode)
: base(statusCode)
{
Payload = payload;
}
}
Since the framework isn’t going to know what to do with this, you have to create your own implementation. I decided to do it on the DelegatingHandler level (also Tugberk’s idea). Here’s my implementation.
public class ExtendedHttpResponseMessageHandler : DelegatingHandler
{
protected async override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, System.Threading.CancellationToken cancellationToken)
{
var result = await base.SendAsync(request, cancellationToken);
var customMessage = result as ExtendedHttpResponseMessage;
if (customMessage == null)
return result;
var config = request.GetConfiguration();
var contentNegotiator = config.Services.GetContentNegotiator();
var connegResult = contentNegotiator.Negotiate(
customMessage.Payload.GetType(), request, config.Formatters
);
var objectContent = new ObjectContent(
customMessage.Payload.GetType(), customMessage.Payload, connegResult.Formatter);
result.Content = objectContent;
return result;
}
}
So now I can...
public HttpResponseMessage Get(string stuff)
{
if (String.IsNullOrEmpty(stuff))
{
return new ExtendedHttpResponseMessage<Error>(new Error
{ Code = 76543, Description = "Please supply stuff" },
HttpStatusCode.BadRequest);
}
var thingy = new Thingy
{
Name = "mabob"
};
return new ExtendedHttpResponseMessage<Thingy>(thingy, HttpStatusCode.Accepted);
}
I can haz easy api. So it’s not that hard to accomplish this. Web Api is extensible enough to allow it, which is good. But in my opinion this seems like a strange thing to not have out of the box.
Maybe I Should Not Unit Test The Controller
I don’t always unit test my controllers in ASP.NET MVC, maybe I shouldn’t here. Perhaps that is the case and I figured some would probably think that when they read this post but that doesn’t actually solve the problems above. If I take these problems down a level deeper into my BL, I still don’t have the ability to make a custom error response. I can avoid the whole Request object setup unit testing problem though and perhaps this is the easiest solution. But for now I’m going with what I outlined about.
Thoughts?
Updates (9/12/2012)
A nice conversation happened after this post was published and here are the things I caught that I found interesting. It might have spread a little further than I saw but here’s at least some of it. First, Amir Rajan was nice enough to respond and explain how ServiceStack does the kind of thing mentioned above.
Second, Darrel Miller provided a better implementation that doesn’t involve the dynamic handler. Here is his gist. Thanks Darrel!
Third, Glenn Block dropped a few lines. I think I’ll quote one because it’s something users of Web Api should keep in mind: "Web API did not aim to be RESTFul, it aimed to offer a good stack for HTTP." I hadn’t actually heard ASP.NET Web Api described quite that way (or missed it if I have). And, frankly, that sounds like a really good goal for a general web api framework.
And finally, a friend of mine mentioned today in an email that, I quote, "Your blog post on WebApi was harsh :)". I didn’t really mean it that way. I just don’t like this little aspect of the api. In general my thoughts have been positive but constructive criticism is ultimately much more valuable than uncritical praise.
So thanks everyone for the conversation. I enjoyed it.
Comments
Tugberk 2012-09-10 09:36:36
You can also do this:
Chris Marisic 2012-09-11 11:20:22
You should not test controllers. Controllers should be thin route drivers, if you have logic in a controller that requires actual unit testing you’re doing it wrong.
The orchestrator pattern is what you want: http://www.simple-talk.com/dotnet/asp.net/never-mind-the-controller,-here-is-the-orchestrator/
This will then allow you to directly test your actual code and not be testing plumbing. Controllers are all plumbing.
The #1 reason to follow the orchestrator pattern (past it’s semantically not correct to have login in controllers as per the pattern) is that it requires to upfront acknowledge all of your dependencies. It’s so easy to start adding redirects, cookie read/writes, header reads/writes etc in the controller that is just poison to unit testing effectively. With an orchestrator you upfront acknowledge all of your dependencies and inputs.
Controllers should be tested through integration testing. Integration testing is where plumbing validation should be done.
Henrik Frystyk Nielsen 2012-09-11 03:02:39
Two quick comments:
1) This seems like a lot of hoops to jump through to avoid creating a mocked request for unit testing. We do have a work item on making this much simpler (today you have to add some obscure properties yourself which is not great). Your input in making this as simple as possible is of course greatly appreciated.
2) You can just create HttpResponseMessage instances and fill them in as you wish. You don’t need to use the Request.Create* -- they are after all just extension methods. You don’t have to run content negotiation to do this -- it’s an optional part which is hooked in when using the Request.Create* extension methods but if you want to just use JSON then you just use the JsonFormatter directly.
Hope this helps,
Henrik
Eric Sowell 2012-09-12 10:48:53
@Chris - that article looks interesting and sometimes I feel the same...but focus on the topic at hand :)
@Henrik - 1) It’s a lot of hoops. Darrel’s approach above takes pretty much all the pain out of the way though so I think I have a hoopless solution :)
2) Yes, I see that you can do that. My only point that a solution that uses something like HttpResponseMessage gives me an easier solution that doesn’t require any extra content negotiation work other than the implementation of HttpResponseMessage.
You said this on Twitter the other day on why the feature was removed: "Two reasons: We couldn’t guarantee the type of (HRM is mutable) and couldn’t do content negotiation consistently". I confess that I don’t really follow what you mean here. Can you point me to a blog post or something that will help me out on this?
Glenn Block 2012-09-12 11:15:59
I’ll let Henrik weigh in why the strongly typed messages were removed. It was not something done lightly, but there a bunch of inconsistencies and hacks within the implementation which there was no great solution for.
As to the RESTfulness of Web API. It is true that we aimed to developer a framework that was opinionated about HTTP not about REST. REST is not for everyone, and beyond that there’s a lot of variance/ambiguity on how to build a RESTful system as it’s a set of constraints. On the other hand HTTP itself is a specification and there’s far less ambiguity. Thus sticking to HTTP is a safe bet.
We did have a deliberate goal though to enable building RESTful systems with Web API. We were intentional about our design from a REST perspective ensuring (to the best of our ability) that nothing in Web API stood in your way / that it felt natural to build a system applying the constraints.
Eric Sowell 2012-09-12 11:26:49
Thanks Glenn. In our particular project at work, for which I’m doing prototyping, we’re definitely heading down the restful route. So far Web API is working fine for us but we’re really just getting started. In some ways this more generic Web API http strategy might work best for us since there may be times when we don’t want to follow RESTful principles 100%. It would be a bummer in such a case to go through hoops to opt out of a constraint by a framework that was too confining. But you never know till you build something in a framework whether or not it’s going to work out, so it should be a fun adventure :)
Glenn Block 2012-09-12 11:42:35
"But you never know till you build something in a framework whether or not it’s going to work out, so it should be a fun adventure :)"
Indeed. Will be great to hear (read) about how the journey goes ;-)
Tugberk 2012-09-13 02:29:44
Here is the sales pitch from the official ASP.NET Web API web site
You can simplify these sentence with this one:
However, I respect your post because you tried something out in person, gave it a try and then you cared to express your opinions in a respectful manner. What is frustrating is to see other posts for the usability issues on ASP.NET Web API from the people which has no clue whatsoever about this framework.
Eric Sowell 2012-09-13 06:48:36
@Tugberk - I guess I hadn’t read that sentence. Thanks. And thanks for your various posts on web api. They have been helpful.