Giter VIP home page Giter VIP logo

cachecow's Introduction

NuGet version Build status

CacheCow

An implementation of HTTP Caching in .NET Core and 4.52+ for HttpClient, ASP.NET Web API, ASP.NET MVC Core and Carter.

This document covers topics below:

NOTE: Breaking changes in Version 2.0.0

There are quite a few changes in the server-side implementation of CacheCow due to shortcomings of previous approach while the client-side mostly remained the same. Hence if you have used CacheCow.Server in the past and you would like to migrate, read the guide on migration further down. CacheCow.Client has some changes which may or may not break your code (depending whether you had used .NET 4.5 packages) which includes:

  • All interfaces are fully async now
  • Many cache storages no longer maintained and available including: MongoDb, Memcached, File and Elasticsearch. Currently only Redis, SQL and InMemory are supported although I accept PR if you would like to add your storage.
  • ICacheStore implementations optionally supported a cache quota management interface which is no longer supported.
  • Renaming CacheCow client header from "x-cachecow" to "x-cachecow-client"
  • CacheCow.Client now copies any headers starting with x-cachecow it receives from the server to the response it delivers to your HttpClient.

Concepts and definitions

It is useful to start with concepts but feel free to use it as a reference. If you get bored reading this ๐Ÿ˜€, jump straight to Getting Started sections or simply browse and run a couple of samples.

  • CacheCow.Client: Client constructs for HTTP Caching when making HTTP calls with .NET's HttpClient.
  • CacheCow.Server: Server constructs for HTTP Caching when serving HTTP APIs in ASP.NET Web API or MVC Core.
  • Resource: A RESTful resource - generally identified by its URI.
  • Representation: A particular embodiment of a resource according to its format, language, etc. For example /api/car/1 is a resource and it might have two representations as JSON or XML. Also the same resource could have two representations in Chinese or Spanish. Headers by which representations will vary is defined by the Vary header from the server.
  • HTTP Caching: Caching as described in HTTP 1.1 Specification (RFC 7230 or 2616). Despite the common notion, the representations get cached on the client (not server) and the server is responsible for providing cache directives and validate conditional requests on resources.
  • ICacheStore: CacheCow interface responsible for storing cacheable representation on the client. There are choice of storages, at this point InMemory and Redis are available. CacheCow itself is responsible for storing representations separately.
  • Cache directives: These are various cache-related headers that the server provides to guide the client on how best cache and validate the resources. These headers include Cache-Control, Vary, ETag and in HTTP 1.0 would include Expires, Last-Modified and Pragma (for the purpose of this article, we include ETag and Last-Modified as part of directives although purely speaking they are not). CacheCow has ICacheDirectiveProvider interface responsible for controlling these headers.
  • Expires header: Defines expiry of a resource in absolute time. Cache-Control header provides richer semantic and supersedes it. CacheCow sets both to support both HTTP 1.0 and 1.1.
  • Expiry: Expiry of a cached representation on the client is defined by the directives sent by the server. It is crucial to note that the client normally does not eject the cached representation right after the expiry - the client can carry on using the cached representation provided it checks with the server if the representation is still valid (see Consistency below).
  • Last-Modified header: Since its precision is at seconds, its use is not recommended for valiation and instead, it is recommended to use ETag.
  • ETag header: ETag or EntityTag is an opaque string that identifies a version of resource (not representation). If you have a high-precision Last-Modified date, it is better to turn it to ETag by binarising the date (example).
  • TimedETag: A CacheCow construct that combines Last-Modifed and ETag (can have either of them but not both) and represents a version of the resource and implemented as TimedEntityTagHeaderValue. It is recommended to construct it with an ETag (due to low precision of DateTime in HTTP Spec's Last-Modified header).
  • ITimedETagExtractor: A CacheCow interface responsible for extracting TimedETag from view models sent back by the API. By default, it checks to see if the ViewModel has implemented ICacheResource, if so it enquires the TimedETag directly. If not, it will resort to serialising the ViewModel to JSON and use its hash as ETag. This can be expensive hence it is suggested to either your ViewModels implementing ICacheResource interface or implement ITimedETagExtractor - plenty of examples in the samples.
  • Consistency: Resources underlying the cached representationa could change without any notice making them invalid - this problem has been half-joking known as one of the two most difficult problems in computer science, the other being naming. For many resources this could be acceptable while for some simply not tolerable. HTTP solves this problem with a single round-trip call that would either approve the use of the version client has cached (by returing status 304) or return the latest version of the resource in the representation requested. This is known as conditional GET. HTTP also provides means for optimistic concurrency and only update (or delete) of the resource if it matches the ETag or has not changed since the Last-Modified date supplied. This is known as conditional PUT/PATCH/DELETE. CacheCow supports these scenarios OOB.
  • Zero-Expiry resources (consistent resources): These resources require a high consistency and the clients are allowed to cache the representations and re-use only if they make a conditional GET to confirm the version they have is valid. Essentially, the representation is expired as soon as it leaves the server but can be safely re-used if the client makes a conditional GET. It might seem that caching in these scenarios is not really beneficial, but in fact it helps to reduce network traffic, client and server resource usage and even can protect back-end systems. The trick on the server is to just find out whether a resource has changed without loading it all the way from back-end systems. For example, to know whether a record has been changed, we can check whether its LastModifiedDate (or Timestamp) has been modified against the ETag or a date. Or for a list of records, the most recent LastModifiedDate of all records along with the count can be used which can be executed in a single query (For example in SQL you would use SELECT COUNT(1), MAX(LastModifiedDate) FROM MyTable). Such queries will be fast and cheap. CacheCow provides ITimedETagQueryProvider interface to preemptively query the backend stores for conditional HTTP calls without load the resources.
  • ITimedETagQueryProvider: This interface allows server implementations to query their back-end and carry out validation against it. This is the best way to have APIs support consistency and the most efficient level of caching.

Getting started - Client

Client scenario is perhaps the most common use case of CacheCow. Most of the concepts discussed above relate to the server-side. Client-side CacheCow has been implemented as a DelegatingHandler and has very few concept counts - most of the complexity of HTTP Caching has been hidden away from you. For the purpose of this guide, we choose an In-Memory storage which is the default.

1) Install the nuget package

