Edit on GitHub

HTTP Caching

ServiceStack’s HTTP Caching support transparently improves the behavior of existing ToOptimized Cached Responses, provides a typed API to opt-in to HTTP Client features, introduces a simpler declarative API for enabling both Server and Client Caching of Services and also includes Cache-aware clients that are able to improve the performance and robustness of all existing .NET Service Clients - functionality that’s especially valuable to bandwidth-constrained Xamarin.iOS / Xamarin.Android clients offering improved performance and greater resilience.

The new caching functionality is encapsulated in the new HttpCacheFeature plugin that’s pre-registered by default and can be removed to disable the new HTTP Caching behavior with:

Plugins.RemoveAll(x => x is HttpCacheFeature);

Caching options in ServiceStack include using the existing ToOptimizedResult* API’s to create Server Caches within your Services as well as returning a customized HttpResult to take advantage of HTTP Caching Client features.

CacheResponse Attribute

The new declarative [CacheResponse] Request Filter attribute provides the best of both worlds supporting both Server and HTTP Client features which is both non-invasive and simple to enhance, as a result we expect it to be the most popular option for adding caching to your Services in future.

Cache-Aware Clients

The Server Caching and HTTP Caching features in ServiceStack will automatically benefit Websites as browsers have excellent support for HTTP Caching. But .NET Desktop Apps or Xamarin.iOS and Xamarin.Android mobile clients wont see any of these benefits since none of the existing Service Clients have support for HTTP Caching. To complete the story we’ve also developed cache-aware Service Clients that can be used to enhance all existing .NET Service Clients which manages its own local cache as instructed by the HTTP Caching directives.

Server Caching

To explain the new HTTP Caching features we’ll revisit the ServiceStack’s previous caching support which enables what we refer to as Server Caching where the response of a Service is cached in the registered Caching Provider by calling the ToOptimizedResult* API’s which lets you programmatically construct the Cache Key and/or how long the cache should be persisted for, e.g:

public class OrdersService : Service
{
    public object Any(GetCustomers request)
    {
        //Good candidates: request.ToGetUrl() or base.Request.RawUrl
        var cacheKey = "unique_key_for_this_request";
        var expireCacheIn = TimeSpan.FromHours(1);

        return Request.ToOptimizedResultUsingCache(Cache, cacheKey, expireCacheIn, 
            () => {
                //Delegate executed only if item doesn't already exist in cache 
                //Any response DTO returned is cached on subsequent requests
            });
    }
}

As the name indicates this stores the most optimal representation of the Service Response in the registered ICacheClient, so for example if a client called the above API with Accept: application/json and Accept-Encoding: deflate, gzip HTTP Request Headers ServiceStack would store the deflated serialized JSON bytes in the Cache and on subsequent requests resulting in the same cache key would write the compressed deflated bytes directly to the Response OutputStream - saving both CPU and bandwidth.

More Optimal OptimizedResults

But there’s an even more optimal result we could return instead: Nothing :) and save even more CPU and bandwidth! Which is precisely what the HTTP Caching directives built into the HTTP spec allow for. To enable it you’d need to return additional HTTP Headers to the client containing the necessary metadata they can use to determine when their locally cached responses are valid. Typically this is the Last-Modified date of when the Response/Resource was last modified or an Entity Tag containing an opaque string the Server can use to determine whether the client has the most up-to-date response.

The most optimal cache validator we can use for existing ToOptimizedResult* API’s is the Last Modified date which is now being cached along with the response. An additional instruction we need to return to the client is the Cache-Control HTTP Response Header which instructs the client what caching behavior to apply to the server response. As we want the new behavior to work transparently without introducing caching issues to existing Services, we’ve opted for a conservative:

Cache-Control: max-age=0

Which tells the client to treat the server response as immediately stale and that it should send another request to the Server for identical requests, but this time the client will append a If-Modified-Since HTTP Request Header which ServiceStack now automatically looks up to determine if the cache the client has is valid. If no newer cache for this request has been created since, ServiceStack returns a 304 NotModified Response with an empty Request Body which tells the client it’s safe to use their local cache instead.

Modify Cache-Control for OptimizedResults

You can change the Cache-Control Header returned for existing ToOptimizedResult responses by modifying the pre-registered HttpCacheFeature, e.g:

var cacheFeature = this.GetPlugin<HttpCacheFeature>();
cacheFeature.CacheControlForOptimizedResults = "max-age=3600, must-revalidate";

This tells the client that they can treat their locally cached server responses as valid for 1hr
but after 1hr they must check back with the Server to determine if their cache is still valid.

HTTP Client Caching

The Caching features above revolve around enhancing existing Server Cached responses with HTTP Caching features to further reduce latency and save CPU and bandwidth resources. In addition to Server Caching pure stand-alone HTTP Caching features (i.e. that don’t cache server responses) are now available as first-class properties on HttpResult:

// Only one Cache Validator below needs to be specified:
string ETag            // opaque string representing integrity of response
DateTime? LastModified // the last time the response was Modified

// Specify Client/Middleware Cache Behavior
TimeSpan? MaxAge          // How long cached response is considered valid
CacheControl CacheControl // More options to specify Cache-Control behavior:
enum CacheControl {
    None,
    Public,
    Private,
    MustRevalidate,
    NoCache,
    NoStore,
    NoTransform,
    ProxyRevalidate,
}

