Caching WebAPI 2 - c#

EDIT: For each request, a new instance of controller is created. However, this is not true with Attribute classes. Once they are created, it is used for multiple requests. I hope it helps.
I wrote my own WebAPI (using latest version of WebAPI and .net framework) caching action filter. I am aware about CacheCow & this. However, i wanted mine anyways.
However, there is some issue with my code because i don't get exepected output when i use it in my project on live server. On local machine everything works fine.
I used below code in my blog RSS generator and i cache the data for each category. There are around 5 categories (food, tech, personal etc).
Issue: When i navigate to say api/GetTech it returns me the rss feed items from personal blog category. When i navigate to say api/GetPersonal , it returns me api/Food
I am not able to find the root cause but I think this is due to use of static method/variable. I have double checked that my _cachekey has unique value for each category of my blog.
Can someone point out any issues with this code esp when we have say 300 requests per minute ?
public class WebApiOutputCacheAttribute : ActionFilterAttribute
{
// Cache timespan
private readonly int _timespan;
// cache key
private string _cachekey;
// cache repository
private static readonly MemoryCache _webApiCache = MemoryCache.Default;
/// <summary>
/// Initializes a new instance of the <see cref="WebApiOutputCacheAttribute"/> class.
/// </summary>
/// <param name="timespan">The timespan in seconds.</param>
public WebApiOutputCacheAttribute(int timespan)
{
_timespan = timespan;
}
public override void OnActionExecuting(HttpActionContext ac)
{
if (ac != null)
{
_cachekey = ac.Request.RequestUri.PathAndQuery.ToUpperInvariant();
if (!_webApiCache.Contains(_cachekey)) return;
var val = (string)_webApiCache.Get(_cachekey);
if (val == null) return;
ac.Response = ac.Request.CreateResponse();
ac.Response.Content = new StringContent(val);
var contenttype = (MediaTypeHeaderValue)_webApiCache.Get("response-ct") ?? new MediaTypeHeaderValue("application/rss+xml");
ac.Response.Content.Headers.ContentType = contenttype;
}
else
{
throw new ArgumentNullException("ac");
}
}
public override void OnActionExecuted(HttpActionExecutedContext actionExecutedContext)
{
if (_webApiCache.Contains(_cachekey)) return;
var body = actionExecutedContext.Response.Content.ReadAsStringAsync().Result;
if (actionExecutedContext.Response.StatusCode == HttpStatusCode.OK)
{
lock (WebApiCache)
{
_wbApiCache.Add(_cachekey, body, DateTime.Now.AddSeconds(_timespan));
_webApiCache.Add("response-ct", actionExecutedContext.Response.Content.Headers.ContentType, DateTimeOffset.UtcNow.AddSeconds(_timespan));
}
}
}
}

