Flatwhite.WebApi

Van Nguyen edited this page May 13, 2016 · 14 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);
GlobalConfiguration.Configure(x => x.UseFlatwhiteCache(new FlatwhiteWebApiConfiguration
{
    EnableStatusController = true
    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);
    config.UseFlatwhiteCache(new FlatwhiteWebApiConfiguration
    {
        EnableStatusController = true,
        LoopbackAddress = null // Set it to web server loopback address if server is behind load balancer
    });
    app.UseWebApi(config);
}

Owin with Autofac:

In your Startup.cs class:

public void Configuration(IAppBuilder app)
{
    var config = new HttpConfiguration();
    var container = BuildAutofacContainer(config);

    WebApiConfig.Register(config);
    config.UseFlatwhiteCache(new FlatwhiteWebApiConfiguration
    {
        EnableStatusController = true,
        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();

    // This will also be set to Global.CacheStrategyProvider in UseFlatwhiteCache method
    builder.RegisterType<WebApiCacheStrategyProvider>().As<ICacheStrategyProvider>().SingleInstance();

    // This is required by EtagHeaderHandler and OutputCacheAttribute when it builds the response
    builder.RegisterType<CacheResponseBuilder>().As<ICacheResponseBuilder>().SingleInstance();

    // This is required by CachControlHeaderHandlerProvider
    // NOTE: Register more instances of ICachControlHeaderHandler here
    builder.RegisterType<EtagHeaderHandler>().As<ICachControlHeaderHandler>().SingleInstance();



    builder.RegisterApiControllers(Assembly.GetExecutingAssembly());
    // OPTIONAL: Register the Autofac filter provider.
    builder.RegisterWebApiFilterProvider(config);



    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.

[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/phoenix to see all phoenix objects in memory 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.