3

I have a simple MVC controller which I've made async which gets data from a web service using await Task.WhenAll. I would like to cache the results of these calls so I don't have to call the API all the time. At the moment I'm caching the results in the view controller when I get the response but ideally I would like the method I'm calling, which calls the API, to handle the caching. The problem is that method doesn't have access to the results yet as it's asynchronous and just returns a task.

Is it possible to have another method which caches the results once they're returned?

public async Task<ActionResult> Index()
{
    // Get data asynchronously
    var languagesTask = GetDataAsync<List<Language>>("languages");
    var currenciesTask = GetDataAsync<List<Currency>>("currencies");

    await Task.WhenAll(languagesTask, currenciesTask);


    // Get results
    List<Language> languages = languagesTask.Result;
    List<Currency> currencies = currenciesTask.Result;


    // Add results to cache
    AddToCache("languages", languages);
    AddToCache("currencies", currencies);


    // Add results to view and return
    ViewBag.languages = languages;
    ViewBag.currencies = currencies;

    return View();
}

public async Task<T> GetDataAsync<T>(string operation)
{
    // Check cache for data first
    string cacheName = operation;

    var cacheData = HttpRuntime.Cache[cacheName];

    if (cacheData != null)
    {
        return (T)cacheData;
    }


    // Get data from remote api
    using (HttpClient client = new HttpClient())
    {
        client.BaseAddress = new Uri("https://myapi.com/");

        var response = await client.GetAsync(operation);

        // Add result to cache
        //...

        return (await response.Content.ReadAsAsync<T>());
    }
}
Mark Clancy
  • 7,831
  • 8
  • 43
  • 49

3 Answers3

11

As long as your cache implementation is in-memory, you can cache the tasks themselves rather than the task results:

public Task<T> GetDataAsync<T>(string operation)
{
  // Check cache for data first
  var task = HttpRuntime.Cache[operation] as Task<T>;
  if (task != null)
    return task;

  task = DoGetDataAsync(operation);
  AddToCache(operation, task);
  return task;
}

private async Task<T> DoGetDataAsync<T>(string operation)
{
  // Get data from remote api
  using (HttpClient client = new HttpClient())
  {
    client.BaseAddress = new Uri("https://myapi.com/");
    var response = await client.GetAsync(operation);
    return (await response.Content.ReadAsAsync<T>());
  }
}

This approach has the added benefit that if multiple HTTP requests try to get the same data, they'll actually share the task itself. So it shares the actual asynchronous operation instead of the result.

However, the drawback to this approach is that Task<T> is not serializable, so if you're using a custom disk-backed cache or a shared cache (e.g., Redis), then this approach won't work.

Stephen Cleary
  • 437,863
  • 77
  • 675
  • 810
  • Thanks for the response. That looks like a good solution. I am using the in-memory cache so will test with caching the task and see if this suits the application. – Mark Clancy Dec 03 '14 at 14:08
1

Bit late to answer this but I think i can improve on Stevens answer with an open source library called LazyCache that will do this for you in a couple of lines of code. It was recently updated to handle caching Tasks in memory for just this sort of situation. It is also available on nuget.

Given your get data method is like so:

private async Task<T> DoGetDataAsync<T>(string operation)
{
  // Get data from remote api
  using (HttpClient client = new HttpClient())
  {
    client.BaseAddress = new Uri("https://myapi.com/");
    var response = await client.GetAsync(operation);
    return (await response.Content.ReadAsAsync<T>());
  }
}

Then your controller becomes

public async Task<ActionResult> Index()
{
    // declare but don't execute a func unless we need to prime the cache
    Func<Task<List<Language>>> languagesFunc = 
        () => GetDataAsync<List<Currency>>("currencies");     

    // get from the cache or execute the func and cache the result   
    var languagesTask = cache.GetOrAddAsync("languages", languagesFunc);

    //same for currencies
    Func<Task<List<Language>>> currenciesFunc = 
        () => GetDataAsync<List<Currency>>("currencies");
    var currenciesTask = cache.GetOrAddAsync("currencies", currenciesFunc);

    // await the responses from the cache (instant) or the api (slow)
    await Task.WhenAll(languagesTask, currenciesTask);

    // use the results
    ViewBag.languages = languagesTask.Result;
    ViewBag.currencies = currenciesTask.Result;

    return View();
}

It has built in locking by default so the cacheable method will only execute once per cache miss, and it uses a lamda so you can do "get or add" in one go. It defaults to 20 minutes sliding expiration but you can set whatever caching policy you like on it.

More info on caching tasks is in the api docs and you may find the sample webapi app to demo caching tasks useful.

(Disclaimer: I am the author of LazyCache)

alastairtree
  • 3,960
  • 32
  • 49
0

I suggest you to use MemoryCache

MemoryCache cache = MemoryCache.Default;
string cacheName = "mycachename";

if cache.Contains(cacheName) == false || cache[cacheName] == null)
{
    // load data
    var data = await response.Content.ReadAsAsync<T>();
    // save data to cache
    cache.Set(cacheName, data, new CacheItemPolicy() { SlidingExpiration = DateTime.Now.AddDays(1).TimeOfDay });
}

return cache[cacheName];
Mihai Dinculescu
  • 19,743
  • 8
  • 55
  • 70