Skip to content

Middleware returns 304 when DisableGlobalHeaderGeneration = true and StoreKey is MarkForInvalidation #140

@Appli4Ever

Description

@Appli4Ever

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions