Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Abstract away async calls to IMemoryCache in API

I have been using actions that look like Example 1 to async cache json results for my .NET Core API. MemoryCache is an instance of IMemoryCache.

Example 1 (works as expected):

[HttpGet]
public async Task<IActionResult> MyAction() =>
    Json(await MemoryCache.GetOrCreateAsync(
        "MyController_MyAction", 
        entry => myService.GetAllAsync()
    ));

The calls to Json() and MemoryCache.GetOrCreate() are duplicated in many of my actions. In my real app, there are even more duplicated implementation details such as setting the AbsoluteExpirationRelativeToNow value and returning NotFound() for nulls. I would like to abstract all of this away into a shared method so that each action passes only its unique details to the call of the shared method.

In order to do this, I extracted a variable for each of the two variables that are in my actions. e.g.:

Example 2 (cache neither updated nor retrieved from):

[HttpGet]
public async Task<IActionResult> MyAction()
{
    var task = myService.GetAllAsync();
    const string cacheKey = "MyController_MyAction";
    return Json(await MemoryCache.GetOrCreateAsync(cacheKey, entry => task));
}

The next step would be to extract a shared method Get() like:

Example 3 (doesn't work because Example 2 doesn't work):

[HttpGet]
public async Task<IActionResult> MyAction()
{
    var task = myService.GetAllAsync();
    const string cacheKey = "MyController_MyAction";
    return await Get(task, cacheKey);
}

protected async Task<IActionResult> Get(Task<T> task, string cacheKey =>
    return Json(await MemoryCache.GetOrCreateAsync(cacheKey, entry => task));

Example 1 successfully retrieves subsequent results from the cache. Example 2, however, finds null in the cache on subsequent requests and retrieves the data anew each time (as verified by temp debug TryGetValue() statements as well as monitoring the underlying SQL queries hitting my database).

To me, Example 1 and Example 2 should be identical. However, maybe my understanding of async/await and Tasks is lacking (very likely).

How can I abstract away the duplicated implementation details (such as Json() and MemoryCache.GetOrCreate() calls) from my actions while still successfully updating and retrieving from the IMemoryCache in an async manner?

like image 881
Collin Barrett Avatar asked Sep 25 '18 20:09

Collin Barrett


2 Answers

var task = myService.GetAllAsync();

This will already run the GetAllAsync method, so by doing this, you are preventing the lazy behavior of the memory cache where it would only call the method when the cache key is not available.

In order to keep doing that, you will have to store an actual expression that creates the value, so you would have to do this:

Func<MyObject> createValue = () => myService.GetAllAsync();
const string cacheKey = "MyController_MyAction";
return Json(await MemoryCache.GetOrCreateAsync(cacheKey, entry => createValue()));

So, abstracting that away, this is what you could end up with:

public Task<IActionResult> MyAction()
    => GetCache("MyController_MyAction", () => myService.GetAllAsync());

The method would be implemented like this:

private async Task<IActionResult> GetCache<T>(string cacheKey, Func<Task<T>> createAction)
{
    var result = await MemoryCache.GetOrCreateAsync(cacheKey, entry => createAction());
    return Json(result);
}

If the cache key is always <ControllerName>_<ActionName>, you could even go one more step and automatically infer that from the call using the CallerMemberNameAttribute:

private async Task<IActionResult> GetCache<T>(Func<Task<T>> createAction, [CallerMemberName] string actionName = null)
{
    var cacheKey = GetType().Name + "_" + actionName;
    var result = await MemoryCache.GetOrCreateAsync(cacheKey, entry => createAction());
    return Json(result);
}

So you can just use it like this:

public Task<IActionResult> MyAction()
    => GetCache(() => myService.GetAllAsync());
like image 161
poke Avatar answered Sep 19 '22 22:09

poke


var task = myService.GetAllAsync(); isn't going to create the Func signature that you need.

public static System.Threading.Tasks.Task<TItem> GetOrCreateAsync<TItem> ( this Microsoft.Extensions.Caching.Memory.IMemoryCache cache, object key, Func<Microsoft.Extensions.Caching.Memory.ICacheEntry, System.Threading.Tasks.Task<TItem>> factory );
- CacheExtensions.GetOrCreateAsync MSDN

Instead, create the func

Func<ICacheEntry,Task<TItem>> taskFunc = entry => myService.GetAllAsync();

And then re-use it as needed

return Json(await MemoryCache.GetOrCreateAsync(cacheKey, taskFunc));
like image 37
Travis J Avatar answered Sep 20 '22 22:09

Travis J