HttpCacheHeaders icon indicating copy to clipboard operation
HttpCacheHeaders copied to clipboard

Middleware returns 304 when DisableGlobalHeaderGeneration = true and StoreKey is MarkForInvalidation

Open Appli4Ever opened this issue 8 months ago • 0 comments

Steps to Reproduce

  • New Blazor 8 Web App Project
  • Hosted by Asp.NET Core
  • Install NuGet Marvin.Cache.Headers
  • Configure
builder.Services.AddHttpCacheHeaders(
    o =>
    {
        o.CacheLocation = CacheLocation.Private;
        o.MaxAge = 20;
    },
    v =>
    {
        v.MustRevalidate = false;
        v.VaryByAll = false;
    },
    m =>
    {
        m.DisableGlobalHeaderGeneration = true;
        m.IgnoredStatusCodes = HttpStatusCodes.AllErrors;
    });
  • Add Middleware
...
app.UseHttpsRedirection();

app.UseHttpCacheHeaders();
...
  • Add Client Service
public class HttpWeatherForecastService
{
    private readonly HttpClient client;

    public HttpWeatherForecastService(HttpClient client)
    {
        this.client = client;
    }

    public async Task<WeatherForecast[]> GetListAsync()
    {
        var forcast = await this.client.GetFromJsonAsync<WeatherForecast[]>("/api/Weather/Get");

        return forcast;
    }

    public async Task Invalidate()
    {
        await this.client.PostAsync("api/Weather/InvalidateCache", null);
    }
}

...
builder.Services.AddTransient<HttpWeatherForecastService>();
builder.Services.AddHttpClient("API", c => c.BaseAddress = new Uri(builder.HostEnvironment.BaseAddress));
builder.Services.AddTransient(
    sp => sp.GetRequiredService<IHttpClientFactory>()
            .CreateClient("API"));
  • Add Endpoints
[Route("api/[controller]/[action]")]
[ApiController]
public class WeatherController : ControllerBase
{
    private readonly IWeatherForecastService service;
    private readonly IValidatorValueInvalidator invalidator;
    private readonly IStoreKeyAccessor storeKeyAccessor;
    private readonly IValidatorValueStore valueStore;

    public WeatherController(IWeatherForecastService service,
                        IValidatorValueInvalidator invalidator,
                        IStoreKeyAccessor storeKeyAccessor,
                        IValidatorValueStore valueStore)
    {
        this.service = service;
        this.invalidator = invalidator;
        this.storeKeyAccessor = storeKeyAccessor;
        this.valueStore = valueStore;
    }

    [HttpPost]
    public async Task<IActionResult> InvalidateCache()
    {
        var validatorValues = this.storeKeyAccessor.FindByKeyPart("Weather");

        await foreach (var value in validatorValues)
        {
            await this.invalidator.MarkForInvalidation(value);
        }

        return this.Ok();
    }

    [HttpGet]
    [HttpCacheExpiration(CacheLocation = CacheLocation.Private, MaxAge = 15)]
    [HttpCacheValidation(MustRevalidate = false, ProxyRevalidate = false, VaryByAll = false)]
    public async Task<ActionResult<WeatherForecast[]>> Get()
    {
        var startDate = DateOnly.FromDateTime(DateTime.Now);
        var summaries = new[]
        {
            "Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot",
            "Sweltering", "Scorching"
        };

        var forecasts = Enumerable.Range(1, 5)
                                  .Select(
                                      index => new WeatherForecast
                                      {
                                          Date = startDate.AddDays(index),
                                          TemperatureC = Random.Shared.Next(-20, 55),
                                          Summary = summaries[
                                              Random.Shared.Next(summaries.Length)]
                                      })
                                  .ToArray();

        return this.Ok(forecasts);
    }
}
...

app.MapRazorComponents<App>()
    .AddInteractiveWebAssemblyRenderMode()
    .AddAdditionalAssemblies(typeof(Cache_Invalidation.Client._Imports).Assembly);

app.MapControllers();
  • Add Helpter Buttons
<p>This component demonstrates showing data.</p>

<button @onclick="Invalidate">
    Invalidate
</button>

<button @onclick="OnInitializedAsync">
    Get Data
</button>

...

public partial class Weather
{
    private WeatherForecast[]? forecasts;

    [Inject]
    protected HttpWeatherForecastService Service { get; set; }

    protected override async Task OnInitializedAsync()
    {
        this.forecasts = await this.Service.GetListAsync();
    }

    private void Invalidate()
    {
        this.Service.Invalidate();
    }
}

Expected Behavior

  1. When clicking the Invalidate Button and waiting 15 seconds for the cache to stale.
  2. Then clicking Get Data
  3. I Expected the Middleware to not find the StoreKey, since it has been invalidated.

Actual Behavior

  1. Doing the first two steps of Expected Behavior.
  2. The Middleware still returns a 304 Not Modified result.

Workaround Setting DisableGlobalHeaderGeneration to false.

m.DisableGlobalHeaderGeneration = true;

This works, but now every Endpoint generates Cache Headers. This is not my desired behavior because I don't want ETags on Blazor Framework files. Also I have to set [HttpCacheIgnore] on every other endpoint that I don't want cache on.

Debugging I have debugged the HttpCacheHeadersMiddleware: ConditionalGetOrHeadIsValid returns true because of this Code:

ValidatorValue async = await this._store.GetAsync(await this._storeKeyGenerator.GenerateStoreKey(this.ConstructStoreKeyContext(httpContext.Request, this._validationModelOptions)));

The StoreKey is found, even after invalidating it via the endpoint.

Appli4Ever avatar Jun 25 '24 08:06 Appli4Ever