@manhng

Welcome to my blog!

Training WebAPI

February 21, 2022 13:16

Training WebAPI (edit)

Action Results in Web API 2 - ASP.NET 4.x | Microsoft Docs

The advantages of IHttpActionResult in Web API 2. (gisspan.com)

c# - Why should I use IHttpActionResult instead of HttpResponseMessage? - Stack Overflow

From HttpResponseMessage to IHttpActionResult (Example) (coderwall.com)

A New Way to Send Response Using IHttpActionResult (c-sharpcorner.com)

CustomIHttpActionResult

thuru-zz/CustomIHttpActionResult (github.com)

    public class Customer
    {
        public int CustomerId { get; set; }
        public string Name { get; set; }
    }

    public class CacheableHttpActionResult<T> : IHttpActionResult
    {
        private T content;
        private int cacheTimeInMin;
        private HttpRequestMessage request;

        public CacheableHttpActionResult(HttpRequestMessage request, T content, int cachetime)
        {
            this.content = content;
            this.request = request;
            this.cacheTimeInMin = cachetime;
        }

        public System.Threading.Tasks.Task<HttpResponseMessage> ExecuteAsync(System.Threading.CancellationToken cancellationToken)
        {
            var response = request.CreateResponse<T>(HttpStatusCode.OK, this.content);
            response.Headers.CacheControl = new System.Net.Http.Headers.CacheControlHeaderValue() 
            {
                MaxAge = TimeSpan.FromMinutes(this.cacheTimeInMin),
                // other customizations
            };
            
            return Task.FromResult(response);
        }
    }
    public class CustomerController : ApiController
    {
        public IHttpActionResult Get()
        {
            var customresult = new CacheableHttpActionResult<Customer>(Request, 
                new Customer() { CustomerId = 1, Name = "Thuru" }, 5);
            return customresult;
        }
    }

Web API 2 introduced the new interface IHttpActionResult to return back REST responses, where Web API 1 was using the class HttpResponseMessage to represent HTTP response, and HTTPResponseException to represent HTTP response error.

IHttpActionResult allows developers to enhance their Web API 1 code to be :

  1. More testable.
  2. More reusable.
  3. Cleaner, and more elegant.

We are going to unleash the potential of this interface and see some creative ideas on how to use it.

Start from HTTP Protocol

First of all, let’s see what Web API is trying to do, and let us look at the HTTP response. A usual responses coming from a web server look like:

HTTP/1.1 200 OK
Cache-Control: max-age=1200
Content-Length: 10
Content-Type: text/plain; charset=utf-16
Server: Microsoft-IIS/8.0
Date: Mon, 27 Jan 2014 08:53:35 GMT

hello

This is a successful response, which we can know from the code 200.
Another example of a failed response:

HTTP/1.1 404 Not Found
Content-type: text/html
Content-length: 47
  
Sorry, the object you requested was not found.

The traditional thinking of Web API 1

Web API 1 introduced HttpResponseMessage, and by looking at the class properties in the picture down, you can see it is a literal .NET representation of the HTTP response.

states

By using HttpResponseMessage we can craft any HTTP response we want. For example to create the HTTP response mentioned above:

var response = new HttpResonseMessage(HttpStatusCode.OK);
response.Content = new StringContent("hello");
response.Content.Headers.ContentType = new MediaTypeHeaderValue("text/plain");
response.Headers.CatcheControl = new CacheControlHeaderValue {MaxAge = TimeSpan.FromMinutes(2)};

One problem with the code above is that it is ignoring the request, and the request content type, and this is why most of the time we will create the response like this:

var response = request.CreateResponse(HttpStatusCode.OK, new StringContent("hello"));
response.Headers.CatcheControl = new CacheControlHeaderValue {MaxAge = TimeSpan.FromMinutes(2)};

By using request.CreateResponse, Web API will create the response with content type requested by the request.

Worth mentioning alternatives

