Search code examples
c#browser-cache.net-8.0

Response Cache for Minimal API


We moved from old-fashioned Controllers to minimal API in our c# projects. So far so good for most view points we prefer minimal API. However one topic we can't solve: Response Caching.

In Controller based development one can add the attribute to add the response cache (important, we are talking about client side caching, not server side)

    [ResponseCache(VaryByHeader = "User-Agent", Duration = 30)]

For minimal API I found this here: https://learn.microsoft.com/en-us/aspnet/core/performance/caching/middleware?view=aspnetcore-8.0

But I can't understand, how I can chose which endpoints to have response cache and which one don't. I appears, all endpoints will use the same cache which is something I absolutely don't want.

My copilot proposes this here

app.MapGet("/data", () =>
{
    var response = Results.Json(new { message = "Cached Data" });
    response.Headers["Cache-Control"] = "public,max-age=60";
    return response;
});

I can live with that, but can we combine that to something like?

app.MapGet("/data", () =>
{
    var response = new { message = "Cached Data" };
    return response;
}).UseResponseCache(60);

Solution

  • I would define such extension method for RouteHandlerBuilder:

    public static RouteHandlerBuilder WithResponseCache(
        this RouteHandlerBuilder builder,
        int durationSeconds,
        string varyByHeader)
    {
        builder.Add(endpointBuilder =>
        {
            endpointBuilder.Metadata.Add(new ResponseCacheAttribute()
            {
                Duration = durationSeconds,
                VaryByHeader = varyByHeader,
            });
        });
    
        return builder;
    }
    

    This unforutnately won't work out of the box with Minimal APIs, for that we need to register custom middleware (as described in the documentation) to handle the metadata:

    app.Use(async (context, next) =>
    {
        var endpointMetadata = context.GetEndpoint()?.Metadata;
    
        if(endpointMetadata is not null)
        {
            var responseCache = endpointMetadata.FirstOrDefault(x => x is ResponseCacheAttribute);
    
            if(responseCache is ResponseCacheAttribute attribute)
            {
                context.Response.GetTypedHeaders().CacheControl =
                    new Microsoft.Net.Http.Headers.CacheControlHeaderValue()
                    {
                        Public = true,
                        MaxAge = TimeSpan.FromSeconds(attribute.Duration)
                    };
                context.Response.Headers[Microsoft.Net.Http.Headers.HeaderNames.Vary] =
                    new string[] { attribute.VaryByHeader };
            }
        }
    
        await next();
    });
    

    Of course, you need to also call AddResponseCaching and UseResponseCaching to add needed services.

    And I just used ResponseCacheAttribute class, but you could of course use also custom class for that.