TaskCaching 0.2.0

dotnet add package TaskCaching --version 0.2.0                
NuGet\Install-Package TaskCaching -Version 0.2.0                
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="TaskCaching" Version="0.2.0" />                
For projects that support PackageReference, copy this XML node into the project file to reference the package.
paket add TaskCaching --version 0.2.0                
#r "nuget: TaskCaching, 0.2.0"                
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
// Install TaskCaching as a Cake Addin
#addin nuget:?package=TaskCaching&version=0.2.0

// Install TaskCaching as a Cake Tool
#tool nuget:?package=TaskCaching&version=0.2.0                

TaskCaching

NuGet Build License

See my blog post for full explanation.

TaskCaching provides a means for easily caching long-lasting or expensive Task operations in .NET.

Its features ensure that:

  • No parallel or unnecessary operations to get a value will be started.
  • Failed Tasks are not cached (no negative caching).
  • Cache users can't get invalidated results from the cache, even if the value is invalidated during an await.
  • Optionally, Tasks can be automatically evicted from the cache as soon as they complete.
  • It targets the .NET Standard 2.0 so should be usable in most .NET projects.

Currently there are two packages/projects:

1. TaskCaching.Microsoft.Extensions.Caching.Memory

This should probably be the go-to implementation right now.

The TaskCaching.Microsoft.Extensions.Caching.Memory package has a dependency on and supports ANY version of Microsoft's Microsoft.Extensions.Caching.Memory NuGet package.

It adds an IMemoryCache.GetOrCreateTask<T>(...) extension method that persists Lazy<Task<T>> objects in the cache, fulfilling the above mentioned features.

This method differs from Microsoft's IMemoryCache.GetOrCreateAsync<T>(...), which only caches the result of a Task after it has successfully completed. Instead, this method caches a Lazy instance of the Task itself without waiting for it - allowing long-running/expensive asynchronous operations to be shared and avoiding concurrent duplication of work.

How to use it?

Install the NuGet package first, E.g.:

dotnet add package TaskCaching.Microsoft.Extensions.Caching.Memory

Now simply take an existing MemoryCache object or create a new one and use the GetOrCreateTask extension-method:

async Task<int> DoSomeSlowTask(int i) {
    Console.WriteLine("Waiting " + i);
    await Task.Delay(1000);
    Console.WriteLine("Waited " + i);
    return i;
}

using var cache = new MemoryCache(new MemoryCacheOptions());

//The following calls to the cache are going to run concurrently as we're not awaiting them yet
var task1 = cache.GetOrCreateTask("uniqueKeyForTask", e => DoSomeSlowTask(1));
var task2 = cache.GetOrCreateTask("uniqueKeyForTask", e => DoSomeSlowTask(2)); //This call to DoSomeSlowTask(2) will not run
var task3 = cache.GetOrCreateTask("uniqueKeyForTask", e => DoSomeSlowTask(3)); //This call to DoSomeSlowTask(3) will not run

await Task.WhenAll(task1, task2, task3);

Assert.Equal(1, await task1); //The result of DoSomeSlowTask(1) was returned by the cache
Assert.Equal(1, await task2); //The result of DoSomeSlowTask(1) was returned by the cache
Assert.Equal(1, await task3); //The result of DoSomeSlowTask(1) was returned by the cache

What if you don't want completed tasks remaining in the cache? You could manually remove them, or set appropriate expiration policies, but alternatively you can tell GetOrCreateTask to evict them as soon as they complete:

async Task<int> DoSomeSlowTask(int i) {
    Console.WriteLine("Waiting " + i);
    await Task.Delay(1000);
    Console.WriteLine("Waited " + i);
    return i;
}

using var cache = new MemoryCache(new MemoryCacheOptions());

//These following cache calls are being awaited so will run in sequence, however the DoSomeSlowTask(1) task will 
//immediately be evicted upon its completion, before the next cache call, because of the expireOnCompletion parameter
var value1 = await cache.GetOrCreateTask("uniqueKeyForTask", e => DoSomeSlowTask(1), expireOnCompletion:true);

//The DoSomeSlowTask(1) task is no longer in the cache anymore, so DoSomeSlowTask(2) will now run
var value2 = await cache.GetOrCreateTask("uniqueKeyForTask", e => DoSomeSlowTask(2), expireOnCompletion:true);

Assert.NotEqual(value1, value2);

2. TaskCaching

The ITaskCache and TaskCache in the TaskCaching package are legacy of the original project.

Refer to the earlier blog post (https://tech.mikkohaapanen.com/net-c-cache-class-for-caching-task-objects/) to read more about this.

The blog post may be quite old now, but is mostly still relevant. The primary things which have changed are:

Product Compatible and additional computed target framework versions.
.NET net5.0 was computed.  net5.0-windows was computed.  net6.0 was computed.  net6.0-android was computed.  net6.0-ios was computed.  net6.0-maccatalyst was computed.  net6.0-macos was computed.  net6.0-tvos was computed.  net6.0-windows was computed.  net7.0 was computed.  net7.0-android was computed.  net7.0-ios was computed.  net7.0-maccatalyst was computed.  net7.0-macos was computed.  net7.0-tvos was computed.  net7.0-windows was computed.  net8.0 was computed.  net8.0-android was computed.  net8.0-browser was computed.  net8.0-ios was computed.  net8.0-maccatalyst was computed.  net8.0-macos was computed.  net8.0-tvos was computed.  net8.0-windows was computed. 
.NET Core netcoreapp2.0 was computed.  netcoreapp2.1 was computed.  netcoreapp2.2 was computed.  netcoreapp3.0 was computed.  netcoreapp3.1 was computed. 
.NET Standard netstandard2.0 is compatible.  netstandard2.1 was computed. 
.NET Framework net461 was computed.  net462 was computed.  net463 was computed.  net47 was computed.  net471 was computed.  net472 was computed.  net48 was computed.  net481 was computed. 
MonoAndroid monoandroid was computed. 
MonoMac monomac was computed. 
MonoTouch monotouch was computed. 
Tizen tizen40 was computed.  tizen60 was computed. 
Xamarin.iOS xamarinios was computed. 
Xamarin.Mac xamarinmac was computed. 
Xamarin.TVOS xamarintvos was computed. 
Xamarin.WatchOS xamarinwatchos was computed. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

NuGet packages

This package is not used by any NuGet packages.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last updated
0.2.0 150 8/22/2023
0.1.3-ci0005 120 8/22/2023
0.1.2 220 3/9/2023