student18
student18

Reputation: 538

Force reload of ResponseCache in .NET Core 2.1 when request data changes

I am using the following attribute [ResponseCache(Duration = 60)] to cache a specific GET Request which is called a lot on my backend in .NET Core.

Everything is working fine except the cache isn't reloaded when some data in database has changed within the 60 seconds.
Is there a specific directive I have to set to reload/update the cache? link

Example Code Snippet from my Controller:

[HttpGet]
[ResponseCache(Duration = 60)]
public ActionResult<SomeTyp[]> SendDtos()
{
    var dtos = _repository.QueryAll();

    return Ok(dtos);
}

Upvotes: 0

Views: 1523

Answers (1)

Igor Goyda
Igor Goyda

Reputation: 2017

There is a solution with a usage of "ETag", "If-None-Match" HTTP headers. The idea is using a code which can give us an answer to the question: "Did action response changed?". This can be done if a controller completely owns particular data lifetime.

Create ITagProvider:

public interface ITagProvider
{
    string GetETag(string tagKey);
    void InvalidateETag(string tagKey);
}

Create an action filter:

public class ETagActionFilter : IActionFilter
{
    private readonly ITagProvider _tagProvider;

    public ETagActionFilter(ITagProvider tagProvider)
    {
        _tagProvider = tagProvider ?? throw new ArgumentNullException(nameof(tagProvider));
    }

    public void OnActionExecuted(ActionExecutedContext context)
    {
        if (context.Exception != null)
        {
            return;
        }
        var uri = GetActionName(context.ActionDescriptor);
        var currentEtag = _tagProvider.GetETag(uri);
        if (!string.IsNullOrEmpty(currentEtag))
        {
            context.HttpContext.Response.Headers.Add("ETag", currentEtag);
        }
    }

    public void OnActionExecuting(ActionExecutingContext context)
    {
        var uri = GetActionName(context.ActionDescriptor);
        var requestedEtag = context.HttpContext.Request.Headers["If-None-Match"];
        var currentEtag = _tagProvider.GetETag(uri);
        if (requestedEtag.Contains(currentEtag))
        {
            context.HttpContext.Response.Headers.Add("ETag", currentEtag);
            context.Result = new StatusCodeResult(StatusCodes.Status304NotModified);
        }
    }

    private string GetActionName(ActionDescriptor actionDescriptor)
    {
        return $"{actionDescriptor.RouteValues["controller"]}.{actionDescriptor.RouteValues["action"]}";
    }
}

Initialize filter in Startup class:

public void ConfigureServices(IServiceCollection services)
{
    // code above
    services.AddMvc(options =>
        {
            options.Filters.Add(typeof(ETagActionFilter));
        });
    services.AddScoped<ETagActionFilter>();
    services.AddSingleton<ITagProvider, TagProvider>();
    // code below
}

Use InvalidateETag method somewhere in controllers (in the place where you modifing data):

    [HttpPost]
    public async Task<ActionResult> Post([FromBody] SomeType data)
    {
        // TODO: Modify data
        // Invalidate tag
        var tag = $"{controllerName}.{methodName}"
        _tagProvider.InvalidateETag(tag);
        return NoContent();
    }

This solution may require a change of a client side. If you are using fetch, you can use, for example, the following library: https://github.com/export-mike/f-etag.

P.S. I didn't specify an implementation of the ITagProvider interface, you will need to write your own. P.P.S. Articles about ETag and caching: https://developers.google.com/web/fundamentals/performance/optimizing-content-efficiency/http-caching, https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/ETag

Upvotes: 2

Related Questions