The same WebApiOutputCacheAttribute instance can be used to cache multiple simultaneous requests, so you should not store cache keys on the instance of the attribute. Instead, regenerate the cache key during each request / method override. The following attribute works to cache HTTP GET requests.
using System;
using System.Linq;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Web.Http.Controllers;
using System.Web.Http.Filters;
using Newtonsoft.Json;
// based on strathweb implementation
// http://www.strathweb.com/2012/05/output-caching-in-asp-net-web-api/
public class CacheHttpGetAttribute : ActionFilterAttribute
{
public int Duration { get; set; }
public ILogExceptions ExceptionLogger { get; set; }
public IProvideCache CacheProvider { get; set; }
private bool IsCacheable(HttpRequestMessage request)
{
if (Duration < 1)
throw new InvalidOperationException("Duration must be greater than zero.");
// only cache for GET requests
return request.Method == HttpMethod.Get;
}
private CacheControlHeaderValue SetClientCache()
{
var cachecontrol = new CacheControlHeaderValue
{
MaxAge = TimeSpan.FromSeconds(Duration),
MustRevalidate = true,
};
return cachecontrol;
}
private static string GetServerCacheKey(HttpRequestMessage request)
{
var acceptHeaders = request.Headers.Accept;
var acceptHeader = acceptHeaders.Any() ? acceptHeaders.First().ToString() : "*/*";
return string.Join(":", new[]
{
request.RequestUri.AbsoluteUri,
acceptHeader,
});
}
private static string GetClientCacheKey(string serverCacheKey)
{
return string.Join(":", new[]
{
serverCacheKey,
"response-content-type",
});
}
public override void OnActionExecuting(HttpActionContext actionContext)
{
if (actionContext == null) throw new ArgumentNullException("actionContext");
var request = actionContext.Request;
if (!IsCacheable(request)) return;
try
{
// do NOT store cache keys on this attribute because the same instance
// can be reused for multiple requests
var serverCacheKey = GetServerCacheKey(request);
var clientCacheKey = GetClientCacheKey(serverCacheKey);
if (CacheProvider.Contains(serverCacheKey))
{
var serverValue = CacheProvider.Get(serverCacheKey);
var clientValue = CacheProvider.Get(clientCacheKey);
if (serverValue == null) return;
var contentType = clientValue != null
? JsonConvert.DeserializeObject<MediaTypeHeaderValue>(clientValue.ToString())
: new MediaTypeHeaderValue(serverCacheKey.Substring(serverCacheKey.LastIndexOf(':') + 1));
actionContext.Response = actionContext.Request.CreateResponse();
// do not try to create a string content if the value is binary
actionContext.Response.Content = serverValue is byte[]
? new ByteArrayContent((byte[])serverValue)
: new StringContent(serverValue.ToString());
actionContext.Response.Content.Headers.ContentType = contentType;
actionContext.Response.Headers.CacheControl = SetClientCache();
}
}
catch (Exception ex)
{
ExceptionLogger.Log(ex);
}
}
public override void OnActionExecuted(HttpActionExecutedContext actionExecutedContext)
{
try
{
var request = actionExecutedContext.Request;
// do NOT store cache keys on this attribute because the same instance
// can be reused for multiple requests
var serverCacheKey = GetServerCacheKey(request);
var clientCacheKey = GetClientCacheKey(serverCacheKey);
if (!CacheProvider.Contains(serverCacheKey))
{
var contentType = actionExecutedContext.Response.Content.Headers.ContentType;
object serverValue;
if (contentType.MediaType.StartsWith("image/"))
serverValue = actionExecutedContext.Response.Content.ReadAsByteArrayAsync().Result;
else
serverValue = actionExecutedContext.Response.Content.ReadAsStringAsync().Result;
var clientValue = JsonConvert.SerializeObject(
new
{
contentType.MediaType,
contentType.CharSet,
});
CacheProvider.Add(serverCacheKey, serverValue, new TimeSpan(0, 0, Duration));
CacheProvider.Add(clientCacheKey, clientValue, new TimeSpan(0, 0, Duration));
}
if (IsCacheable(actionExecutedContext.Request))
actionExecutedContext.ActionContext.Response.Headers.CacheControl = SetClientCache();
}
catch (Exception ex)
{
ExceptionLogger.Log(ex);
}
}
}
Just replace the CacheProvider with your MemoryCache.Default. In fact, the code above uses the same by default during development, and uses azure cache when deployed to a live server.
Even though your code resets the _cachekey instance field during each request, these attributes are not like controllers where a new one is created for each request. Instead, the attribute instance can be repurposed to service multiple simultaneous requests. So don't use an instance field to store it, regenerate it based on the request each and every time you need it.

Related

Creating AWS initialization class

