Things you didn’t know about action return types in ASP.NET Web API

· 1029 words · 5 minutes to read

When you are developing an ASP.NET Web API application, you can chose whether you want to return a POCO from your action, which can be any type that will then be serialized, an instance of HttpResponseMessage, or, since Web API 2, an instance of IHttpActionResult.

Let’s have a look at what really happens under the hood afterwards, and discuss some of the things about the response pipeline that you might have not known before.

Understanding the role of IHttpActionInvoker 🔗

Once a suitable action is selected to handle an HTTP request, a service called IHttpActionInvoker (or, typically, its default implementation, ApiControllerActionInvoker) is responsible for producing an HttpResponseMessage out of your action.

The service is replaceable and you can provide a custom implementation yourself. The signature is very simple:

public interface IHttpActionInvoker  
{  
Task<HttpResponseMessage> InvokeActionAsync(HttpActionContext actionContext, CancellationToken cancellationToken);  
}  

In other words, the invoker gets an instance of HttpActionContext and is expected to produce an HttpResponseMessage out of it. This is done by invoking an ExecuteAsync method on the HttpActionDescriptor present on the HttpActionContext. The result of this operation is the object returned by your action (a method on a controller).

Upon obtaining the object returned by your action, the service can do 3 things:

  • if you return a POCO, run one of the built-in IActionResultConverter. This effectively means that it will call Request.CreateResponse and run content negotiation on the response, and create an instance of HttpResponseMessage with an ObjectContent. If the return is void, an empty HttpResponseMessage, with status code 204 is created instead.

  • if you return IHttpActionResult, it will execute it. This ends up producing an instance of HttpResponseMessage too.

  • if you have already produced an HttpResponseMessage yourself in the action, the service will do nothing, as there is no need to construct a new instance of the response anymore

Mixing different action return types 🔗

It is a fairly common question - how can I mix, say, POCO return types with HttpResponseMessage, in the same action?

The most common way you’d see being suggested in different Web API articles and publications is to use HttpResponseException. For example, consider the following action:

public Item GetById(int id)  
{  
if (id <= 0) { throw new HttpResponseException(new HttpResponseMessage(HttpStatusCode.BadRequest)); } Item item = _itemRepo.FindById(id); if (item == null) { throw new HttpResponseException(new HttpResponseMessage(HttpStatusCode.NotFound); } return item; }

The special handling of exceptions of HttpResponseException type allow you to count on the framework to use the instance of HttpResponseMessage supplied in the exception’s constructor.

Of course, some would argue that using exceptions to control the flow of your application code is not the best practice. Turns out, the same logical result can be achieved by tweaking the action accordingly:

public object GetById(int id)  
{  
if (id <= 0) { return Request.CreateResponse(HttpStatusCode.BadRequest); } Item item = _itemRepo.FindById(id); if (item == null) { return Request.CreateResponse(HttpStatusCode.BadRequest); } return item; }

Using object as return type is obviously going to allow you to return anything from the action (the code will compile). Now, the reason why this is going to work in the Web API pipeline, is that ApiControllerActionInvoker is always inspecting the return object at runtime anyway, and will pick the correct behavior regardless of whether your code path returned a POCO, HttpResponseMessage or IHttpActionResult.

Find out the runtime return type of an action 🔗

Suppose you are in an action filter, and want to perform some activity after the action has executed based on the runtime return type of the action.

Here is an example:

public class Person {}

public class VipPerson : Person {}

public class PersonController  
{  
public Person GetById(int id)  
{  
//imagine that the object from repo  
//can be of type Person or VipPerson  
return _personRepo.FindById(id);  
}  
}  

Now, imagine a filter:

public class PersonDecoratorAttribute : ActionFilterAttribute  
{  
public override void OnActionExecuted(HttpActionExecutedContext actionExecutedContext)  
{  
//do something only when you deal with VipPerson  
Type realReturnType = ??  
}  
}  

Normally, you can reach into the HttpActionDescriptor, which stores the return type information.

Type realReturnType = actionExecutedContext.ActionContext.ActionDescriptor.ReturnType;  

However, this is obtained statically, from the MethodInfo of the action, not at runtime!

Which means, if you apply it to our example, even if you return an instance of VipPerson, the action descriptor ReturnType property will tell you its Person. Also, by that time (inside a filter), the real instance of an object returned from the action is no longer available, as its already been converted into the HttpResponseMessage.

It would be elegant to implement a custom IHttpActionInvoker which will store the real runtime type of the response object in the request properties disctionary (so that it’s available for later use anywhere).

This is shown below. Notice that as soon as the result of the action is obtained, we store the runtime type in the request Properties, under RuntimeReturnType key. The rest of the code is simply mimicking what ApiControllerActionInvoker does - the code has to be internalized, since its not overrideable in any way).

public class SmartHttpActionInvoker : IHttpActionInvoker  
{  
public Task<HttpResponseMessage> InvokeActionAsync(HttpActionContext actionContext, CancellationToken cancellationToken)  
{  
return InvokeActionInternal(actionContext, cancellationToken);  
}

private static async Task<HttpResponseMessage> InvokeActionInternal(HttpActionContext actionContext, CancellationToken cancellationToken)  
{  
if (actionContext.ActionDescriptor == null)  
{  
throw new ArgumentNullException("actionContext.ActionDescriptor");  
}

if (actionContext.ControllerContext == null)  
{  
throw new ArgumentNullException("actionContext.ControllerContext");  
}

try  
{  
var result = await actionContext.ActionDescriptor.ExecuteAsync(actionContext.ControllerContext, actionContext.ActionArguments, cancellationToken);

if (result != null)  
actionContext.Request.Properties["RuntimeReturnType"] = result.GetType();

var isActionResult = typeof(IHttpActionResult).IsAssignableFrom(actionContext.ActionDescriptor.ReturnType);

if (result == null && isActionResult)  
{  
throw new InvalidOperationException();  
}

if (isActionResult || actionContext.ActionDescriptor.ReturnType == typeof(object))  
{  
var actionResult = result as IHttpActionResult;

if (actionResult == null && isActionResult)  
{  
throw new InvalidOperationException();  
}

if (actionResult == null)  
return actionContext.ActionDescriptor.ResultConverter.Convert(actionContext.ControllerContext, result);

var response = await actionResult.ExecuteAsync(cancellationToken);  
if (response == null)  
{  
throw new InvalidOperationException();  
}

if (response.RequestMessage == null)  
{  
response.RequestMessage = actionContext.Request;  
}

return response;  
}

return actionContext.ActionDescriptor.ResultConverter.Convert(actionContext.ControllerContext, result);  
}  
catch (HttpResponseException httpResponseException)  
{  
var response = httpResponseException.Response;  
if (response.RequestMessage == null)  
{  
response.RequestMessage = actionContext.Request;  
}

return response;  
}  
}  
}  

Only thing left is to register the invoker against your HttpConfiguration:

config.Services.Replace(typeof(IHttpActionInvoker), new SmartHttpActionInvoker());  

You can now know exactly what the action has returned - in any place where you have access to the HttpRequestMessage (so de facto anywhere in the Web API pipeline) - by just checking the RuntimeReturnType key of the request’s properties.

About


Hi! I'm Filip W., a cloud architect from Zürich 🇨🇭. I like Toronto Maple Leafs 🇨🇦, Rancid and quantum computing. Oh, and I love the Lowlands 🏴󠁧󠁢󠁳󠁣󠁴󠁿.

You can find me on Github and on Mastodon.

My Introduction to Quantum Computing with Q# and QDK book
Microsoft MVP