Skip to content
Van Nguyen edited this page Oct 18, 2017 · 20 revisions

Flatwhite.WebApi is an output cache library for WebApi with VaryByParam (on action method) and VaryByHeader support, facilitate usages of cache control and HTTP Cache-Control Extensions for Stale Content

WebApi2:

In your Global.asax.cs class:

GlobalConfiguration.Configure(WebApiConfig.Register);
//Optional custom cache store
Global.CacheStoreProvider.RegisterAsyncStore(new MyAsyncCacheStore());
GlobalConfiguration.Configure(x => x.UseFlatwhiteCache(new FlatwhiteWebApiConfiguration
{
    EnableStatusController = true,
    IgnoreVaryCustomKeys = false, // set to true could improve performance if you don't use vary by headers and vary by custom
    LoopbackAddress = null // Set it to web server loopback address if server is behind load balancer
}));

Owin Usages:

In your Startup.cs class:

public void Configuration(IAppBuilder app)
{
	var config = new HttpConfiguration();            
	WebApiConfig.Register(config);
	//Optional custom cache store
	Global.CacheStoreProvider.RegisterAsyncStore(new MyAsyncCacheStore());
	config.UseFlatwhiteCache(new FlatwhiteWebApiConfiguration
	{
		EnableStatusController = true,
		IgnoreVaryCustomKeys = false, // set to true could improve performance if you don't use vary by headers and vary by custom
		LoopbackAddress = null // Set it to web server loopback address if server is behind load balancer
	});
	app.UseWebApi(config);
}

Owin with Autofac:

You will need package Flatwhite.Autofac to use this obviously. Basically you will need it when you want to inject your own implementation of below interfaces. In your Startup.cs class:

public void Configuration(IAppBuilder app)
{
	var config = new HttpConfiguration();
	var container = BuildAutofacContainer(config);
	
	WebApiConfig.Register(config);
	//Optional custom cache store
	Global.CacheStoreProvider.RegisterAsyncStore(container.Resolve<IAsyncCacheStore>());
	config.UseFlatwhiteCache(new FlatwhiteWebApiConfiguration
	{
		EnableStatusController = true,
		IgnoreVaryCustomKeys = false, // set to true could improve performance if you don't use vary by headers and vary by custom
		LoopbackAddress = null // Set it to web server loopback address if server is behind load balancer
	});
	app.UseWebApi(config);
}

private IContainer BuildAutofacContainer(HttpConfiguration config)
{
    var builder = new ContainerBuilder().EnableFlatwhite();

    builder.RegisterApiControllers(Assembly.GetExecutingAssembly());
    builder.RegisterWebApiFilterProvider(config);            

    // Register your custom Flatwhite stuff below
    builder.RegisterType<MyCustomAsyncCacheStore>().As<IAsyncCacheStore>().SingleInstance();

    var container = builder.Build();
    config.DependencyResolver = new AutofacWebApiDependencyResolver(container);
    return container;
}

Decorate your action method with OutputCache attribute:

The server will response HttpStatusCode 304 by the eTag sent by browser immediately when it sees the cache is available without creating controllers, descriptors, etc which could slow down the performance.

NOTE: The OutputCache decorated here is from Flatwhite.WebApi as there is the same attribute from Flatwhite package

[HttpGet]
[Route("api/vary-by-param-async/{packageId}")]
[OutputCache(
    MaxAge = 10,
    StaleWhileRevalidate = 5,
    VaryByParam = "packageId",
    RevalidateKeyFormat = "Package_{packageId}",
    IgnoreRevalidationRequest = true)]
public async Task<HttpResponseMessage> VaryByParamAsync(string packageId)
{
    var sw = Stopwatch.StartNew();
    var content = await new WebClient().DownloadStringTaskAsync(new Uri($"https://www.nuget.org/packages/" + packageId));
    return new HttpResponseMessage()
    {
        Content = new StringContent($"Elapsed {sw.ElapsedMilliseconds} Milliseconds", Encoding.UTF8, "text/html")
    };
}

[HttpGet]
[Route("api/vary-by-param/{packageId}")]
[OutputCache(
    MaxAge = 10, 
    StaleWhileRevalidate = 5, 
    VaryByParam = "packageId", 
    RevalidateKeyFormat = "Package_{packageId}",
    IgnoreRevalidationRequest = true)]
public HttpResponseMessage VaryByParam(string packageId)
{
    var sw = Stopwatch.StartNew();
    var content = new WebClient().DownloadString(new Uri($"https://www.nuget.org/packages/" + packageId));
    return new HttpResponseMessage()
    {
        Content = new StringContent($"Elapsed {sw.ElapsedMilliseconds} Milliseconds", Encoding.UTF8,"text/html")
    };
}

