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>());
}
}
source to share
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.
source to share
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)
source to share
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];
source to share