Before we move to Web API 2, it is worth mentioned that Web API 1 provides some built-in translation from any object type to HttpResponseMessage. For example, all these are valid return types of a Controller action:

void Post()
IEnumeration<string> Get()
string Get(int id);
MyCustomClass Get(int id);

Web API will serialize the object returned and wrap it in a successful HttpResponseMessage with status code 200.
And in the case of void, the Web API return 204 No Content

Web API 2 and IHttpActionResult

To see the advantages of this interface, let us check an example in the version 1, and see its translation into version 2.

public class ProductsController : ApiController
{
    IProductRepository _repository;
    public ProductsController(IProductRepository repository)
    {
        _repository = repository;
    }
    public HttpResponseMessage Get(int id)
    {
      try {
        Product product = _repository.GetById(id);
        if (product == null)
        {
            return Request.CreateResponse(HttpStatusCode.NotFound);
        }
        var response = Request.CreateResponse(product);
        // add more code for caching and other custom response headers...
        // for example: 
        response.Headers.CatcheControl = new CacheControlHeaderValue {MaxAge = TimeSpan.FromMinutes(1)};
        return response;
      }
      catch (Exception ex) {
        return Request.CreateResponse(HttpStatusCode.InternalServerError);
      }
    }
}

The problems with previous code are:

  1. The class is responsible of calling repository and creating the HttpResponseMessage.
  2. There is no easy way to re-use code that set response header, like the caching for example.

Let us rewrite the code using IHttpActionResult:

public class ProductsController : ApiController
{
    IProductRepository _repository;
    public ProductsController(IProductRepository repository)
    {
        _repository = repository;
    }
    public HttpResponseMessage Get(int id)
    {
      try {
        Product product = _repository.GetById(id);
        if (product == null)
        {
            return NotFound();
        }
        return Ok(product);
      }
      catch (Exception ex) {
        return InternalServerError(ex);
      }
    }
}

Where Ok, NotFound, InternalServerError are ApiController methods that are returning concrete implementation of IHttpActionResult.
Web API already comes with a library of classes that implement this interface which represent the most used responses on the web like : Ok, NotFound…, and they are located in the library system.web.http.results.

The benefits of using the previous code are:

  1. Separation of concerns.
  2. Better testability.
  3. Produce reusable code.

1. Separation of concerns

The code that produce the HttpResponseMessage is separate, and we can implement our own generation of the response message, and our controller concern only on retrieving the data from the repository.

2. Better testability

Let us write test code for the previous code:

[TestMethod]
public void GetReturnsProductWithSameId()
{
    var mockRepository = new Mock<IProductRepository>();
    mockRepository.Setup(x => x.GetById(3))
        .Returns(new Product { Id = 3 });

    var controller = new ProductsController(mockRepository.Object);

    IHttpActionResult actionResult = controller.Get(3);
    var contentResult = actionResult as OkNegotiatedContentResult<Product>;

    Assert.IsNotNull(contentResult);
    Assert.IsNotNull(contentResult.Content);
    Assert.AreEqual(3, contentResult.Content.Id);
}

Obviously, testing is easier because we are testing the business logic which is retrieving data, and not generating the Http Response.

3. Produce reusable code

Let us write the code that add the cash repose header to the response.

In order to do that, I will borrow this brilliant idea of using chaining IHttpActionResult together to write reusable Http Response Headers.
Let us create a code that implement IHttpActionResult and crate a response header for cashing:
(P.S: I am borrowing his code, but just make it simpler)

public class CachedResult<T> : IHttpActionResult where T : IHttpActionResult
{
    public T InnerResult { get; private set; }
    public CachedResult(T innerResult)
    {
      InnerResult = innerResult;
    }

    public async Task<HttpResonseMessage> ExecuteAsync(CancellationToken cancellationToken)
    {
        var response = await InnerResult.ExecuteAsync(cancellationToken);
        response.Headers.CacheControl = new CacheControlHeaderValue
        {
          MaxAge = TimeSpan.FromMinutes(2)
        };
        return response;
    }
}