> install-package CacheCow.Client

2) Use ClientExtensions to create an HttpClient (piped to a CachingHandler fronted by HttpClientHandler):

var client = ClientExtensions.CreateClient();

This is simply a helper and you saves you writing a couple of lines of code.

3) Make two calls to a cacheable resource

JQuery CDN is a handy little cacheable resource. We make a call twice and check CacheCow header:

const string CacheableResource = "https://code.jquery.com/jquery-3.3.1.slim.min.js";
var response = client.GetAsync(CacheableResource).
      ConfigureAwait(false).GetAwaiter().GetResult();
var responseFromCache = client.GetAsync(CacheableResource).
      ConfigureAwait(false).GetAwaiter().GetResult();
Console.WriteLine(response.Headers.GetCacheCowHeader().ToString()); // outputs "2.0.0.0;did-not-exist=true"
Console.WriteLine(responseFromCache.Headers.GetCacheCowHeader().ToString()); // outputs "2.0.0.0;did-not-exist=false;retrieved-from-cache=true"

As you can see, second time round the resource came from the cache and the request did not even hit the network.

NOTE: In-Memory storage is OK for test scenarios or cases where the load is limited. In many cases you would choose to use Redis storage or you can implement your own if you need to. If you would need an alternative storage not yet supported, feel free to discuss by opening an issue before sending a PR.

Getting started - ASP.NET MVC Core

From CacheCow 2.0, ASP.NET MVC Core scenarios are supported. Server-side CacheCow has been implemented as a Resource Filter.

1) Add the nuget package:

> install-package CacheCow.Server.Core.Mvc

2) Add HTTP Caching dependencies:

public virtual void ConfigureServices(IServiceCollection services)
{
    ... // usual startup code
    services.AddHttpCachingMvc(); // add HTTP Caching for Core MVC
}

3) Decorate your Controller's actions with HttpCacheFactory attribute

Provide the expiry as the first parameter (number of seconds):

public class MyController : Controller
{
    [HttpGet]
    [HttpCacheFactory(300)]
    public IActionResult Get(int id)
    {
        ... // implementation
    }
}

Here we have set the expiry to 5 minutes. This covers the basic scenario, browse the samples for the advanced and efficient use cases.

Getting started - ASP.NET Web API

Web API has always been supported by CacheCow but the server-side has been radically changed. There is no more a DelegatingHandler and all you need is to decorate your actions with the HttpCache filter.

1) Add the nuget package:

> install-package CacheCow.Server.WebApi

2) Decorate your Controller's actions with HttpCache attribute

Provide the expiry as a parameter (number of seconds):

public class MyController : ApiController
{
    [HttpGet]
    [HttpCache(DefaultExpirySeconds = 300)]
    public IHttpActionResult Get(int id)
    {
        ...
    }
}

Here we have set the expiry to 5 minutes. This covers the basic scenario, browse the samples for the advanced and efficient use cases.

CacheCow Header

CacheCow.Client and CacheCow.Server variants include diagnostic headers (x-cachecow-client and x-cachecow-server) to inform you of the actions taken and their results. They are useful in debugging and in case you would like to log them to understand cache hit ratios.

CacheCow.Client Header