// Available, but rarely used
TimeSpan? Age      // Used by proxies to indicate how old cache is
DateTime? Expires  // Less preferred alternative to MaxAge

We’ll walk through a couple of real-world examples to show how we can make use of these new properties and explain the HTTP Caching behavior they enable.

Using ETags

The minimum properties required for HTTP Caching is to specify either an ETag or LastModified Caching Validator. You can use any opaque string for the ETag that uniquely represents a version of the response that you can use to determine what version the client has, which could be an MD5 or SHA Hash of the response but can also be a unique version string. E.g. Adding a RowVersion property to your OrmLite POCO Data Models turns on OrmLite’s Optimistic Concurrency feature where each time a record is modified it’s automatically populated with a new version, these characteristics makes it ideal for use as an ETag which we can just return as a string:

public object Any(GetCustomer request)
{
    var response = Db.SingleById<Customer>(request.Id);    
    return new HttpResult(response) {
        ETag = response.RowVersion.ToString(), 
    };
}

Whilst this is the minimum info required in your Services, the client also needs to know how long the cache is valid for as typically indicated by the MaxAge property. If it’s omitted ServiceStack falls back to use the HttpCacheFeature.DefaultMaxAge of 10 minutes which can be changed in your AppHost with:

this.GetPlugin<HttpCacheFeature>().DefaultMaxAge = TimeSpan.FromHours(1);

So the HTTP Response Headers the client receives when calling this Service for the first time is something similar to:

ETag: "42"
Cache-Control: max-age=600

Which tells the client that they can use their local cache for identical requests issued within the next 10 minutes. After 10 minutes the cache is considered stale and the client will issue a new request to the server but this time it will include the ETag it has associated with the Response, i.e:

If-None-Match: "42"

When this happens the Service is still executed as normal and if the Customer hasn’t changed, the HttpCacheFeature will compare the HttpResult.ETag response with the clients ETag above and if they match ServiceStack will instead return a 304 NotModified with an Empty Response Body to indicate to the client that it can continue to use their locally cached response.

So whilst using a HttpResult doesn’t cache the response on the Server and save further resources used in executing the Service, it still benefits from allowing the client to use their local cache for 10 minutes - eliminating server requests and yielding instant response times. Then after 10 minutes the 304 NotModified Response Status and Empty Body improves latency and saves the Server CPU and bandwidth resources it didn’t have to use for serializing and writing the executed Services response it would have need to do if no Caching was enabled.

Using LastModified

The alternative to using an ETag is to use the Last-Modified Caching Validator. When you’re constructing a complex response you’ll want to use the most recent Last Modified Date from all sources so that you can determine that the cache is no longer valid when any of the sources have been updated.

If you also want to customize the clients Cache-Control behavior you can use the additional HttpResult properties, below is an example of doing both:

public object Any(GetCustomerOrders request)
{
    var response = new GetCustomerOrdersResponse {
        Customer = Db.SingleById<Customer>(request.Id),
        Orders = Db.Select<Order>(x => x.CustomerId == request.Id),
    };

    var allDates = new List<DateTime>(response.Orders.Select(x => x.ModifiedDate)) {
        response.Customer.ModifiedDate,
    };

    return new HttpResult(response)
    {
        LastModified = allDates.OrderByDescending(x => x).First(),
        MaxAge = TimeSpan.FromSeconds(60),
        CacheControl = CacheControl.Public | CacheControl.MustRevalidate,
    };
}

Which returns the Last Modified Date of the Customer record or any of their Orders as well as the customized Cache-Control Header which together returns Response Headers similar to:

Last-Modified: Fri, 19 April 2016 05:00:00 GMT
Cache-Control: max-age=60, public, must-revalidate

Then after 60 seconds have elapsed the client will re-issue a request but instead of sending a If-None-Match Request Header and ETag, instead sends If-Modified-Since and the Last-Modified date:

If-Modified-Since: Fri, 19 April 2016 05:00:00 GMT

The resulting behavior is identical to that of the ETag above but instead compares the LastModified dates instead of ETag strings for validity.

Short-circuiting HTTP Cache Validation

A further optimization that can be added to the HTTP Cache workflow is using IRequest.HasValidCache() to short-circuit the execution of a Service after you’ve processed enough information to determine either the ETag or LastModified for the response.

For example if you had a Service that transcodes video on-the-fly, you can use Request.HasValidCache() to check whether the client already has the latest version of the video, if it does we can return a 304 NotModified result directly, short-circuiting the Service and saving any resources in executing the remainder of the implementation, which in this case would bypass reading and transcoding the .mp4 video:

public object Any(GetOggVideo request)
{
    var mp4Video = VirtualFileSources.GetFile($"/videos/{request.Id}.mp4");   
    if (mp4Video == null)
        throw HttpError.NotFound($"Video #{request.Id} does not exist");
    
    if (Request.HasValidCache(mp4Video.LastModified))
        return HttpResult.NotModified();
    
    var encodedOggBytes = EncodeToOggVideo(file.ReadAllBytes());
    return new HttpResult(encodedOggBytes, "video/ogg") 
    {
        LastModified = mp4Video.LastModified,
        MaxAge = TimeSpan.FromDays(1),
    };
}