I'm trying to build a response cache with custom key values (from headers and cookies). Here's the code I have so far, and it kinda works, but I need a second opinion or suggestions for improvements.
Are there any possible errors, faster key generation methods etc.
Here's the code for middleware and cache key building:
app.Use(async (ctx, next) => {
//cache only get requests
if (ctx.Request.Method != "GET") {
await next();
return;
}
//remove cache key header in case it's appended already
ctx.Request.Headers.Remove(ResponseCacheKeyProvider.CacheKeyHeader);
//remove cache control header. I would like to control this (and response cache does not work if header is set to 0
if (ctx.Request.Headers.ContainsKey("Cache-Control"))
ctx.Request.Headers.Remove("Cache-Control");
ctx.Request.GetTypedHeaders().CacheControl = new Microsoft.Net.Http.Headers.CacheControlHeaderValue() {
Public = true,
MaxAge = TimeSpan.FromSeconds(60)
};
//set cookie value if user does not have it (for a/b) testing
ABTestingCookieSetter.SetAbCookie(ctx);
//build a key for cache
var key = ResponseCacheKeyProvider.BuildKey(ctx);
//attach a key to header
if (!ctx.Request.Headers.ContainsKey(ResponseCacheKeyProvider.CacheKeyHeader))
ctx.Request.Headers.Add(ResponseCacheKeyProvider.CacheKeyHeader, key);
await next();
});
//mark necessary flags in request, so we know when to render international layout
app.Use(async (ctx, next) => {
if (ctx.Request.GetUri().AbsolutePath.StartsWith("/eng") || ctx.Request.GetUri().AbsolutePath.StartsWith("/mobile/eng"))
ctx.LayoutOptions().SetAsInternational();
await next();
});
//attach response caching middleware
app.UseResponseCaching();
//add header to see when cache was generated (so i can debug is it working)
app.Use(async (ctx, next) => {
if (ctx.Request.Method != "GET") {
await next();
return;
}
ctx.Response.Headers.Remove("Index-Generated-Utc");
if (!ctx.Response.Headers.ContainsKey("Index-Generated-Utc"))
ctx.Response.Headers.Add("Index-Generated-Utc", DateTime.UtcNow.ToString("HH:mm:ss"));
await next();
});
//class to build a cache key
public class ResponseCacheKeyProvider {
public
const string CacheKeyHeader = "Index-Cache-Key";
//header names to take values from for cache key
private static readonly List < string > defaultVaryByHeaders = new List < string > {
"CF-IPCountry",
"Accept-Encoding"
};
//cookie names, to take values from for cache key
private static readonly List < string > defaultVaryByCookies = new List < string > {
ABTestUser.cookieName,
"dark-theme",
"weatherSelectedCity",
"hiddenBreakingNews"
};
public static string BuildKey(HttpContext context) {
//mobile/desktop device resolver
var _deviceResolver = context.RequestServices.GetService < IDeviceResolver > ();
var sb = new StringBuilder();
sb.Append(context.Request.GetUri().AbsoluteUri);
sb.Append(_deviceResolver.Device.Type.ToString());
for (var i = 0; i < defaultVaryByHeaders.Count; i++) {
var headerValues = context.Request.Headers[defaultVaryByHeaders[i]];
for (var j = 0; j < headerValues.Count(); j++) {
sb.Append(headerValues[j]);
}
}
for (var i = 0; i < defaultVaryByCookies.Count; i++) {
var cookieValue = context.Request.Cookies[defaultVaryByCookies[i]];
sb.Append(defaultVaryByCookies[i]);
if (cookieValue != null && cookieValue != string.Empty)
sb.Append(cookieValue);
else if (cookieValue == string.Empty)
sb.Append("empty"); //if cookie has empty value, append this as a part of the string, to avoid empty Cookie corruptingCache
}
return CalculateHash(sb.ToString());
}
private static string CalculateHash(string read) {
var data = Encoding.ASCII.GetBytes(read);
using(SHA384 shaM = new SHA384Managed()) {
var hash = shaM.ComputeHash(data);
return Convert.ToBase64String(hash);
}
}
}
And here is where is set response caching profiles:
services.AddMvc(options =>
{
options.CacheProfiles.Add("HomePage", new CacheProfile()
{
Duration = Constants.HomePageOutputCacheInSeconds,
Location = ResponseCacheLocation.Any,
VaryByHeader = ResponseCacheKeyProvider.CacheKeyHeader
});
options.CacheProfiles.Add("Article", new CacheProfile()
{
Duration = Constants.ArticleOutputCacheInSeconds,
Location = ResponseCacheLocation.Any,
VaryByHeader = ResponseCacheKeyProvider.CacheKeyHeader
});
options.CacheProfiles.Add("Default", new CacheProfile()
{
Duration = Constants.DefaultOutputCacheInSeconds,
Location = ResponseCacheLocation.Any,
VaryByHeader = ResponseCacheKeyProvider.CacheKeyHeader
});
}).SetCompatibilityVersion(Microsoft.AspNetCore.Mvc.CompatibilityVersion.Version_2_2);
services.AddMemoryCache();
services.AddResponseCaching();