• Stars
    star
    266
  • Rank 154,103 (Top 4 %)
  • Language
    C#
  • License
    MIT License
  • Created almost 8 years ago
  • Updated 9 months ago

Reviews

There are no reviews yet. Be the first to send feedback to the community and the maintainers!

Repository Details

ASP.NET Core middleware that adds HttpCache headers to responses (Cache-Control, Expires, ETag, Last-Modified), and implements cache expiration & validation models

Http Cache Headers Middleware for ASP.NET Core

ASP.NET Core middleware that adds HttpCache headers to responses (Cache-Control, Expires, ETag, Last-Modified), and implements cache expiration & validation models. It can be used to ensure caches correctly cache responses and/or to implement concurrency for REST-based APIs using ETags.

The middleware itself does not store responses. Looking at this description, this middleware handles the "backend"-part: it generates the correct cache-related headers, and ensures a cache can check for expiration (304 Not Modified) & preconditions (412 Precondition Failed) (often used for concurrency checks).

It can be used together with a shared cache, a private cache or both. For production scenarios the best approach is to use this middleware to generate the ETags, combined with a cache server or CDN to inspect those tags and effectively cache the responses. In the sample, the Microsoft.AspNetCore.ResponseCaching cache store is used to cache the responses.

NuGet version

Installation (NuGet)

Install-Package Marvin.Cache.Headers

Usage

First, register the services with ASP.NET Core's dependency injection container (in the ConfigureServices method on the Startup class)

services.AddHttpCacheHeaders();

Then, add the middleware to the request pipeline. Starting with version 6.0, the middleware MUST be added between UseRouting() and UseEndpoints().

app.UseRouting(); 

app.UseHttpCacheHeaders();

app.UseEndpoints(...);

Configuring Options

The middleware allows customization of how headers are generated. The AddHttpCacheHeaders() method has parameters for configuring options related to expiration, validation and middleware.

For example, this code will set the max-age directive to 600 seconds, add the must-revalidate directive and ignore header generation for all responses with status code 500.

services.AddHttpCacheHeaders(
    expirationModelOptions =>
    {
        expirationModelOptions.MaxAge = 600;
    },
    validationModelOptions =>
    {
        validationModelOptions.MustRevalidate = true;
    },
    middlewareOptions => 
    {
        middlewareOptions.IgnoreStatusCodes = new[] { 500 };
    });

There are some predefined collections with status codes you can use when you want to ignore:

  • all server errors HttpStatusCodes.ServerErrors
  • all client errors HttpStatusCodes.ClientErrors
  • all errors HttpStatusCodes.AllErrors

Action (Resource) and Controller-level Header Configuration

For anything but the simplest of cases having one global cache policy isn't sufficient: configuration at level of each resource (action/controller) is required. For those cases, use the HttpCacheExpiration and/or HttpCacheValidation attributes at action or controller level.