The header name is x-cachecow-client and can optionally contain extensions below (values separated by semicolon) depending on the scenario:

  • Version of the CacheCow.Client
  • was-stale: whether the stored representation was stale
  • did-not-exist: the requested representation did not exist in the cache
  • not-cacheable: the request or response were not cacheable (due to status, various headers and directives)
  • cache-validation-applied: CacheCow attempted the HTTP call (GET, PUT, PATCH or DELETE) with cache validation
  • retrieved-from-cache: whether the representation ultimately came from the cache or the one sent by the server

CacheCow.Server Header

The header name is x-cachecow-server and contains extensions below (values separated by semicolon):

  • validation-applied: whether validation was attempted
  • validation-matched: validation attempted and the conditions met (resulting in 304 for GET and 2xx for PUT/PATCH/DELETE)
  • short-circuited: the request was short-circuited and did not hit deeper API layers (with status 304 or 412)
  • query-made: ITimedETagQueryProvider made a query to the back-end stores

Running Samples

CacheCow project contains 3 sample projects that demonstrate how to use both client and server libraries. The samples are exactly similar in functionality, shared by CacheCow.Samples.Common project. Server is an API hosting functionality for adding, modifying and querying cars. it a command-line interface with 6 options to choose from:

  • A. To return all cars
  • L. To return the last car (default is JSON)
  • X. To return the last car in XML
  • C. To create a new car
  • U. To update the last car
  • O. To update the last car - outside the API by modifying underlying data so the API does not get to see it
  • D. To delete the last car
  • F. To delete the first car
  • V. To toggle on/off verbose output of headers
  • Q. to quit

Samples screenshot

After choosing options A, L or X application prints the value of the CacheCow header from both client and the server. These values will denote the caching actions taken and their result.

You can test and try different scenarios. For example:

  • A, C, A, A to test the impact of creating a new car on the car collection's GET
  • C, C, A, A, F, A to test the impact of deleting a car on on the car collection's GET
  • C, C, L, L, U, L to test the caching header changes on subsequent requests and after updating an item
  • C, L, L, X, U, L, X to see that the representations of the same resource (in JSON and XML) get cached separately but invalidated together (after the update)
  • C, C, C to add a few cars and then L, O, U to see an unsuccessful HTTP PUT/PATCH/DELETE validation resulting in status 412

CacheCow.Samples.MvcCore

You can run this sample on Windows, Mac and Linux and requires .NET Core +2.0. Essentially in your shell of your choice cd to the CacheCow.Samples.MvcCore folder and type:

dotnet run

CacheCow.Samples.WebApi

This is a simple Web API example that displays out-of-the-box features for Web API. This sample is in .NET 4.52 and you can build and run as a console app on Windows.

CacheCow.Samples.WebApi.WithQueryAndIoc

This is an advanced Web API example that displays advanced features of server-side CacheCow, especially IoC. This sample is in .NET 4.52 and you can build and run as a console app on Windows.

CacheCow.Samples.Carter

This sample is for a typical Carter implementation. You can run this sample on Windows, Mac and Linux and requires .NET Core +2.0. Essentially in your shell of your choice cd to the CacheCow.Samples.Carter folder and type:

dotnet run

CacheCow.Server advanced options

Scenarios in the Getting-Started sections above choose simple out-of-the-box options to get you started. Depending on the load on your server, these are not necessarily the optimal. To get the best out of your API's caching, you would have to do a little more work and help CacheCow optimise HTTP Caching. By default, CacheCow server relies on Serialising your payloads/viewmodels to generate ETag. While for low-mid traffic scenarios this could be sufficient, it would be detrimental for high-load APIs or cases where your payload is big. That is why, instead of leaving CacheCow to generate ETag (rather TimedETag) by serialisation, you could supply it yourself.

There are two times when a TimedETag is needed:

  • when serving the viewmodel
  • when carrying out validation (conditional GET/PUT/PATCH/DELETE)

TimedETag when serving the ViewModel

TimedETag needs to be included in the response headers (in the form of ETag or Last-Modified headers). If your view models implement ICacheResource, CacheCow will attempt to get TimedETag by calling interface's only method. Otherwise it will use serialisation unless you provide an alternative ITimedETagExtractor implementation that extracts the TimedETag. And example would be an implementation that uses LastModifiedDate field and turns it into an ETag by binarisation (example here).

TimedETag when carrying out validation

This is the preemotive validation of the resource in response to conditional GET (or PUT/PATCH/DELETE). In case of a conditional GET, client requests for a later version of the resource unless it has changed since it has had its version, providing its last modified date or ETag(s). In this case, by default, CacheCow allows the call to controller to load the view model and then generates its TimedETag (by querying ICacheResource or serialisation). If the version the client has is still the most recent, it will send back status 304 or NotModified. While this reduces network traffic and reduces server (and client) resource usage, it does not relieve pressure from your back-end services. That is where ITimedETagQueryProvider interface comes into play: by implementing this interface you could go back to your back-end store and check whether the condition is met without loading the whole view model from the back-end services. For example, you could go back to the record requested and check if the LastModifiedDate matches.

