markvpc markvpc - 2 months ago 28
ASP.NET (C#) Question

Caching results from asynchronous methods in ASP.NET

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

Answer

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.

Comments