Reputation: 51
I want to cache responses from APIs to DistributedSqlServerCache. The default ResponseCaching only uses a memory cache. There is a constructor which allows to configure what cache to use, but it's internal.
I wrote a filter. If the response is not cached and the http response is OK and the ActionResult is an ObjectActionResult, it serializes the value as JSON and saves it to SQL cache. If the response is cached, it deserializes it and sets the result as an OkObject result with the deserielized object.
It works ok, but it has some clumsy things (like, to use the attribute, you have to specify the type which will be de/serialized, with typeof()).
Is there a way to cache responses to a distributed sql cache, which doesn't involve me hacking together my own mostly working solution?
Another option would be to copy-pasta the netcore ResponseCacheMiddleWare, and modify it to use a diffirent cache. I could even make it a nuget package maybe.
Are there any other solutions out there?
Here's the filter I put together (simplified for display purposes)
namespace Api.Filters
{
/// <summary>
/// Caches the result of the action as data.
/// The action result must implement <see cref="ObjectResult"/>, and is only cached if the HTTP status code is OK.
/// </summary>
public class ResponseCache : IAsyncResourceFilter
{
public Type ActionType { get; set; }
public ExpirationType ExpirationType;
private readonly IDistributedCache cache;
public ResponseCache(IDistributedCache cache)
{
this.cache = cache;
}
public async Task OnResourceExecutionAsync(ResourceExecutingContext executingContext, ResourceExecutionDelegate next)
{
var key = getKey(executingContext);
var cachedValue = await cache.GetAsync(key);
if (cachedValue != null && executingContext.HttpContext.Request.Query["r"] == "cache")
{
await cache.RemoveAsync(key);
cachedValue = null;
}
if (cachedValue != null)
{
executingContext.Result = new OkObjectResult(await fromBytes(cachedValue));
return;
}
var executedContext = await next();
// Only cache a successful response.
if (executedContext.HttpContext.Response.StatusCode == StatusCodes.Status200OK && executedContext.Result is ObjectResult result)
{
await cache.SetAsync(key, await toBytes(result.Value), getExpiration());
}
}
private async Task<byte[]> toBytes(object value)
{
using var stream = new MemoryStream();
await JsonSerializer.SerializeAsync(stream, value, ActionType);
return stream.ToArray();
}
private async Task<object> fromBytes(byte[] bytes)
{
using var stream = new MemoryStream(bytes);
using var reader = new BinaryReader(stream, Encoding.Default, true);
return await JsonSerializer.DeserializeAsync(stream, ActionType);
}
}
public class ResponseCacheAttribute : Attribute, IFilterFactory
{
public bool IsReusable => true;
public ExpirationType ExpirationType;
public Type ActionType { get; set; }
public ResponseCacheAttribute(params string[] queryParameters)
{
this.queryParameters = queryParameters;
}
public IFilterMetadata CreateInstance(IServiceProvider serviceProvider)
{
var cache = serviceProvider.GetService(typeof(IDistributedCache)) as IDistributedCache;
return new ResponseCache(cache)
{
ExpirationType = ExpirationType,
ActionType = ActionType
};
}
}
}
Upvotes: 4
Views: 1532
Reputation: 51
In the end I made a nuget package, sourced on github. See this issue for some more context as to why a new package was made.
Upvotes: 0