This table highlights different options in CacheCow.Server and value associated with each. Benefits of different CacheCow.Server Approaches

Dependency Injection scenarios on ASP.NET Core

ASP.NET Core can already integrate with Dependency Injection frameworks and supports resolving its own dependencies through such abstractions. One of the challenges with server-side CacheCow is that there interfaces such as ITimedETagExtractor or ITimedETagQueryProvider would have implementations that would be different for different resources (rather view models). For example, if an API serves 3 entities as Customer, Product and Order you would need 6 different implementations, one for each entity and one for each collection (e.g. IEnumerable<Customer>). It would be certainly cleaner to have one implementation per each and somehow know the view model type of each action. Looking at the return type is an option but quite commonly actions return IActionResult.

So the solution is to let the filter on the action define the type of the view model. Hence, for example, on a CarController's Get action, you would define ViewModelType in the attribute as below:

public class CarController : Controller
{
    [HttpGet]
    [HttpCacheFactory(300, ViewModelType = typeof(Car))]
    public IActionResult Get(int id)
    {
        ... // implementation
    }
}

This will help CacheCow to know that it should look for ITimedETagExtractor<Car> and you would create an implementation for ITimedETagExtractor<Car> and register it on your DI container.

The same applies to ITimedETagQueryProvider<T>, essentially: 1) define ViewModelType on filter 2) implement generic interfaces 3) register them in your container

Configuration options with ASP.NET Core

CacheCow uses Options Pattern to allow consumers to configure a few options at the time of using AddHttpCachingMvc. Currently you can control whether to emit CacheCow diagnostic header (for now default is true) and also whether respect configuration:

services.AddHttpCachingMvc(options =>
{
    options.EnableConfiguration = true; // default is false
    options.DoNotEmitCacheCowHeader = false; // default is true
});

If you enable configuration (using EnableConfiguration = true), CacheCow will try to read expiry values from .NET Core's IConfiguration. For example using the appsettings.json below, you can override the settings defined in the attribute:

{
    "CacheCow": 
    {
      "Test": 
      {
        "Get": 
        {
          "Expiry": "00:00:10"
        }
      }
    }
}

Example below assumes a controller of "Test" and action of "Get" and it sets the cache expiry to 10 seconds. The value of controller and action are picked up from the route data and the keys need to be defined under "CacheCow" entry.

As per .NET Core configuration, you can override file configuration values with environment variables or command line arguments.

At this point, there is also "Enabled" property that can be set to false to disable CacheCow for a particular controller/action - something that might be handy during development. For example, setting environment variable "CacheCow__Test__Get__Enabled" to "false" will turn off CacheCow functionality on action Get of controller Test.

Dependency Injection scenarios on ASP.NET Web API

You should first register default types in your Web API Dependency Resolver:

// registering in a Windsor Castle container
CachingRuntime.RegisterDefaultTypes((
    (t1, t2, isTransient) =>
    {
        if (isTransient)
            container.Register(Component.For(t1).ImplementedBy(t2).LifestyleTransient());
        else
            container.Register(Component.For(t1).ImplementedBy(t2).LifestyleSingleton());
    }));

The rest to use ITimedETagExtractor<T> and ITimedETagQueryProvider<T> is similar to ASP.NET Core: simply define ViewModelType and register your dependencies.

Migrating older CacheCow.Server projects to the new CacheCow.Server.Core.Mvc or CacheCow.Server.WebApi

Almost all projects using CacheCow.Client would carry on working in version 2.0. But servider-side implementation has been radically changed and in many ways simplified. In order to migrate:

  • Remove CachingHandler delegating handler
  • Remove any storage since there is no need for a storage anymore
  • Decorate your actions with [HttpCache] attribute
  • For optimising caching see above
  • For Dependency Injection options see here

cachecow's People

Contributors

alexzeitler avatar aliostad avatar aliostad-asos avatar damienbod avatar diverdw avatar evo-terren avatar idisposable avatar imperugo avatar lostdev avatar lxgaming avatar petegoo avatar pietervdvn avatar prajon84 avatar reyhn3 avatar ronnelsantiago avatar sayedihashimi avatar tugberkugurlu avatar wrathza avatar zhofre avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

cachecow's Issues

Making Helpers internal

I still think that some of the classes and helpers inside the CacheCow.Common project should be internal. Reasons:

  • If the helpers methods don't provide a use for the library's public API, they shouldn't be exposed as public.
  • If the helpers methods are out of this library's scope, they should stay as internal.

