Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to use ETag in Web API using action filter along with HttpResponseMessage

I have a ASP.Net Web API controller which simply returns the list of users.

public sealed class UserController : ApiController
{
    [EnableTag]
    public HttpResponseMessage Get()
    {
        var userList= this.RetrieveUserList(); // This will return list of users
        this.responseMessage = new HttpResponseMessage(HttpStatusCode.OK)
        {
            Content = new ObjectContent<List<UserViewModel>>(userList, new  JsonMediaTypeFormatter())
        };
        return this.responseMessage;
       }
}

and an action filter attribute class EnableTag which is responsible to manage ETag and cache:

public class EnableTag : System.Web.Http.Filters.ActionFilterAttribute
{
    private static ConcurrentDictionary<string, EntityTagHeaderValue> etags = new ConcurrentDictionary<string, EntityTagHeaderValue>();

    public override void OnActionExecuting(HttpActionContext context)
    {
        if (context != null)
        {
            var request = context.Request;
            if (request.Method == HttpMethod.Get)
            {
                var key = GetKey(request);
                ICollection<EntityTagHeaderValue> etagsFromClient = request.Headers.IfNoneMatch;

                if (etagsFromClient.Count > 0)
                {
                    EntityTagHeaderValue etag = null;
                    if (etags.TryGetValue(key, out etag) && etagsFromClient.Any(t => t.Tag == etag.Tag))
                    {
                        context.Response = new HttpResponseMessage(HttpStatusCode.NotModified);
                        SetCacheControl(context.Response);
                    }
                }
            }
        }
    }

    public override void OnActionExecuted(HttpActionExecutedContext context)
    {
        var request = context.Request;
        var key = GetKey(request);

        EntityTagHeaderValue etag;
        if (!etags.TryGetValue(key, out etag) || request.Method == HttpMethod.Put || request.Method == HttpMethod.Post)
        {
            etag = new EntityTagHeaderValue("\"" + Guid.NewGuid().ToString() + "\"");
            etags.AddOrUpdate(key, etag, (k, val) => etag);
        }

        context.Response.Headers.ETag = etag;
        SetCacheControl(context.Response);
    }

    private static void SetCacheControl(HttpResponseMessage response)
    {
        response.Headers.CacheControl = new CacheControlHeaderValue()
        {
            MaxAge = TimeSpan.FromSeconds(60),
            MustRevalidate = true,
            Private = true
        };
    }

    private static string GetKey(HttpRequestMessage request)
    {
        return request.RequestUri.ToString();
    }
}

The above code create an attribute class to manage ETag. So on the first request, it will create a new E-Tag and for the subsequent request it will check whether any ETag is existed. If so, it will generate Not Modified HTTP Status and return back to client.

My problem is, I want to create a new ETag if there are changes in my user list, ex. a new user is added, or an existing user is deleted. and append it with the response. This can be tracked by the userList variable.

Currently, the ETag received from client and server are same from every second request, so in this case it will always generate Not Modified status, while I want it when actually nothing changed.

Can anyone guide me in this direction?

like image 536
SoftSan Avatar asked Nov 22 '13 12:11

SoftSan


People also ask

What is ETag in Web API?

An entity tag, or ETag, is a mechanism that is provided by the HTTP protocol so that a browser client or a script can make conditional REST requests for optimistic updating or optimized retrieval of entities.

How do I pass body parameters in Web API?

Use [FromUri] attribute to force Web API to get the value of complex type from the query string and [FromBody] attribute to get the value of primitive type from the request body, opposite to the default rules.

What is FromBody and FromUri in Web API?

The [FromUri] attribute is prefixed to the parameter to specify that the value should be read from the URI of the request, and the [FromBody] attribute is used to specify that the value should be read from the body of the request.


1 Answers

My requirement was to cache my web api JSON responses... And all the solutions provided don't have an easy "link" to where the data is generated - ie in the Controller...

So my solution was to create a wrapper "CacheableJsonResult" which generated a Response, and then added the ETag to the header. This allows a etag to be passed in when the controller method is generated and wants to return the content...

public class CacheableJsonResult<T> : JsonResult<T>
{
    private readonly string _eTag;
    private const int MaxAge = 10;  //10 seconds between requests so it doesn't even check the eTag!

    public CacheableJsonResult(T content, JsonSerializerSettings serializerSettings, Encoding encoding, HttpRequestMessage request, string eTag)
        :base(content, serializerSettings, encoding, request)
    {
        _eTag = eTag;
    }

    public override Task<HttpResponseMessage> ExecuteAsync(System.Threading.CancellationToken cancellationToken)
    {
        Task<HttpResponseMessage> response = base.ExecuteAsync(cancellationToken);

        return response.ContinueWith<HttpResponseMessage>((prior) =>
        {
            HttpResponseMessage message = prior.Result;

            message.Headers.ETag = new EntityTagHeaderValue(String.Format("\"{0}\"", _eTag));
            message.Headers.CacheControl = new CacheControlHeaderValue
            {
                Public = true,
                MaxAge = TimeSpan.FromSeconds(MaxAge)
            };

            return message;
        }, cancellationToken);
    }
}

And then, in your controller - return this object:

[HttpGet]
[Route("results/{runId}")]
public async Task<IHttpActionResult> GetRunResults(int runId)
{               
    //Is the current cache key in our cache?
    //Yes - return 304
    //No - get data - and update CacheKeys
    string tag = GetETag(Request);
    string cacheTag = GetCacheTag("GetRunResults");  //you need to implement this map - or use Redis if multiple web servers

    if (tag == cacheTag )
            return new StatusCodeResult(HttpStatusCode.NotModified, Request);

    //Build data, and update Cache...
    string newTag = "123";    //however you define this - I have a DB auto-inc ID on my messages

    //Call our new CacheableJsonResult - and assign the new cache tag
    return new CacheableJsonResult<WebsiteRunResults>(results, GlobalConfiguration.Configuration.Formatters.JsonFormatter.SerializerSettings, System.Text.UTF8Encoding.Default, Request, newTag);

    }
}

private static string GetETag(HttpRequestMessage request)
{
    IEnumerable<string> values = null;
    if (request.Headers.TryGetValues("If-None-Match", out values))
        return new EntityTagHeaderValue(values.FirstOrDefault()).Tag;

    return null;
}

You need to define how granular to make your tags; my data is user-specific, so I include the UserId in the CacheKey (etag)

like image 130
James Joyce Avatar answered Oct 09 '22 13:10

James Joyce