[HttpGet]
[Route("api/vary-by-header")]
[OutputCache(
    MaxAge = 10, 
    StaleWhileRevalidate = 5, 
    VaryByHeader = "UserAgent")]
public async Task<HttpResponseMessage> VaryByHeader()
{
    var sw = Stopwatch.StartNew();
    var content = await new WebClient().DownloadStringTaskAsync(new Uri($"https://www.nuget.org/packages/Flatwhite"));
    return new HttpResponseMessage()
    {
        Content = new StringContent($"Elapsed {sw.ElapsedMilliseconds} Milliseconds", Encoding.UTF8, "text/html")
    };
}


[HttpGet]
[Route("api/reset")]
[Revalidate("Package_{packageId}")]
public HttpResponseMessage ResetCache(string packageId)
{
    return new HttpResponseMessage(HttpStatusCode.OK);
}

Check cache item status:

If you call method UseFlatwhiteCache() with enableStatusController = true, Flatwhite will register the status controller which can be accessed at /_flatwhite/store/{storeId} (default store id is 0 which is MemoryCache). Go there and you can see json objects of cache items:

  • Go to _/flatwhite/store{storeId} to see all cache item in current cache store. Below is sample payload:
[
  {
    "_type": "WebApiCacheItem",
    "key": "fw-0-D274D4F7CF6AFC6D81283435F1977E85",
    "size": 296,
    "createdTime": "2015-12-11T09:46:18.3391372Z",
    "maxAge": 2,
    "staleWhileRevalidate": 5,
    "storeId": 0,
    "age": 1,
    "isStale": false,
    "checksum": "8445C4B330D87783166BF19B078A0813",
    "responseMediaType": "text/html",
    "responseCharSet": "utf-8",
    "staleIfError": 0,
    "autoRefresh": true,
    "phoenixStatus": "inactive for 0.6 seconds"
  },
  {
    "_type": "CacheItem",
    "key": "Flatwhite::Flatwhite.WebApi2.Controllers.ICoffeeService.OrderCoffeeAsync()",
    "size": 146791,
    "createdTime": "2015-12-11T09:46:15.0149471Z",
    "maxAge": 2,
    "staleWhileRevalidate": 5,
    "storeId": 0,
    "age": 4,
    "isStale": true,
    "autoRefresh": false,
    "phoenixStatus": "raising"
  }
]
  • Go to _/flatwhite/phoenixes to see all phoenix objects in memory
  • In distributed environment, there is certainly cases where phoenix object is created from a server but not available in others. So hitting this enpdpoint from load balancer may show different results. The phoenix object may be created on other servers when cache is stale.
  • Below is sample payload:
[
  {
    "_type": "Phoenix",
    "key": "Flatwhite::Flatwhite.WebApi2.Controllers.ICoffeeService.OrderCoffeeAsync()",
    "size": 359171,
    "createdTime": "2015-12-11T09:45:46.7323294Z",
    "maxAge": 2,
    "staleWhileRevalidate": 5,
    "storeId": 0,
    "age": 4,
    "isStale": true,
    "autoRefresh": false,
    "phoenixStatus": "raising"
  },
  {
    "_type": "WebApiPhoenix",
    "key": "fw-0-D274D4F7CF6AFC6D81283435F1977E85",
    "size": 296,
    "createdTime": "2015-12-11T09:45:50.3125342Z",
    "maxAge": 2,
    "staleWhileRevalidate": 5,
    "storeId": 0,
    "age": 1,
    "isStale": false,
    "checksum": "8445C4B330D87783166BF19B078A0813",
    "responseMediaType": "text/html",
    "responseCharSet": "utf-8",
    "staleIfError": 0,
    "autoRefresh": true,
    "phoenixStatus": "inactive for 0.6 seconds"
  }
]

Similar to the Core Flatwhite package, Flatwhite.WebApi can automatically refresh the cache item if you have staleWhileRevalidate greater than 0. The first request come to the server when the cache item started to be stale will kick off the refresh process by sending a background request to the loopback address.

So with a OutputCache setting like below:

  • max-age: 2 seconds
  • staleWhileRevalidate: 5 seconds

Given that it normally takes 1 to 2 seconds to refresh a heavy cache, most of requests (except the special request sent by the server itself to refresh the stale item) sent to server will be served cached data.

Please check out a screen cast here for more details, you would see that "age" is always below 2 seconds as the cache item is refreshed fast enough within a few seconds.