If you think that they will be useful to end user, I would suggest to have them inside another project which is meant to be a common library for .NET.

Also, if the helpers used by multiple projects, having them as a linked resource would be a better approach.

no-transform lost in HttpCacheControlPolicy

Dear aliostad,

I set CacheControlHeaderProvider to include no-transform by default,

            var cachingHandler = new CachingHandler
                                     {
                                         CacheControlHeaderProvider =
                                             new AttributeBasedCacheControlPolicy(
                                             new CacheControlHeaderValue
                                                 {
                                                     Private = true,
                                                     MustRevalidate = true,
                                                     NoTransform = true,
                                                     MaxAge = TimeSpan.Zero
                                                 })
                                             .GetCacheControl
                                     };

            globalConfiguration.MessageHandlers.Add(cachingHandler);

However, after applies HttpCacheControlPolicy on actions, the no-transform setting is no longer included in the response.

Is this an intended behaviour?

Regards

CachingHandler with CacheRefreshPolicyProvider throws incorrect HttpResponseException

If I enable to use CacheRefreshPolicyProvider, all HttpResponseException becomes user unhandled with status code 500. E.g. If I visit an endpoint which doesn't exist, http://localhost:36236/errererer/, it returns

500 
HttpResponseException: Processing of the HTTP request resulted in an exception. Please see the HTTP response returned by the 'Response' property of this exception for details.

instead of:

{
  "message": "No HTTP resource was found that matches the request URI 'http://localhost:36236/eereererer'.",
  "messageDetail": "No type was found that matches the controller named 'eereererer'."
}

Store Request/Resource URI in Entity Tag Store

Since we're using a Route Pattern that does not include querystrings it would be nice to be able to view the original request/resource URI within the entity tag store. In our case we just see several entries with the same Route Pattern but different ETags which makes it difficult to correlate client requests with the cache state.

I'd be happy to submit a pull request if you're happy to add this feature.

Wrong implementation in Revalidation and staleness logic

Currently in CacheCow.Client, if the resource goes stale, we remove the resource from cache and retrieved it again. This is wrong according to the spec.

In all cases cache should make a conditional GET.

I will try to fix it this weekend.

Method Dispose in type CacheCow.Server.EntityTagStore.SqlServer does not have an implementation

I'm getting

Method 'Dispose' in type 'CacheCow.Server.EntityTagStore.SqlServer.SqlServerEntityTagStore' from assembly 'CacheCow.Server.EntityTagStore.SqlServer, Version=0.4.1.0, Culture=neutral, PublicKeyToken=null' does not have an implementation.

The code i'm using in WebApiConfig is

var connectionString =                 ConfigurationManager.ConnectionStrings["Default"].ConnectionString;
var eTagStore = new SqlServerEntityTagStore(connectionString);
var cacheHandler = new CachingHandler(eTagStore);
config.MessageHandlers.Add(cacheHandler);

When using MemoryCaching

var cacheHandler = new CachingHandler();
config.MessageHandlers.Add(cacheHandler);

there was an error:

Could not load file or assembly 'System.Web.Http.WebHost, Version=4.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35' or one of its dependencies. The located assembly's manifest definition does not match the assembly reference. (Exception from HRESULT: 0x80131040)

but adding

<dependentAssembly>
        <assemblyIdentity name="System.Web.Http.WebHost"  publicKeyToken="31bf3856ad364e35" culture="neutral" />
        <bindingRedirect oldVersion="4.0.0.0" newVersion="5.0.0.0" />
      </dependentAssembly>   

solved the problem so i suspect some depending assembly is missing for the Sql caching?

Server CachingHandler changes caching directive

