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?
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>());