[HttpGet]
[HttpCacheExpiration(CacheLocation = CacheLocation.Public, MaxAge = 99999)]
[HttpCacheValidation(MustRevalidate = true)]
public IEnumerable<string> Get()
{
    return new[] { "value1", "value2" };
}
```
Both override the global options.  Action-level configuration overrides controller-level configuration.

# Ignoring Cache Headers / eTag Generation

You don't always want tags / headers to be generated for all resources (e.g.: for a large file).  You can ignore generation by applying the HttpCacheIgnore attribute at controller or action level. 

````csharp
[HttpGet]
[HttpCacheIgnore]
public IEnumerable<string> Get()
{
    return new[] { "value1", "value2" };
}

If you want to globally disable automatic header generation, you can do so by setting DisableGlobalHeaderGeneration on the middleware options to true.

services.AddHttpCacheHeaders(     
    middlewareOptionsAction: middlewareOptions => 
    {
        middlewareOptions.DisableGlobalHeaderGeneration = true;
    });

Marking for Invalidation

Cache invalidation essentially means wiping a response from the cache because you know it isn't the correct version anymore. Caches often partially automate this (a response can be invalidated when it becomes stale, for example) and/or expose an API to manually invalidate items.

The same goes for the cache headers middleware, which holds a store of records with previously generated cache headers & tags. Replacement of store key records (/invalidation) is mostly automatic. Say you're interacting with values/1. First time the backend is hit and you get back an eTag in the response headers. Next request you send is again a GET request with the "If-None-Match"-header set to the eTag: the backend won't be hit. Then, you send a PUT request to values/1, which potentially results in a change; if you send a GET request now, the backend will be hit again.

However: if you're updating/changing resources by using an out of band mechanism (eg: a backend process that changes the data in your database, or a resource gets updated that has an update of related resources as a side effect), this process can't be automated.

Take a list of employees as an example. If a PUT statement is sent to one "employees" resource, then that one "employees" resource will get a new Etag. Yet: if you're sending a PUT request to one specific employee ("employees/1", "employees/2", ...), this might have the effect that the "employees" resource has also changed: if the employee you just updated is one of the employees in the returned employees list when fetching the "employees" resource, the "employees" resource is out of date. Same goes for deleting or creating an employee: that, too, might have an effect on the "employees" resource.

To support this scenario the cache headers middleware allows marking an item for invalidation. When doing that, the related item will be removed from the internal store, meaning that for subsequent requests a stored item will not be found.

To use this, inject an IValidatorValueInvalidator and call MarkForInvalidation on it, passing through the key(s) of the item(s) you want to be removed. You can additionally inject an IStoreKeyAccessor, which contains methods that make it easy to find one or more keys from (part of) a URI.

Extensibility

The middleware is very extensible. If you have a look at the AddHttpCacheHeaders method you'll notice it allows injecting custom implementations of IValidatorValueStore, IStoreKeyGenerator, IETagGenerator and/or IDateParser (via actions).

IValidatorValueStore

A validator value store stores validator values. A validator value is used by the cache validation model when checking if a cached item is still valid. It contains ETag and LastModified properties. The default IValidatorValueStore implementation (InMemoryValidatorValueStore) is an in-memory store that stores items in a ConcurrentDictionary<string, ValidatorValue>.

/// <summary>
/// Contract for a store for validator values.  Each item is stored with a <see cref="StoreKey" /> as key```
/// and a <see cref="ValidatorValue" /> as value (consisting of an ETag and Last-Modified date).   
/// </summary>
public interface IValidatorValueStore
{
    /// <summary>
    /// Get a value from the store.
    /// </summary>
    /// <param name="key">The <see cref="StoreKey"/> of the value to get.</param>
    /// <returns></returns>
    Task<ValidatorValue> GetAsync(StoreKey key);
    /// <summary>
    /// Set a value in the store.
    /// </summary>
    /// <param name="key">The <see cref="StoreKey"/> of the value to store.</param>
    /// <param name="validatorValue">The <see cref="ValidatorValue"/> to store.</param>
    /// <returns></returns>
    Task SetAsync(StoreKey key, ValidatorValue validatorValue);

    /// <summary>
    /// Find one or more keys that contain the inputted valueToMatch 
    /// </summary>
    /// <param name="valueToMatch">The value to match as part of the key</param>
    /// <param name="ignoreCase">Ignore case when matching</param>
    /// <returns></returns>
    Task<IEnumerable<StoreKey>> FindStoreKeysByKeyPartAsync(string valueToMatch, bool ignoreCase);
}

BREAKING CHANGE from v7 onwards: the FindStoreKeysByKeyPartAsync methods return an IAsyncEnumerable to enable async streaming of results.

/// <summary>
/// Contract for a store for validator values.  Each item is stored with a <see cref="StoreKey" /> as key```
/// and a <see cref="ValidatorValue" /> as value (consisting of an ETag and Last-Modified date).   
/// </summary>
public interface IValidatorValueStore
{
    /// <summary>
    /// Get a value from the store.
    /// </summary>
    /// <param name="key">The <see cref="StoreKey"/> of the value to get.</param>
    /// <returns></returns>
    Task<ValidatorValue> GetAsync(StoreKey key);
    /// <summary>
    /// Set a value in the store.
    /// </summary>
    /// <param name="key">The <see cref="StoreKey"/> of the value to store.</param>
    /// <param name="validatorValue">The <see cref="ValidatorValue"/> to store.</param>
    /// <returns></returns>
    Task SetAsync(StoreKey key, ValidatorValue validatorValue);

