Ability to easily and cleanly add caching headers to the response. (Wolverine.HTTP)
Is your feature request related to a problem? Please describe. There are workarounds for this, however it could be better to have a wolverine feature for it.
Describe the solution you'd like An easy and clean way to apply caching in response headers. Maybe an attribute? Extension methods on wolverine http methods?
Describe alternatives you've considered Extension methods to IResult to apply httpContext.Response.Headers.CacheControl. Have also tried middleware to apply caching but struggled getting a parameter passed in for the cache duration etc. Each endpoint should ideally have its own caching options, or maybe having global profiles will also help for those endpoints that just need the general cache time applied.
Additional context Here is an example of a basic setup. (Can ignore the fusion cache part, this is just for less calls to the database/ handler logic). We are looking for less calls to the API itself:
public class TestEndpoint
{
[Tags("Test")]
[WolverineGet("/Test")]
public async Task<IResult> TestGet(IMessageBus messageBus, IFusionCache cache, ILogger<TestEndpoint> logger,
int num,
HttpContext context,
CancellationToken cancellationToken = default)
{
var record = await cache.GetOrSetAsync($"test{context.Request.GetDisplayUrl()}",
_ => messageBus.InvokeAsync<object>(new TestRecord(num), cancellationToken), token: cancellationToken);
return Results.Ok(record).WithCacheControl(PrivateMaxAge10Seconds);
}
}
public static class WolverineResponseCachingExtensions
{
public static IResult WithCacheControl(this IResult result, CacheControlHeaderValue cacheControl)
{
return new CachedResult(result, cacheControl);
}
}
public static class CacheControlConstants
{
public static readonly CacheControlHeaderValue PrivateMaxAge10Seconds = new()
{
Private = true,
MaxAge = 10.Seconds()
};
}
public class CachedResult(IResult innerResult, CacheControlHeaderValue cacheControl) : IResult
{
public Task ExecuteAsync(HttpContext httpContext)
{
httpContext.Response.GetTypedHeaders().CacheControl = cacheControl;
return innerResult.ExecuteAsync(httpContext);
}
}
Thanks for reading :)
You can try with the IHttpPolicy using the ResponseCacheAttribute from MVC. Here an example:
using JasperFx.CodeGeneration;
using JasperFx.CodeGeneration.Frames;
using JasperFx.Core.Reflection;
using Microsoft.AspNetCore.Mvc;
using Oakton;
using Scalar.AspNetCore;
using Wolverine;
using Wolverine.Http;
using Wolverine.Runtime;
var builder = WebApplication.CreateBuilder(args);
// Add services to the container.
// Learn more about configuring OpenAPI at https://aka.ms/aspnet/openapi
builder.Services.AddOpenApi();
builder.Host.UseWolverine(opts =>
{
opts.CodeGeneration.TypeLoadMode = TypeLoadMode.Auto;
});
builder.Services.AddWolverineHttp();
var app = builder.Build();
// Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment())
{
app.MapOpenApi();
app.MapScalarApiReference();
}
app.UseHttpsRedirection();
app.MapWolverineEndpoints(opts =>
{
opts.AddPolicy<HttpChainResponseCacheHeaderPolicy>();
});
return await app.RunOaktonCommands(args);
[ResponseCache(Duration = 30)]
public class HelloEndpoint
{
[WolverineGet("/")]
public string Get() => "Hello.";
[ResponseCache(Duration = 10)]
[WolverineGet("/now")]
public string GetNow() // using the custom parameter strategy for "now"
{
return DateTimeOffset.Now.ToString("G");
}
}
internal class HttpChainResponseCacheHeaderPolicy : IHttpPolicy
{
public void Apply(IReadOnlyList<HttpChain> chains, GenerationRules rules, IServiceContainer container)
{
foreach (var chain in chains.Where(x => x.HasAttribute<ResponseCacheAttribute>()))
{
Apply(chain, container);
}
}
public void Apply(HttpChain chain, IServiceContainer container)
{
var cache = chain.Method.Method.GetAttribute<ResponseCacheAttribute>();
cache ??= chain.Method.HandlerType.GetAttribute<ResponseCacheAttribute>()!;
chain.Postprocessors.Add(new ResponseCacheFrame(cache.Duration));
}
}
internal class ResponseCacheFrame : SyncFrame
{
private readonly int _duration;
public ResponseCacheFrame(int duration)
{
_duration = duration;
}
public override void GenerateCode(GeneratedMethod method, ISourceWriter writer)
{
writer.Write($"httpContext.Response.GetTypedHeaders().CacheControl = new() {{ MaxAge = TimeSpan.FromSeconds({_duration}) }};");
Next?.GenerateCode(method, writer);
}
}
The code generated:
// <auto-generated/>
#pragma warning disable
using Microsoft.AspNetCore.Routing;
using System;
using System.Linq;
using Wolverine.Http;
namespace Internal.Generated.WolverineHandlers
{
// START: GET_
public class GET_ : Wolverine.Http.HttpHandler
{
private readonly Wolverine.Http.WolverineHttpOptions _wolverineHttpOptions;
public GET_(Wolverine.Http.WolverineHttpOptions wolverineHttpOptions) : base(wolverineHttpOptions)
{
_wolverineHttpOptions = wolverineHttpOptions;
}
public override async System.Threading.Tasks.Task Handle(Microsoft.AspNetCore.Http.HttpContext httpContext)
{
var helloEndpoint = new HelloEndpoint();
// The actual HTTP request handler execution
var result_of_Get = helloEndpoint.Get();
--> httpContext.Response.GetTypedHeaders().CacheControl = new() { MaxAge = TimeSpan.FromSeconds(30) };
await WriteString(httpContext, result_of_Get);
}
}
// END: GET_
// START: GET_now
public class GET_now : Wolverine.Http.HttpHandler
{
private readonly Wolverine.Http.WolverineHttpOptions _wolverineHttpOptions;
public GET_now(Wolverine.Http.WolverineHttpOptions wolverineHttpOptions) : base(wolverineHttpOptions)
{
_wolverineHttpOptions = wolverineHttpOptions;
}
public override async System.Threading.Tasks.Task Handle(Microsoft.AspNetCore.Http.HttpContext httpContext)
{
var helloEndpoint = new HelloEndpoint();
// The actual HTTP request handler execution
var result_of_GetNow = helloEndpoint.GetNow();
--> httpContext.Response.GetTypedHeaders().CacheControl = new() { MaxAge = TimeSpan.FromSeconds(10) };
await WriteString(httpContext, result_of_GetNow);
}
}
// END: GET_now
}
// <auto-generated/>
#pragma warning disable
namespace Internal.Generated.WolverineHandlers
{
}