The above code will take an IHttpActionResult in the construction and add a caching header to it. To use it, we can do the following code:

var responseOk =  Ok(product);
var cachedResult = new CachedResult<System.Web.Http.OkResult>(responseOk);

Or we can do better, by creating an extension method to the IHttpActionResult, like this:

public static class HttpActionResultExtensions
{
  public static CachedResult<T> Cached<T>(this T actionResult) where T : IHttpActionResult
  {
    return new CachedResult<T>(actionResult);
  }
}

And now we can use it as follows:

var responseOk =  Ok(product).Cached();

The Cached extension method can be reused whenever we want to add Cache response header to the HttpResponseMessage

BradWilson’s gists (github.com)

Using chaining to create cached results in ASP.NET Web API v2 (github.com)

using System;
using System.Collections.Generic;
using System.Linq;
using System.Web.Http;

public SampleController : ApiController
{
    public IHttpActionResult GetExample(string name)
    {
        return Ok("Hello, " + name).Cached(Cacheability.Public, maxAge: TimeSpan.FromMinutes(15));
    }
}
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web.Http;

public static class HttpActionResultExtensions
{
    public static CachedResult<T> Cached<T>(
        this T actionResult,
        Cacheability cacheability = Cacheability.Private,
        string eTag = null,
        DateTimeOffset? expires = null,
        DateTimeOffset? lastModified = null,
        TimeSpan? maxAge = null,
        bool? noStore = null) where T : IHttpActionResult
    {
        return new CachedResult<T>(actionResult, cacheability, eTag, expires, lastModified, maxAge, noStore);
    }
}
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Threading;
using System.Threading.Tasks;
using System.Web.Http;

public class CachedResult<T> : IHttpActionResult
    where T : IHttpActionResult
{
    public CachedResult(
        T innerResult,
        Cacheability cacheability,
        string eTag,
        DateTimeOffset? expires,
        DateTimeOffset? lastModified,
        TimeSpan? maxAge,
        bool? noStore)
    {
        Cacheability = cacheability;
        ETag = eTag;
        Expires = expires;
        InnerResult = innerResult;
        LastModified = lastModified;
        MaxAge = maxAge;
        NoStore = noStore;
    }

    public Cacheability Cacheability { get; private set; }
    public string ETag { get; private set; }
    public DateTimeOffset? Expires { get; private set; }
    public T InnerResult { get; private set; }
    public DateTimeOffset? LastModified { get; private set; }
    public TimeSpan? MaxAge { get; private set; }
    public bool? NoStore { get; private set; }

    public async Task<HttpResponseMessage> ExecuteAsync(CancellationToken cancellationToken)
    {
        var response = await InnerResult.ExecuteAsync(cancellationToken);
        if (!response.Headers.Date.HasValue)
            response.Headers.Date = DateTimeOffset.UtcNow;

        response.Headers.CacheControl = new CacheControlHeaderValue
        {
            NoCache = Cacheability == Cacheability.NoCache,
            Private = Cacheability == Cacheability.Private,
            Public = Cacheability == Cacheability.Public
        };

        if (response.Headers.CacheControl.NoCache)
        {
            response.Headers.Pragma.TryParseAdd("no-cache");
            response.Content.Headers.Expires = response.Headers.Date;
            return response;  // None of the other headers are valid
        }

        response.Content.Headers.Expires = Expires;
        response.Content.Headers.LastModified = LastModified;
        response.Headers.CacheControl.MaxAge = MaxAge;

        if (!String.IsNullOrWhiteSpace(ETag))
            response.Headers.ETag = new EntityTagHeaderValue(String.Format("\"{0}\"", ETag));

        if (NoStore.HasValue)
            response.Headers.CacheControl.NoStore = NoStore.Value;

        return response;
    }
}
public enum Cacheability
{
    NoCache,
    Private,
    Public,
}

Categories

Recent posts