I have a API which connects to my dynamo db. My API has quite a few endpoints for GET, POST, Delete etc. I am using the following code:
var awsCredentials = Helper.AwsCredentials(id, password);
var awsdbClient = Helper.DbClient(awsCredentials, "us-east-2");
var awsContext = Helper.DynamoDbContext(awsdbClient);
List<ScanCondition> conditions = new List<ScanCondition>();
var response = await context.ScanAsync<MyData>(conditions).GetRemainingAsync();
return response.ToList();
The first three lines of my code ie setting awsCredentials, awsdbClient & awsContext are repeated in each of my WEB API call.
And this is my static helper class:
public static class Helper
{
public static BasicAWSCredentials AwsCredentials(string id, string password)
{
var credentials = new BasicAWSCredentials(id, password);
return credentials;
}
public static AmazonDynamoDBClient DynamoDbClient(BasicAWSCredentials credentials, RegionEndpoint region)
{
var client = new DBClient(credentials, region);
return client;
}
public static DynamoDBContext DynamoDbContext(AmazonDynamoDBClient client)
{
var context = new DynamoDBContext(client);
return context;
}
}
I use this helper class in my API to initialize AWS.
Is there a better way to initialize this?
Let's take advantage of ASP.Net's built-in Dependency Injection.
We need to make a quick interface to expose the values you need.
public interface IDynamoDbClientAccessor
{
DynamoDBContext GetContext();
}
And a settings class that we'll use in a bit.
public class DynamoDbClientAccessorSettings
{
public string Id { get; set; }
public string Password { get; set; }
public string Region { get; set; }
}
Now the concrete class.
public class DynamoDbClientAccessor : IDynamoDbClientAccessor
{
private readonly DynamoDbClientAccessorSettings settings;
public DynamoDbClientAccessor(IOptions<DynamoDbClientAccessorSettings> options)
{
settings = options?.Value ?? throw new ArgumentNullException(nameof(options));
}
public DynamoDBContext GetContext()
{
// You have the option to alter this if you don't
// want to create a new context each time.
// Have a private variable at the top of this class
// of type DynamoDBContext. If that variable is not null,
// return the value. If it is null, create a new value,
// set the variable, and return it.
var awsCredentials = Helper.AwsCredentials(settings.Id, settings.Password);
var awsdbClient = Helper.DbClient(awsCredentials, settings.Region);
var awsContext = Helper.DynamoDbContext(awsdbClient);
return awsContext;
}
}
Hook all of this up in your Startup class
services.AddSingleton<IDynamoDbClientAccessor, DynamoDbClientAccessor>();
services.Configure<DynamoDbClientAccessorSettings>(c =>
{
c.Id = "YOUR ID";
c.Password = "YOUR PASSWORD";
c.Region = "YOUR REGION";
});
Now in your controller or other DI service you ask for a IDynamoDbClientAccessor instance in the constructor.
Once you get more familar with Dependency Injection you'll be able to break apart more things into their own dependent services. As Daniel says, the AWS SDK even provides some interfaces for you to use which can help as well.

Response Cookie not updating after first time being set

I have a cookie helper class that gets and sets data for a cookie.
In my controller action I'm trying to update a List collection and persist that to the cookie.
UPDATE: It seems using HttpContext.Current.Response.Cookies.Add() even if the cookie exists or not it will do an upsert on it and work correctly.
So what's the purpose of Reponse.Cookie.Set() then?
private List<int> _TestNumbers = new List<int>();
cookie = new CookieHelper(_searchCookieName);
cookie.SetData("testNumbers", _TestNumbers);
_TestNumbers.Add(1);
cookie.SetData("testNumbers", _TestNumbers);
_TestNumbers.Add(2);
cookie.SetData("testNumbers", _TestNumbers);
_TestNumbers.Add(3);
cookie.SetData("testNumbers", _TestNumbers);
The cookie helper class
public class CookieHelper
{
public CookieHelper(string cookieName = null, HttpContext context = null)
{
// Set param defaults
context = context ?? HttpContext.Current;
if (cookieName != null)
_cookieName = cookieName;
// Load cookie if it exists, if not create one.
_cookie = context.Request.Cookies[_cookieName] ?? new HttpCookie(_cookieName);
Save();
}
public object GetData(string name)
{
return _cookie[name] == null ? null : new Base64Serializer().Deserialize(_cookie[name]);
}
public void SetData(string name, object value)
{
_cookie[name] = new Base64Serializer().Serialize(value);
Save();
}
public void Save()
{
_cookie.Expires = DateTime.UtcNow.AddDays(_cookieExpiration);
// Create the cookie if it doesn't exist
if(HttpContext.Current.Request.Cookies.Get(_cookieName) == null)
HttpContext.Current.Response.Cookies.Add(_cookie);
else
HttpContext.Current.Response.Cookies.Set(_cookie);
}
}

