Search code examples
c#.netcachingoutputcache.net-7.0

.NET 7 create custom Output Cache Policy


I am currently implementing some of the new features of the .NET 7 framework. One part is related to the new caching mechanism.

At startup I have configured the cache:

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddHealthChecks();
builder.Services.AddCors();
builder.Services.AddOutputCache();
//builder.Services.InitializeApplication(builder.Configuration);

var app = builder.Build();

app.UseOutputCache();
app.UseCors();
//app.UseAuthentication();
//app.UseAuthorization();

if (app.Environment.IsDevelopment())
{
    app.UseSwagger();
    app.UseSwaggerUI();
}

app.UseHttpsRedirection();

app.MapGroup("reports-api/requests")
    .MapRequestsApi();

app.MapHealthChecks("/healthcheck");

app.Run();

The route group looks like this:

public static class RequestsEndpoint
{
    public static RouteGroupBuilder MapRequestsApi(this RouteGroupBuilder group)
    {
        group.MapGet("/all", async (IMediator mediator) =>
            {
                await Task.Delay(2000);
                return await mediator.Send(new RetrieveRequestsQuery());
            })
            .CacheOutput(x => x.Tag("requests"));

        group.MapPost("/add", async (string details, IMediator mediator, IOutputCacheStore store) =>
        { 
            await mediator.Send(new AddRequestCommand { Details = details });
            await store.EvictByTagAsync("requests", CancellationToken.None);

        });

        group.MapGet("/{id}", async (Guid requestId, IMediator mediator) =>
        {
            await mediator.Send(new RetrieveRequestDetailsQuery()
            {
                Id = requestId
            });
        });

        //group.MapPut("/{id}", UpdateRequest);
        //group.MapDelete("/{id}", DeleteRequest);

        return group;
    }
}

The cache mechanism with tags works fine when I want to serve the requests list from the cache or when I want to evict the cache (new item in the list)

However, I'd like to have some sort of cache per item - when I retrieve the request based on an ID, I'd like to only cache those values until I have the details changed by a PUT or PATCH.

What I could do is to register the group.MapGet("/{id}") endpoint with the same tag ("requests"). However, if there is an update I am forced to evict everything. That's not ideal.

I was looking at this video (Output Cache Microsoft) and they are looking at something called DefaultOutputCachePolicy

I can only find in .NET 7 the interface: IOutputCachePolicy which is asking me to implement the following method:

public class ByIdCachePolicy : IOutputCachePolicy
{
    public ValueTask CacheRequestAsync(OutputCacheContext context, CancellationToken cancellation) => throw new NotImplementedException();

    public ValueTask ServeFromCacheAsync(OutputCacheContext context, CancellationToken cancellation) => throw new NotImplementedException();

    public ValueTask ServeResponseAsync(OutputCacheContext context, CancellationToken cancellation) => throw new NotImplementedException();
}

There is no clue in the docs about how this needs to be implemented, and I couldn't find the code of the Default Policy. How are we supposed to implement this interface?


Solution

  • I managed to achieve what I want with a workaround:

     group.MapGet("/{id}", async (Guid requestId, IMediator mediator) =>
            {
                await Task.Delay(2000);
                return await mediator.Send(new RetrieveRequestDetailsQuery
                {
                    Id = requestId
                });
            })
            .CacheOutput(cachePolicyBuilder => cachePolicyBuilder.With(context =>
            {
                if (context.HttpContext.Request.QueryString.Value != null)
                {
                  var queryParams =  HttpUtility.ParseQueryString(context.HttpContext.Request.QueryString.Value);
                    context.Tags.Add(queryParams["requestId"]!);
                }
    
                return true;
            }));
    
        group.MapPut("/{id}",
            async (Guid id, IOutputCacheStore store, CancellationToken ct) =>
                await store.EvictByTagAsync(id.ToString(), CT));
    

    Using the cachePolicyBuilder With method, I got access to the cache context and could look into the query string & add the custom tag (by ID).

    However, this is not straightforward since the With function allows you to filter the cached requests. What am I doing here is always returning true but, also adding the custom tag.

    Not ideal, but it works...

    Edit

    After looking a bit more into it I ended up using the following policy:

    public class ByIdCachePolicy : IOutputCachePolicy
    {
        public ValueTask CacheRequestAsync(OutputCacheContext context, CancellationToken cancellation)
        {
            var idRoute = context.HttpContext.Request.RouteValues["id"];
            if (idRoute == null)
            {
                return ValueTask.CompletedTask;
            }
            context.Tags.Add(idRoute.ToString()!);
            return ValueTask.CompletedTask;
        }
    
        public ValueTask ServeFromCacheAsync(OutputCacheContext context, CancellationToken cancellation)=> ValueTask.CompletedTask;
    
        public ValueTask ServeResponseAsync(OutputCacheContext context, CancellationToken cancellation) => ValueTask.CompletedTask;
    }
    

    like this:

    group.MapGet("/{id}", async (Guid id, IMediator mediator) =>
        {
            await Task.Delay(2000);
            return await mediator.Send(new RetrieveRequestDetailsQuery
            {
                Id = id
            });
        }).CacheOutput(x => x.AddPolicy<ByIdCachePolicy>());