Caching Results of Asynchronous Methods in ASP.NET

I have a simple MVC controller that I created async that receives data from a web service using wait.WhenAll. I would like to cache the results of these calls, so I don't have to constantly call the API. At the moment, I cache the results in the view controller when I receive a response, but ideally I would like the method I call that calls the API to handle the caching. The problem is that the method has no access to the results while it is asynchronous and just returns the task.

Is it possible to have another method that caches the results after they return?

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>());
    }
}

      

+3


source to share


3 answers


As long as your cache implementation is in memory, you can cache the tasks themselves, not the results of the task:

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 are trying to get the same data, they are effectively sharing the task. So it uses the actual asynchronous operation instead of the result.

However, the disadvantage of this approach is that Task<T>

it is not serializable, so if you are using a custom disk-backed cache or a shared cache (like Redis) then this approach will not work.

+8


source


A 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 few lines of code. It was recently updated to handle in-memory caching tasks only for this kind of situations. It's also available on nuget.

Considering your data fetch method looks like this:

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();
}

      

By default, it is built into locking, so the cache method will only execute once per missed cache, and it uses lamda so you can "get or add" at a time. By default, this expires 20 minutes, but you can set any caching policy you like.

More information on caching tasks is in the api docs and you can find a sample webapi application for demo caching tasks .

(Disclaimer: I am the author of LazyCache)

+1


source


I suggest you 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];

      

0


source







All Articles