In AddCaching there is the following code,

           if (cacheControlHeaderValue.NoCache)
                    {
                        cacheControlHeaderValue.NoStore = true;

NoCache and NoStore don't mean the same thing so it is not valid for a caching layer to assume that because NoCache is true then NoStore should also be true. See [1]and [2]. Curiously in investigating this I realized that the CacheControlHeaderValue.NoCache is actually defined wrong. It should not be a Boolean value at all.

[1] http://www.squid-cache.org/mail-archive/squid-users/201201/0376.html
[2] http://tools.ietf.org/html/draft-ietf-httpbis-p6-cache-22#section-7.2.2.3

Make IEntityTagStore and ICacheStore Signatures Task Returning

Currently, IEntityTagStore and ICacheStore provide us to create synchronous stores. Methods of these interfaces should be Task returning so that they can be async or sync depending on the case. If we are making synchronous calls, we can just return a pre-completed Task objects. This way, there will be no overhead.

Cache-control information in 304 response

Hi Ali,

Should Cache-control, Expires, Vary and etc remain the same in the 304 response? It is quite strange to see the correct response in 200 OK (Cache-Control: no-transform, must-revalidate, max-age=0, private), then Cache-Control change to no-cache in 304.

This is not critical, but nice to have.

Thanks

CachingHandler and its order inside the pipeline

I just installed the CacheCow.Server.0.2.1 and add the following config:

public static void Configure(HttpConfiguration config) {

    config.IncludeErrorDetailPolicy = IncludeErrorDetailPolicy.LocalOnly;

    //Message Handlers
    config.MessageHandlers.Add(new RemoveServerHeaderMessageHandler());
    config.MessageHandlers.Add(new CachingHandler("Accept"));

    //...
}

Then I sent a request through the browser and got back the response successfully. The response headers are as below:

HTTP/1.1 200 OK
Cache-Control: no-transform, must-revalidate, max-age=604800, private
Content-Type: application/json; charset=utf-8
Last-Modified: Tue, 28 Aug 2012 08:56:43 GMT
ETag: "753efe001b284ccea6226b487fb63508"
Vary: Accept
X-AspNet-Version: 4.0.30319
X-SourceFiles: =?UTF-8?B?RTpcRHJvcGJveFxBcHBzXE12Y0Jsb2dneVxzcmNcTXZjQmxvZ2d5LldlYlxhcGlcYmxvZ3Bvc3Rz?=
X-Powered-By: ASP.NET
Date: Tue, 28 Aug 2012 09:00:07 GMT
Content-Length: 10223

When I send another request with the following headers:

GET /api/blogposts?take=2&page=1&lang=en HTTP/1.1
Host: localhost:32729
Connection: keep-alive
Cache-Control: max-age=0
User-Agent: Mozilla/5.0 (Windows NT 6.2; WOW64) AppleWebKit/537.1 (KHTML, like Gecko) Chrome/21.0.1180.83 Safari/537.1
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,/;q=0.8
Accept-Encoding: gzip,deflate,sdch
Accept-Language: en-US,en;q=0.8
Accept-Charset: ISO-8859-1,utf-8;q=0.7,*;q=0.3
Cookie: __RequestVerificationToken=r0W6R5pjjunBrJ6cxVZtoaMaUKOqz1Kj1UpkMdk6xQ29aFbgTy5zmmczAG7YjCfOzImk5h5Y3_X8cV-aWegjGNmF1qy2RmmBXHE1QNfPDUDKKvdagwHejxBxZU04InrWVUx8gTPCYuhSH-cMeXI6UQ2; ASP.NET_SessionId=qteniaqyo5rcgbf2pb3qarls
If-None-Match: "753efe001b284ccea6226b487fb63508"
If-Modified-Since: Tue, 28 Aug 2012 08:56:43 GMT

I got back the following error:

Object reference not set to an instance of an object.

Description: An unhandled exception occurred during the execution of the current web request. Please review the stack trace for more information about the error and where it originated in the code.

Exception Details: System.NullReferenceException: Object reference not set to an instance of an object.

Source Error:

An unhandled exception was generated during the execution of the current web request. Information regarding the origin and location of the exception can be identified using the exception stack trace below.

Stack Trace:

[NullReferenceException: Object reference not set to an instance of an object.]
System.Web.Http.WebHost.HttpControllerHandler.EndProcessRequest(IAsyncResult result) +112
System.Web.Http.WebHost.HttpControllerHandler.System.Web.IHttpAsyncHandler.EndProcessRequest(IAsyncResult result) +10
System.Web.CallHandlerExecutionStep.OnAsyncHandlerCompletion(IAsyncResult ar) +129

However, if I change the message handler order as below:

public static void Configure(HttpConfiguration config) {

    config.IncludeErrorDetailPolicy = IncludeErrorDetailPolicy.LocalOnly;

     //Message Handlers
     config.MessageHandlers.Add(new CachingHandler("Accept"));
     config.MessageHandlers.Add(new RemoveServerHeaderMessageHandler());

    //..
}

I got back what I needed:

Request:
GET http://localhost:32729/api/blogposts?take=2&page=1&lang=en HTTP/1.1
User-Agent: Fiddler
Host: localhost:32729
Accept: application/json
If-None-Match: "f190a3dd94d040edaeca73d8ba28947d"
If-Modified-Since: Tue, 28 Aug 2012 09:12:50 GMT

Response:
HTTP/1.1 304 Not Modified
Cache-Control: no-cache
Pragma: no-cache
Expires: -1
ETag: "f190a3dd94d040edaeca73d8ba28947d"
Server: Microsoft-IIS/8.0
X-AspNet-Version: 4.0.30319
X-SourceFiles: =?UTF-8?B?RTpcRHJvcGJveFxBcHBzXE12Y0Jsb2dneVxzcmNcTXZjQmxvZ2d5LldlYlxhcGlcYmxvZ3Bvc3Rz?=
X-Powered-By: ASP.NET
Date: Tue, 28 Aug 2012 09:13:26 GMT

Is this always the issue? should The CachingHandler be registered fist? If I have an authHandler, that would be a problem I think since I need the auth handler to be registered first.

Cache Cow Client Caching / ETag generation

A while ago I posted a question about handling cache invalidation in a hierarchical API.

I ended up overriding the CacheKeyGenerator and the LinkedRoutePatternProvider such that the generated route pattern was always a top level resource:

/api/blog/posts/10 --> /api/blog

This would ensure that a POST to the /api/blog/posts/10 would invalidate the cache for /api/blog/posts/10, api/blog/posts and /api/blog.

In order to do this I added a UriTrimmer that simply returned Uri.LocalPath.

After wiring up the SQL Server tag store today I was able to inspect the cache keys and noticed that due to my custom CacheKeyGenerator a request with a querystring would return the same ETag, for example:

GET /blog/posts?pageSize=10&page=1 and GET /blog/posts?pageSize=20&page=2 both return "509037a1ee5548749f1eab4b82e85750".

My first thought was that the Cache Cow client would end up returning the same page of results when paging, but this was not the case - everything worked as expected. Navigating to a new page would return a 200. Reissuing the request would return a 304.

Is this because of how the responses are handled on the client? I'm guessing unlike the server, the responses are cached based on the full request URL path and query, and since the client cache wouldn't have contained an entry in the cache, no If-None-Match header would be sent.

Despite everything "working" I'm starting to feel like using the same URL (and therefore cache key) for all resources is wrong. Surely I would be better off dropping my CacheKeyGenerator so that a different page of results would yield a different ETag?

Cache invalidation with multi-tenant resource heirarchy

Unfortunately cache invalidation is not working for any of our resources which I believe is simply down to our URI templates.

Our API is multi-tenant with each tenant being identified by the Authorization header.

Each tenant can have a number of sites which can be located at GET /sites.

Non-GET operations always occur on a site level resource i.e. POST /sites/{siteId}/pages.

Sub-resources are grouped logically. For example:

  • sites/{siteId}/portfolio - Used to manage the order of projects in the portfolio
  • sites/{siteId}/portfolio/projects - Portfolio projects
  • sites/{siteId}/portfolio/projects/{projectId} - A specific project
  • sites/{siteId}/portfolio/projects/{projectId}/media - Manage media for a specific project

Due to the nature of the application and how the API data is used, in almost all cases, cache invalidation should occur at the top level site resource.

So a POST to sites/{siteId}/pages should invalidate (including any querystrings):

  • sites/{siteId}/pages
  • sites/{siteId}/pages/{pageId}

In the same way a POST to sites/{siteId}/projects/{projectId}/media should invalidate everything from sites/{siteId}/portfolio/*. For example, if we change the cover image for a project we need to ensure the project list resource cache is invalidated so that it picks up the new image.

I've added a Help page to our api detailing our resources. Any pointers would be appreciated.

MaxAge ignored / overwritten on per action basis

I would like to use Max-Age along with ETags for a mobile web service to minimize almost all traffic to the webservice if it's not absolutely required. However setting the cache control header is overwritten in the handler instead of checking for their existence and only setting the unset ones.

Web application hangs when using Redis client

Just wondered how production ready/complete the Redis client is?

I've got Redis running on my Mac and can see my web application (running in a Windows VM) connecting.

From redis terminal it would seem that the cached data is being returned to the web application immediately, but the page never loads. Any pointers on how I can diagnose this further? Everything works fine with the default in memory client.

Here's my code for the Redis client:

    var connectionSettings = new RedisConnectionSettings
    {
        HostName = "jasper"
    };
    var httpClient = new HttpClient(new CachingHandler(new RedisStore(connectionSettings))
    {
        InnerHandler = new HttpClientHandler
        {
            AutomaticDecompression = DecompressionMethods.GZip | DecompressionMethods.Deflate
        }
    });

    httpClient.DefaultRequestHeaders.AcceptEncoding.Add(new StringWithQualityHeaderValue("gzip"));

Add more detailed tracing

Other than proxying traffic through Fiddler it is difficult to check when the response is a cached response or not (with .NET client). Adding better tracing on both the server and the client would be very useful so we can verify caching is working from the Visual Studio output window.

Sign NuGet assemblies with strong name

Signing the assemblies would make it possible to reference CacheCow assemblies from other components that have a strong name. Thanks for your time and such a great project!

Support for inheritor of Server.CachingHandler to invalidate more items based on response headers

Scenario from Linked Cache Invalidation draft RFC

  1. POST comment to http://example.com/blog/2012/05/04/hi
  2. Response includes header Link: <http://example.com/blog/>; rel="invalidates"
  3. Cache for http://example.com/blog/ is now be invalidated

As of now a handler inheriting from Server.CachingHandler cannot get at these headers. It only has access to the uri and the method if it replaces LinkedRoutePatternProvider.

Possible api alteration suggestions that would amend this:

  • LinkedRoutePatternProvider could also send the response headers
  • ExecuteCacheInvalidationRules could be virtual

Should requests to the same resource with a different query return the same ETag?

As per the title really. A good example of this is a resource that allows paging.

Should a request to:

/contacts?page=1

return the same ETag as:

/contacts?page=2

By default CacheCow will generate 2 cache entries for these requests on the server since the Query is included in the CacheKeyHash.

This means that a POST to /contacts will not actually invalidate either entry since the RoutePattern will not match.

This can be fixed by setting the UriTrimmer to return the Path only, but the question is, is this semantically correct?

A branching model

IMO, the main CacheCow repository should have a branching model. It might be a simple one. The main idea is not to have the unstable code under the master branch. For instance, in my fork, I don't do anything under the master branch. I branch of off the master branch and open up separate branches. When there is a change under the master branch of the upstream repo, I merge it into my master first and then merge my master branch with my other branches.

You could simply have two branches: master and dev. When u feel that the dev branch is ready and can be released, u can merge it into master and u can even tag that commit by labeling it with the version.

Split CacheControlHeader generation and CacheControlHeader processing into two handlers

I realize this is a big wish and I'm just curious as to your perspective on this. It will help me decide how I should proceed.

IMHO a caching layer should be all about reading a CacheControlHeader and acting on those instructions. Personally, I think that the header should be defined all the way back in the controller action. However, I realize that some people like the idea of an intermediary using policy to generate the caching header. It would be awesome if we could pick and choose how we want the caching layer to work.

All of the "state" related to caching directives should be persisted into the response message headers anyway, so splitting these two functions into two handlers, in theory, should be fairly easy to do.

Thoughts?

must-validate in cache-control

Dear aliostad,

How can we control the must-validate in cache-control header? It seems to be sent regardless if HttpCacheControlPolicy is used.

Regards

Remove dependency on WebHost

From what I can tell, the only dependency on WebHost is the use of the static GlobalConfiguration.Configuration. It would be nice if we could use this library in Self-Host and other Owin hosts.

HttpClient deadlock with async methods

After updating to the latest version of CacheCow, our web project started deadlocking. We are using the async keywords from .NET 4.5.

I have been using an older version CacheCow (0.1.3) without issues.

After investigation, the problem seems to be because of blocking calls in the InMemoryCacheStore, such as _messageSerializer.DeserializeToResponseAsync(new MemoryStream(buffer)).Result.

Here are the repro steps:

  1. Create a web API project and target the .NET 4.5 framework.
  2. Call an API method asynchronously:
    protected static InMemoryCacheStore Cache = new InMemoryCacheStore();

    //
    // GET: /Home/
    public async Task<ActionResult> Index()
    {
        // create HTTP client
        HttpClient client = new HttpClient(new CachingHandler(Cache) { InnerHandler = new HttpClientHandler() });

        // fetch content
        string url = "http://localhost:7277//api/values/1"; // change me
        HttpResponseMessage request = await client.GetAsync(url);
        string response = await request.Content.ReadAsStringAsync();

        // return MVC view
        this.ViewBag.ApiResult = response;
        return this.View("Index");
    }
  1. Trigger a call to the API again (this time the result should be cached).
  2. Deadlock occurs.

Beware: mixing blocking and asynchronous code within UI applications (like a web project) can lead to deadlocks. See Parallel Programming with .NET: Await, and UI, and deadlocks! Oh my! (Stephen Toub, MSDN) and Don't Block on Async Code (Stephen Cleary).

Cache-Control header default to no-cache on subsequent request when using HttpCacheControlPolicy

I am using AttributeBasedCacheControlPolicy with CacheControlHeaderValue that cache privately for a couple of days by default.

When I use HttpCacheControlPolicy(false, 600) in an action, the first request responded as expected "Cache-Control: public, must-revalidate, max-age=600". However the subsequent request (after 600 second or expired) , contain "Cache-Control: no-cache" even though the HTTP status is correct "304 Not Modified". I expect it should always "Cache-Control: public, must-revalidate, max-age=600" to perform 'sliding' expiration.

I inspect using Fiddler

Memory leak in CacheCow.Client.CachingHandler

Hi,

In the constructor of the client's CachingHandler the VaryHeaderStore cache is initialized with a new instance of InMemoryVaryHeaderStore.

The InMemoryVaryHeaderStore class initializes a new MemoryCache object internally which implements IDisposable (and is never disposed). This very quickly leads to OutOfMemoryExceptions when a new HTTP client is initialized per request.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.