    /// <summary>
    /// Find one or more keys that contain the inputted valueToMatch 
    /// </summary>
    /// <param name="valueToMatch">The value to match as part of the key</param>
    /// <param name="ignoreCase">Ignore case when matching</param>
    /// <returns></returns>
    IAsyncEnumerable<StoreKey> FindStoreKeysByKeyPartAsync(string valueToMatch, bool ignoreCase);
}

IStoreKeyGenerator

The StoreKey, as used by the IValidatorValueStore as key, can be customized as well. To do so, implement the IStoreKeyGenerator interface. The default implementation (DefaultStoreKeyGenerator) generates a key from the request path, request query string and request header values (taking VaryBy into account). Through StoreKeyContext you can access all applicable values that can be useful for generating such a key.

/// <summary>
/// Contract for a key generator, used to generate a <see cref="StoreKey" /> ```
/// </summary>
public interface IStoreKeyGenerator
{
    /// <summary>
    /// Generate a key for storing a <see cref="ValidatorValue"/> in a <see cref="IValidatorValueStore"/>.
    /// </summary>
    /// <param name="context">The <see cref="StoreKeyContext"/>.</param>         
    /// <returns></returns>
    Task<StoreKey> GenerateStoreKey(
        StoreKeyContext context);
}

IETagGenerator

You can inject an IETagGenerator-implementing class to modify how ETags are generated (ETags are part of a ValidatorValue). The default implementation (DefaultStrongETagGenerator) generates strong Etags from the request key + response body (MD5 hash from combined bytes).

/// <summary>
/// Contract for an E-Tag Generator, used to generate the unique weak or strong E-Tags for cache items
/// </summary>
public interface IETagGenerator
{
    Task<ETag> GenerateETag(
        StoreKey storeKey,
        string responseBodyContent);
}

ILastModifiedInjector

You can inject an ILastModifiedInjector-implementing class to modify how LastModified values are provided. The default implementation (DefaultLastModifiedInjector) injects the current UTC.

/// <summary>
/// Contract for a LastModifiedInjector, which can be used to inject custom last modified dates for resources
/// of which you know when they were last modified (eg: a DB timestamp, custom logic, ...)
/// </summary>
public interface ILastModifiedInjector
{
    Task<DateTimeOffset> CalculateLastModified(
        ResourceContext context);
}

IDateParser