WCF: How to cache collections from OData in client

Is there a possibility to cache a collection, retrieved using WCF from an OData service.
The situation is the following:
I generated a WCF service client with Visual Studio 2015 using the metadata of the odata service. VS generated a class inheriting from System.Data.Services.Client.DataServiceContext. This class has some properties of type System.Data.Services.Client.DataServiceQuery<T>. The data of some of these properties change seldom. Because of performance reasons I want the WCF client to load these properties just the first time and not every time I use it in the code.
Is there a built in possibility to cache the data of these properties? Or can I tell the service client not to load specific proeprties newly every time.
Assuming the service client class is ODataClient and one of its properties is `Area, for now I get the values in the following way:
var client = new ODataClient("url_to_the_service");
client.IgnoreMissingProperties = true;
var propertyInfo = client.GetType().GetProperty("Area");
var area = propertyInfo.GetValue(client) as IEnumerable<object>;
The reason why I do this in such a complicated way is, that the client should be very generic: The properties to be handled can be configured in a configuration file.
* EDIT *
I already tried to find properties in the System.Data.Services.Client.DataServiceContext class or the System.Data.Services.Client.DataServiceQuery<T> class for the caching. But i wasn't able to find any.
To my knowledge there is no "out of the box" caching concept on the client. There are options for caching the output of a request on the server which is something you might want consider as well. Googling "WCF Caching" would get you a bunch of info on this.
Regarding client side caching...#Evk is correct it is pretty straight forward. Here is an sample using MemoryCache.
using System;
using System.Runtime.Caching;
namespace Services.Util
{
public class CacheWrapper : ICacheWrapper
{
ObjectCache _cache = MemoryCache.Default;
public void ClearCache()
{
MemoryCache.Default.Dispose();
_cache = MemoryCache.Default;
}
public T GetFromCache<T>(string key, Func<T> missedCacheCall)
{
return GetFromCache<T>(key, missedCacheCall, TimeSpan.FromMinutes(5));
}
public T GetFromCache<T>(string key, Func<T> missedCacheCall, TimeSpan timeToLive)
{
var result = _cache.Get(key);
if (result == null)
{
result = missedCacheCall();
if (result != null)
{
_cache.Set(key, result, new CacheItemPolicy { AbsoluteExpiration = DateTimeOffset.Now.Add(timeToLive) });
}
}
return (T)result;
}
public void InvalidateCache(string key)
{
_cache.Remove(key);
}
}
}
This is an example of code that uses the cache...
private class DataAccessTestStub
{
public const string DateTimeTicksCacheKey = "GetDateTimeTicks";
ICacheWrapper _cache;
public DataAccessTestStub(ICacheWrapper cache)
{
_cache = cache;
}
public string GetDateTimeTicks()
{
return _cache.GetFromCache(DateTimeTicksCacheKey, () =>
{
var result = DateTime.Now.Ticks.ToString();
Thread.Sleep(100); // Create some delay
return result;
});
}
public string GetDateTimeTicks(TimeSpan timeToLive)
{
return _cache.GetFromCache(DateTimeTicksCacheKey, () =>
{
var result = DateTime.Now.Ticks.ToString();
Thread.Sleep(500); // Create some delay
return result;
}, timeToLive);
}
public void ClearDateTimeTicks()
{
_cache.InvalidateCache(DateTimeTicksCacheKey);
}
public void ClearCache()
{
_cache.ClearCache();
}
}
And some tests if you fancy...
[TestClass]
public class CacheWrapperTest
{
private DataAccessTestStub _dataAccessTestClass;
[TestInitialize]
public void Init()
{
_dataAccessTestClass = new DataAccessTestStub(new CacheWrapper());
}
[TestMethod]
public void GetFromCache_ShouldExecuteCacheMissCall()
{
var original = _dataAccessTestClass.GetDateTimeTicks();
Assert.IsNotNull(original);
}
[TestMethod]
public void GetFromCache_ShouldReturnCachedVersion()
{
var copy1 = _dataAccessTestClass.GetDateTimeTicks();
var copy2 = _dataAccessTestClass.GetDateTimeTicks();
Assert.AreEqual(copy1, copy2);
}
[TestMethod]
public void GetFromCache_ShouldRespectTimeToLive()
{
_dataAccessTestClass.ClearDateTimeTicks();
var copy1 = _dataAccessTestClass.GetDateTimeTicks(TimeSpan.FromSeconds(2));
var copy2 = _dataAccessTestClass.GetDateTimeTicks();
Assert.AreEqual(copy1, copy2);
Thread.Sleep(3000);
var copy3 = _dataAccessTestClass.GetDateTimeTicks();
Assert.AreNotEqual(copy1, copy3);
}
[TestMethod]
public void InvalidateCache_ShouldClearCachedVersion()
{
var original = _dataAccessTestClass.GetDateTimeTicks();
_dataAccessTestClass.ClearDateTimeTicks();
var updatedVersion = _dataAccessTestClass.GetDateTimeTicks();
Assert.AreNotEqual(original, updatedVersion);
}
}

Serialization of nested Action<T> for service bus Queue

The general consensus from reading other posts is that serializing anonymous delegates and actions etc is a bad idea and brittle etc. The lifetime of these serialized actions will be very short lived but i'm open to better ideas about how to accomplish what i'm doing.
I have a web role and a worker role. The user can upload massive amounts of data that I need to split down and send to external wcf services. I need to queue this data and I just wanted a generic interface. I'd like to be able to deserialize it and call Execute() on it without having custom logic in the worker role.
I have 4 external services (may grow) each with multiple calls and there own unique parameters. I wanted a method of constructing the parameters in the web role, declaring which serviceInvoker class and interface I want to construct (generates a ChannelFactory with client credentials etc), and executing the child method with said parameters.
// Code
var queueMessage = new WorkOutMessage<IService1>(User.agent.Client.Id,
new Action<ServiceInvoker<IService1>>(serviceInvoker =>
serviceInvoker.InvokeService(proxy =>
{
using (new OperationContextScope((IContextChannel)proxy))
{
OperationContext.Current.OutgoingMessageHeaders.ReplyTo = new EndpointAddress("https://someAddress.com/");
OperationContext.Current.OutgoingMessageHeaders.MessageId = new System.Xml.UniqueId(head.CorrelationID);
proxy.SomeChildFunction(envelope);
}
})));
var stream = new MemoryStream();
var formatter = new BinaryFormatter();
formatter.Serialize(stream, queueMessage;
var brokeredMessage = new BrokeredMessage(stream) { CorrelationId = correlationId };
// Formatter.Serialize throws an exception
Type '<MyController>+<>c__DisplayClassc' in Assembly '<MyDll>, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null' is not marked as serializable.
// Classes
public interface IQueueProxyMessage
{
int Attempts();
bool Execute();
}
[Serializable]
public class WorkOutMessage<T> : IQueueProxyMessage where T : class
{
public Guid clientId { get; set; }
public Action<ServiceInvoker<T>> action { get; set; }
public int attempts { get; set; }
public WorkOutMessage(Guid clientId, Action<ServiceInvoker<T>> action)
{
this.clientId = clientId;
this.action = action;
this.attempts = 0;
}
public int Attempts()
{
return this.attempts;
}
public bool Execute()
{
this.attempts++;
try
{
var config = InfrastructureConfiguration.Instance;
var storage = new AzureStorageService();
var db = new DbContext(config.azure.dbConnectionString,
DbValidationMode.Enabled,
DbLazyLoadingMode.Disabled,
DbAutoDetectMode.Enabled);
var client = db.Client.Include(x => x.Organisation)
.Include(x => x.Certificate)
.Where(x => x.Active
&& x.Organisation.Active
&& x.Certificate != null
&& x.Id == this.clientId)
.FirstOrDefault();
if(client != null)
{
X509Certificate2 clientCert;
string clientCertContentType;
byte[] clientCertContent;
if (storage.GetBlob(AzureBlobType.Certificate, client.Certificate.StorageId.ToString(), out clientCertContentType, out clientCertContent))
{
var base64EncodedBytes = Encoding.Unicode.GetString(clientCertContent);
clientCert = new X509Certificate2(Convert.FromBase64String(base64EncodedBytes), "test", X509KeyStorageFlags.PersistKeySet);
if(clientCert != null)
{
var serviceInvoker = new ServiceInvoker<T>(client.Id, "test", clientCert, config.publicCertificate);
if(serviceInvoker != null)
{
this.action.Invoke(serviceInvoker);
}
// error
}
// error
}
// error
}
else
{
}
return true;
}
catch (Exception ex)
{
return false;
}
}
Obviously my serialization knowledge is pretty non-existant. which is why I thought this would work in the first place. Is there any way to re-architect this so I can still specify what serviceInvoker to use, what method and pass the parameters?
Edit: the code where I try to use BinaryFormatter and Exception
Edit2: This is a bad idea, i'm going to pass the data and reconstruct the class on the worker.

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?
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)
a good solution for ETag and in ASP.NET Web API is to use CacheCow . A good article is here.
It's easy to use and you don't have to create a custom Attribute.
Have fun
.u
I found CacheCow very bloated for what it does, if the only reason is, to lower the amount of data transfered, you might want to use something like this:
public class EntityTagContentHashAttribute : ActionFilterAttribute
{
private IEnumerable<string> _receivedEntityTags;
private readonly HttpMethod[] _supportedRequestMethods = {
HttpMethod.Get,
HttpMethod.Head
};
public override void OnActionExecuting(HttpActionContext context) {
if (!_supportedRequestMethods.Contains(context.Request.Method))
throw new HttpResponseException(context.Request.CreateErrorResponse(HttpStatusCode.PreconditionFailed,
"This request method is not supported in combination with ETag."));
var conditions = context.Request.Headers.IfNoneMatch;
if (conditions != null) {
_receivedEntityTags = conditions.Select(t => t.Tag.Trim('"'));
}
}
public override void OnActionExecuted(HttpActionExecutedContext context)
{
var objectContent = context.Response.Content as ObjectContent;
if (objectContent == null) return;
var computedEntityTag = ComputeHash(objectContent.Value);
if (_receivedEntityTags.Contains(computedEntityTag))
{
context.Response.StatusCode = HttpStatusCode.NotModified;
context.Response.Content = null;
}
context.Response.Headers.ETag = new EntityTagHeaderValue("\"" + computedEntityTag + "\"", true);
}
private static string ComputeHash(object instance) {
var cryptoServiceProvider = new MD5CryptoServiceProvider();
var serializer = new DataContractSerializer(instance.GetType());
using (var memoryStream = new MemoryStream())
{
serializer.WriteObject(memoryStream, instance);
cryptoServiceProvider.ComputeHash(memoryStream.ToArray());
return String.Join("", cryptoServiceProvider.Hash.Select(c => c.ToString("x2")));
}
}
}
No need for setting up anything, set and forget. The way i like it. :)
I like the answer which was provided by #Viezevingertjes. It is the most elegant and "No need for setting up anything" approach is very convenient. I like it too :)
However I think it has a few drawbacks:
The whole OnActionExecuting() method and storing ETags in _receivedEntityTags is unnecessary because the Request is available inside the OnActionExecuted method as well.
Only works with ObjectContent response types.
Extra work load because of the serialization.
Also it was not part of the question and nobody mentioned it. But ETag should be used for Cache validation. Therefore it should be used with Cache-Control header so clients don't even have to call the server until the cache expires (it can be very short period of time depends on your resource). When the cache expired then client makes a request with ETag and validate it. For more details about caching see this article.
So that's why I decided to pimp it up a little but. Simplified filter no need for OnActionExecuting method, works with Any response types, no Serialization. And most importantly adds CacheControl header as well. It can be improved e.g. with Public cache enabled, etc... However I strongly advise you to understand caching and modify it carefully. If you use HTTPS and the endpoints are secured then this setup should be fine.
/// <summary>
/// Enables HTTP Response CacheControl management with ETag values.
/// </summary>
public class ClientCacheWithEtagAttribute : ActionFilterAttribute
{
private readonly TimeSpan _clientCache;
private readonly HttpMethod[] _supportedRequestMethods = {
HttpMethod.Get,
HttpMethod.Head
};
/// <summary>
/// Default constructor
/// </summary>
/// <param name="clientCacheInSeconds">Indicates for how long the client should cache the response. The value is in seconds</param>
public ClientCacheWithEtagAttribute(int clientCacheInSeconds)
{
_clientCache = TimeSpan.FromSeconds(clientCacheInSeconds);
}
public override async Task OnActionExecutedAsync(HttpActionExecutedContext actionExecutedContext, CancellationToken cancellationToken)
{
if (!_supportedRequestMethods.Contains(actionExecutedContext.Request.Method))
{
return;
}
if (actionExecutedContext.Response?.Content == null)
{
return;
}
var body = await actionExecutedContext.Response.Content.ReadAsStringAsync();
if (body == null)
{
return;
}
var computedEntityTag = GetETag(Encoding.UTF8.GetBytes(body));
if (actionExecutedContext.Request.Headers.IfNoneMatch.Any()
&& actionExecutedContext.Request.Headers.IfNoneMatch.First().Tag.Trim('"').Equals(computedEntityTag, StringComparison.InvariantCultureIgnoreCase))
{
actionExecutedContext.Response.StatusCode = HttpStatusCode.NotModified;
actionExecutedContext.Response.Content = null;
}
var cacheControlHeader = new CacheControlHeaderValue
{
Private = true,
MaxAge = _clientCache
};
actionExecutedContext.Response.Headers.ETag = new EntityTagHeaderValue($"\"{computedEntityTag}\"", false);
actionExecutedContext.Response.Headers.CacheControl = cacheControlHeader;
}
private static string GetETag(byte[] contentBytes)
{
using (var md5 = MD5.Create())
{
var hash = md5.ComputeHash(contentBytes);
string hex = BitConverter.ToString(hash);
return hex.Replace("-", "");
}
}
}
Usage e.g: with 1 min client side caching:
[ClientCacheWithEtag(60)]
Seems to be a nice way to do it:
public class CacheControlAttribute : System.Web.Http.Filters.ActionFilterAttribute
{
public int MaxAge { get; set; }
public CacheControlAttribute()
{
MaxAge = 3600;
}
public override void OnActionExecuted(HttpActionExecutedContext context)
{
if (context.Response != null)
{
context.Response.Headers.CacheControl = new CacheControlHeaderValue
{
Public = true,
MaxAge = TimeSpan.FromSeconds(MaxAge)
};
context.Response.Headers.ETag = new EntityTagHeaderValue(string.Concat("\"", context.Response.Content.ReadAsStringAsync().Result.GetHashCode(), "\""),true);
}
base.OnActionExecuted(context);
}
}

Categories

Resources