Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

WebApi 2 return types

I'm looking at the documentation of WebAPI 2, and i'm severely disappointed with the way the action results are architected. I really hope there is a better way.

So documentation says I can return these:

**void**    Return empty 204 (No Content)

**HttpResponseMessage** Convert directly to an HTTP response message.

**IHttpActionResult**   Call ExecuteAsync to create an HttpResponseMessage, then convert to an HTTP response message.

**Other type**  Write the serialized return value into the response body; return 200 (OK).

I don't see a clean way to return an array of items with custom HTTP status code, custom headers and with auto negotiated content though.

What I would like to see is something like

public HttpResult<Item> Post()
{
   var item = new Item();
   var result = new HttpResult<Item>(item, HttpStatusCode.Created);
   result.Headers.Add("header", "header value");

   return result;
}

This way I can glance over a method and immediately see whats being returned, and modify status code and headers.

The closest thing I found is NegotiatedContentResult<T>, with weird signature (why does it need an instance of controller?), but there's no way to set custom headers?

Is there a better way ?

like image 786
Evgeni Avatar asked Apr 09 '15 21:04

Evgeni


2 Answers

The following code should give you everything you want:

[ResponseType(typeof(Item))]
public IHttpActionResult Post()
{
    var item = new Item();
    HttpContext.Current.Response.AddHeader("Header-Name", "Header Value");
    return Content(HttpStatusCode.Created, item);
}

... if you really need to return an array of items ...

[ResponseType(typeof(List<Item>))]
public IHttpActionResult Post()
{
    var items = new List<Item>();
    // Do something to fill items here...
    HttpContext.Current.Response.AddHeader("Item-Count", items.Count.ToString());
    return Content(HttpStatusCode.Created, items);
}
like image 117
afrazier Avatar answered Oct 02 '22 16:10

afrazier


I don't think the designers of the web-api intended for controller methods to be fiddling with the headers. The design pattern seems to be to use DelegatingHandler, ActionFilterAttribute and the ExecuteAsync overridable method of ApiController to handle authentication and response formatting.

So perhaps your logic for message content negotiation should be handled there ?

However if you definitely need to control headers from within your controller method you can do a little set-up to make it work. To do so you can create your own DelegationHandler that forwards selected headers from your "Inner" response headers:

public class MessageHandlerBranding : DelegatingHandler {
    protected async override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
    {
        var response = await base.SendAsync(request, cancellationToken);
        //If we want to forward headers from inner content we can do this:
        if (response.Content != null && response.Content.Headers.Any())
        {
            foreach (var hdr in response.Content.Headers)
            {
                var keyUpr = hdr.Key.ToUpper(); //Response will not tolerate setting of some header values
                if ( keyUpr != "CONTENT-TYPE" && keyUpr != "CONTENT-LENGTH")
                {
                    string val = hdr.Value.Any() ? hdr.Value.FirstOrDefault() : "";
                    response.Headers.Add(hdr.Key, val);                       
                }
            }
        }
        //Add our branding header to each response
        response.Headers.Add("X-Powered-By", "My product");
        return response;
    }  
}

Then you register this handler in your web-api configuration, this is usually in the GlobalConfig.cs file.

config.MessageHandlers.Add(new MessageHandlerBranding());

You could also write your own custom class for the response object like this:

public class ApiQueryResult<T> : IHttpActionResult where T : class
{
    public ApiQueryResult(HttpRequestMessage request)
    {
        this.StatusCode = HttpStatusCode.OK; ;
        this.HeadersToAdd = new List<MyStringPair>();
        this.Request = request;
    }

    public HttpStatusCode StatusCode { get; set; }
    private List<MyStringPair> HeadersToAdd { get; set; }
    public T Content { get; set; }
    private HttpRequestMessage Request { get; set; }

    public void AddHeaders(string headerKey, string headerValue)
    {
        this.HeadersToAdd.Add(new MyStringPair(headerKey, headerValue));
    }

    public Task<HttpResponseMessage> ExecuteAsync(CancellationToken cancellationToken)
    {
        var response = this.Request.CreateResponse<T>(this.StatusCode, this.Content);
        foreach (var hdr in this.HeadersToAdd)
        {
            response.Content.Headers.Add(hdr.key, hdr.value); 
        }
        return Task.FromResult(response);
    }


    private class MyStringPair
    {
        public MyStringPair(string key, string value)
        {
            this.key = key;
            this.value = value;
        }
        public string key;
        public string value;
    }
}

And use it like this in your controller:

 [HttpGet]
    public ApiQueryResult<CustomersView> CustomersViewsRow(int id)
    {
        var ret = new ApiQueryResult<CustomersView>(this.Request);
        ret.Content = this.BLL.GetOneCustomer(id);
        ret.AddHeaders("myCustomHkey","myCustomValue");
        return ret;
    }
like image 23
Svakinn Avatar answered Oct 02 '22 16:10

Svakinn