Through IDateParser you can inject a custom date parser in case you want to override the default way dates are stringified. The default implementation (DefaultDateParser) uses the RFC1123 pattern (https://msdn.microsoft.com/en-us/library/az4se3k1(v=vs.110).aspx).

/// <summary>
/// Contract for a date parser, used to parse Last-Modified, Expires, If-Modified-Since and If-Unmodified-Since headers.
/// </summary>
public interface IDateParser
{
    Task<string> LastModifiedToString(DateTimeOffset lastModified);

    Task<string> ExpiresToString(DateTimeOffset lastModified);

    Task<DateTimeOffset?> IfModifiedSinceToDateTimeOffset(string ifModifiedSince);

    Task<DateTimeOffset?> IfUnmodifiedSinceToDateTimeOffset(string ifUnmodifiedSince);
}

IValidatorValueInvalidator

An IValidatorValueInvalidator-implenting class is responsible for marking items for invalidation.

/// <summary>
/// Contract for the <see cref="ValidatorValueInvalidator" />
/// </summary>
public interface IValidatorValueInvalidator
{
    /// <summary>
    /// Get the list of <see cref="StoreKey" /> of items marked for invalidation
    /// </summary>
    List<StoreKey> KeysMarkedForInvalidation { get; }

    /// <summary>
    /// Mark an item stored with a <see cref="StoreKey" /> for invalidation
    /// </summary>
    /// <param name="storeKey">The <see cref="StoreKey" /></param>
    /// <returns></returns>
    Task MarkForInvalidation(StoreKey storeKey);

    /// <summary>
    /// Mark a set of items for invlidation by their collection of <see cref="StoreKey" /> 
    /// </summary>
    /// <param name="storeKeys">The collection of <see cref="StoreKey" /></param>
    /// <returns></returns>
    Task MarkForInvalidation(IEnumerable<StoreKey> storeKeys);
}

IStoreKeyAccessor

The IStoreKeyAccessor contains helper methods for getting keys from parts of a URI. Override this if you're not storing items with their default keys.

/// <summary>
/// Contract for finding (a) <see cref="StoreKey" />(s)
/// </summary>    
public interface IStoreKeyAccessor
{
    /// <summary>
    /// Find a  <see cref="StoreKey" /> by part of the key
    /// </summary>
    /// <param name="valueToMatch">The value to match as part of the key</param>
    /// <returns></returns>
    Task<IEnumerable<StoreKey>> FindByKeyPart(string valueToMatch);

    /// <summary>
    /// Find a  <see cref="StoreKey" /> of which the current resource path is part of the key
    /// </summary>
    /// <returns></returns>
    Task<IEnumerable<StoreKey>> FindByCurrentResourcePath();
}

BREAKING CHANGE from v7 onwards: the methods return an IAsyncEnumerable to enable async streaming of results.

/// <summary>
/// Contract for finding (a) <see cref="StoreKey" />(s)
/// </summary>    
public interface IStoreKeyAccessor
{
    /// <summary>
    /// Find a  <see cref="StoreKey" /> by part of the key
    /// </summary>
    /// <param name="valueToMatch">The value to match as part of the key</param>
    /// <returns></returns>
    IAsyncEnumerable<StoreKey> FindByKeyPart(string valueToMatch);

    /// <summary>
    /// Find a  <see cref="StoreKey" /> of which the current resource path is part of the key
    /// </summary>
    /// <returns></returns>
    IAsyncEnumerable<StoreKey> FindByCurrentResourcePath();
}

More Repositories

1

AspNetCore6WebAPIFundamentals

Fully functioning sample code for my ASP.NET Core 6 Web API Fundamentals course
C#
259
star
2

BuildingRESTfulAPIAspNetCore3

Fully functioning sample application accompanying my Building a RESTful API with ASP.NET Core 3 course
C#
224
star
3

JsonPatch

JSON Patch (JsonPatchDocument) RFC 6902 implementation for .NET
C#
173
star
4

CSharp10DesignPatterns

Fully functioning sample code for my C#10 Design Patterns course
C#
153
star
5

SecuringAspNetCore3WithOAuth2AndOIDC

Fully functioning finished sample code for my Securing ASP.NET Core 3 with OAuth2 and OpenID Connect course
C#
122
star
6

ImplementingAdvancedRESTfulConcernsAspNetCore3

Fully functioning sample application accompanying my Implementing Advanced RESTful Concerns with ASP.NET Core 3 course
C#
115
star
7

SecuringAspNetCore6WithOAuth2AndOIDC

Fully functioning finished sample code for my Securing ASP.NET Core 6 with OAuth2 and OpenID Connect course
C#
94
star
8

RESTfulAPIAspNetCore_Course

Building a RESTful API with ASP.NET Core starter files
C#
93
star
9

SecuringAspNetCore2WithOAuth2AndOIDC

Fully functioning finished sample code for my Securing ASP.NET Core 2 with OAuth2 and OpenID Connect course
C#
71
star
10

OpenIDConnectInDepth

OpenID Connect code sample (Angular 5, ASP.NET Core 3.0, IdentityServer4) containing SSOn/Out, reference tokens, custom grants and multi-tenancy.
C#
65
star
11

StreamExtensions

A set of helper extension methods (on Stream) for working with streams. Particularly useful for interaction with an API through HttpClient.
C#
60
star
12

AngularASPNetCoreBusinessApplications

Fully functioning finished sample code for my Building Business Applications with Angular and ASP.NET Core course
C#
56
star
13

BuildingAsyncAPIAspNetCore

Fully functioning finished sample code for my Building an Async API with ASP.NET Core course
C#
54
star
14

DocumentingAspNetCoreApisWithOpenAPI

Fully functioning finished sample code for my Documenting an ASP.NET Core API with OpenAPI / Swagger course
C#
53
star
15

AspNetCore6WebAPIDeepDive

Fully functioning sample and starter files for my ASP.NET Core 6 Web API Deep Dive course
C#
47
star
16

AspNetCoreWebApiFundamentals

Fully functioning sample code for my ASP.NET Core Web API Fundamentals course, currently targeting .NET 8.
C#
46
star
17

AspNetCoreAsyncBestPracticesJetBrainsWebinar

Demo code for my webinar at Jetbrains on async best practices in ASP.NET Core
C#
45
star
18

UnitTestingAspNetCore6WebAPI

Fully functioning sample code for my Unit Testing an ASP.NET Core 6 Web API course
C#
44
star
19

HttpClientInNetCore

Fully functioning finished sample code for my Using HttpClient to Consume APIs in .NET Core course
C#
42
star
20

AspNetCoreAsyncBestPractices

Demo code for my "Best Practices for Building Async APIs with ASP.NET Core" session
C#
39
star
21

APIAspNetCore_Course

Starter files and fully functioning finished sample for my Building Your First API with ASP.NET Core course
C#
39
star
22

DealingWithCredentialsWhenSecuringAspNetCore3

Starter files and fully finished sample code for my Dealing with Credentials when Securing an ASP.NET Core 3 Application course at Pluralsight.
C#
32
star
23

BlazorAuthenticationAndAuthorization

Fully functioning finished sample code for my Authentication and Authorization in Blazor Applications course
C#
31
star
24

SecuringMicroservicesAspNetCore

Fully functioning starter & finished sample code for my "Securing Microservices in ASP.NET Core 3" course
C#
30
star
25

CSharpDesignPatterns

Fully functioning sample code for my C# Design Patterns course, currently targeting .NET 8.
C#
29
star
26

UsingHttpClientInDotNet

Fully functioning finished sample code for my Using HttpClient to Consume APIs in .NET course
C#
28
star
27

AspNetCoreAsyncAPI

Fully async version of the CityInfo demo project from my Building Your First API with ASP.NET Core course at Pluralsight. Many thanks to @petersantiago for this!
C#
27
star
28

SecuringBlazorClientSideApplications

Fully functioning sample application accompanying my Securing Blazor Client-side Applications course.
C#
26
star
29

ApiSecurityInDepth

Sample implementing reference token support, token exchange, impersonation and delegation and enhanced client authentication with private key JWTs
C#
26
star
30

HttpCache

Marvin.HttpCache is a complete implementation of the RFC2616 Http Caching standard for use with HttpClient, for all .NET Framework platforms (Windows Store, Windows Phone, WPF, WinForms, Console apps). Have a look around for the current status and to know what's yet to be implemented.
C#
26
star
31

BuildingAnODataAPIAspNetCore

Building an OData API in ASP.NET Core (.NET 5 and .NET Core 3.1)
C#
25
star
32

SecuringBlazorServerSideApplications

Fully functioning sample application accompanying my Securing Blazor Server-side Applications course.
C#
24
star
33

BlazorWASMSecurityBestPractices

Sample showing a best practice security approach for Blazor WASM apps via the BFF pattern
C#
23
star
34

DocumentingAspNetCore6API

Starter files & fully functioning finished sample code for my Documenting an ASP.NET Core 6 Web API using Swagger course.
C#
23
star
35

TestingWithEFCore

Fully functioning finished sample code for my Testing With EF Core course
C#
22
star
36

UnitTestingAspNetCoreWebApi

Fully functioning sample code for my Unit Testing an ASP.NET Core Web API course, over at Pluralsight, currently targeting .NET 8.
C#
22
star
37

DevelopingAsyncWebAPIAspNetCore6

Starter and fully finished sample solution for my Developing an Asynchronous Web API with ASP.NET Core 6 course, over at Pluralsight.
C#
21
star
38

BuildingAspNetCore7MinimalAPIs

Fully functioning sample code for my "Building ASP.NET Core 7 Minimal APIs" course
C#
19
star
39

CleanCodeResultFilterSample

Sample of using ResultFilter and ValueTuple when building an API with ASP.NET Core
C#
18
star
40

SecuringAspNetCoreWithOAuth2andOIDC

Starter files & fully functioning finished sample code for my Securing ASP.NET Core with OAuth2 and OpenID Connect course, currently targeting .NET 8.
C#
18
star
41

AspNetCoreWebAPIDeepDive

Fully functioning sample and starter files for my ASP.NET Core Web API Deep Dive course, currently targeting .NET 8.
C#
16
star
42

ReflectionInCSharp

Fully functioning sample for my "Using Reflection in a C# Application: Best Practices" course at Pluralsight
C#
15
star
43

GeneratingCodeAndTestingAspNetCoreApis

Fully functioning finished sample code for my Using Swagger / OpenAPI for Generating Client Side Code and API Tests in ASP.NET Core course
C#
15
star
44

AccessingAPIsWithHttpClientDotNet6

Fully functioning sample and starter files for my Accessing APIs with HttpClient in .NET 6 course
C#
14
star
45

XamarinFormsOIDCSample

Xamarin Forms OAuth 2.0 / OpenID Connect Sample with IdentityServer3
C#
14
star
46

UnitTestingAspNetCoreMVC

Fully functioning sample code for my Unit Testing an ASP.NET Core MVC Web Application course, over at Pluralsight, currently targeting .NET 8.
C#
10
star
47

UnitTestingAspNetCore6MVC

Fully functioning sample code for my Unit Testing an ASP.NET Core 6 MVC Web Application course
C#
10
star
48

ConsumingAnODataAPI

Fully functioning starter and finished sample solution for my Consuming an OData v4 API course
C#
10
star
49

BuildingAspNetCoreMinimalApis

Starter files & fully functioning finished sample code for my Building ASP.NET Core Minimal APIs course, currently targeting .NET 8.
C#
10
star
50

CSharp10Reflection

Finished sample code for my C# 10 Reflection course
C#
9
star
51

HttpClientInDepth

Code for my "An in-depth look at HttpClient" session at Techorama 2019
C#
9
star
52

Sprotify_Demo

Demo project for the ASP.NET Core DevStories sessions (building an API, web app & securing it).
CSS
9
star
53

ODataV4_Course

Building a Consistent RESTful API with OData V4 in ASP.NET starter files
C#
9
star
54

JsonPatch.Dynamic

Support for dynamically typed objects for Marvin.JsonPatch (Json Patch Document RFC 6902 implementation for .NET)
C#
8
star
55

SecurityPatternsForMicroservices

Code samples: various security patterns for microservices, with Ocelot as API Gateway and Duende.IdentityServer as IDP.
C#
8
star
56

AccessingApisWithHttpClient

Fully functioning finished sample and starter files for my Accessing APIs with HttpClient in .NET course, currently targeting .NET 8.
C#
8
star
57

AuthNZinBlazor

Starter files & fully functioning finished sample code for my Authentication and Authorization in ASP.NET Core Blazor course, currently targeting .NET 8.
HTML
6
star
58

ExtendingOAuthAndOpenIdConnect

Code for my session on extending OAuth & OpenId Connect. Contains samples for private key JWT, token encryption, proof of possession access tokens, singed authorization requests and more.
C#
6
star
59

JsonPatchInAspNet5

Example of how to use JsonPatch in ASP .NET 5 (with ASP .NET 5 Web API, ASP .NET MVC 5 and 6 clients)
PowerShell
6
star
60

OAuth2OIDCAngularASPNET_Course

These are the starter files for my "OAuth2 and OpenID Connect Strategies for Angular and ASP.NET" course at Pluralsight.
C#
6
star
61

DevelopingAsynchronousAspNetCoreWebApi

Fully functioning sample code for my Developing an Asynchronous ASP.NET Core Web API, over at Pluralsight, currently targeting .NET 8.
C#
6
star
62

Marvin.Polly.Cache.HttpCache

Cache provider for Polly supporting the Http Cache standard
C#
5
star
63

DocumentingAspNetCoreWebApiUsingSwagger

Starter files & fully functioning finished sample code for my Documenting an ASP.NET Core Web API using Swagger course, currently targeting .NET 8.
C#
5
star
64

SecuringAspNetCoreWithOAuth2andOIDC_Course

Starter demo files for my Securing ASP.NET Core with OAuth2 and OpenID Connect course
C#
5
star
65

AngularOIDC_Techorama2016

Demo code for my Techorama 2016 session: "Angular, OAuth2 and OpenID Connect, Heaven or Hell?"
JavaScript
5
star
66

TwoStepsAuthenticator.NETStandard

Port of https://github.com/glacasa/TwoStepsAuthenticator by https://github.com/glacasa to .NET Standard 1.6.
C#
4
star
67

Techorama_2017

Demo code for my "REST - Just the Hard Parts" session at Techorama 2017
C#
2
star
68

CSharpReflection

Fully functioning sample application accompanying my C# Reflection course, over at Pluralsight, currently targeting .NET 8.
C#
2
star
69

SigningAndEncryption

Samples for signing and encrypting tokens, generating a jwkset, ... in ASP.NET Core
C#
1
star