缓存结果来自 ASP.NET 中的异步方法

本文关键字:异步方法 NET ASP 结果 缓存 | 更新日期: 2023-09-27 18:32:53

我有一个简单的MVC控制器,我已经做了异步,它使用await Task.WhenAll从Web服务获取数据。我想缓存这些调用的结果,这样我就不必一直调用 API。目前,当我得到响应时,我正在视图控制器中缓存结果,但理想情况下,我希望我调用的方法(调用 API)来处理缓存。问题是该方法还无法访问结果,因为它是异步的,只返回一个任务。

是否有可能使用另一种方法在返回结果后缓存结果?

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

缓存结果来自 ASP.NET 中的异步方法

只要缓存实现在内存中,就可以缓存任务本身而不是任务结果:

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

这种方法还有一个额外的好处,即如果多个HTTP请求尝试获取相同的数据,它们实际上将共享任务本身。因此,它共享实际的异步操作而不是结果。

但是,此方法的缺点是Task<T>不可序列化,因此,如果您使用的是自定义磁盘支持的缓存或共享缓存(例如 Redis),则此方法将不起作用。

回答这个问题有点晚了,但我想我可以通过一个名为 LazyCache 的开源库来改进史蒂文斯的答案,它将在几行代码中为您完成此操作。它最近进行了更新,以处理这种情况在内存中缓存任务。它也在 nuget 上可用。

给定您的获取数据方法是这样的:

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

然后您的控制器变为

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

默认情况下,它具有内置锁定功能,因此可缓存方法每次缓存未命中只会执行一次,并且它使用lamda,因此您可以一次性执行"获取或添加"。它默认为 20 分钟滑动过期,但您可以设置您喜欢的任何缓存策略。

有关缓存任务的详细信息,请参阅 api 文档,你可能会发现用于演示缓存任务的示例 webapi 应用非常有用。

(免责声明:我是LazyCache的作者)

我建议你使用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];