Skip to content

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

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.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

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