An externally initializing singleton that uses double-check asynchronous locking, with optional async and sync disposal
$ dotnet add package Soenneker.Utils.AsyncSingleton
Soenneker.Utils.AsyncSingletonAsyncSingleton is a lightweight utility that provides lazy (and optionally asynchronous) initialization of an instance. It ensures that the instance is only created once, even in highly concurrent scenarios. It also offers both synchronous and asynchronous initialization methods while supporting a variety of initialization signatures. Additionally, AsyncSingleton implements both synchronous and asynchronous disposal.
Get(), GetAsync(), Init() or InitSync().params object[])CancellationTokendotnet add package Soenneker.Utils.AsyncSingleton
There are two different types: , and :
AsyncSingletonAsyncSingleton<T>AsyncSingleton<T>Useful in scenarios where you need a result of the initialization. Get() is the primary method.
using Microsoft.Extensions.Logging;
public class MyService
{
private readonly ILogger<MyService> _logger;
private readonly AsyncSingleton<HttpClient> _asyncSingleton;
public MyService(ILogger<MyService> logger)
{
_logger = logger;
_asyncSingleton = new AsyncSingleton(async () =>
{
_logger.LogInformation("Initializing the singleton resource synchronously...");
await Task.Delay(1000);
return new HttpClient();
});
}
public async ValueTask StartWork()
{
var httpClient = await _asyncSingleton.Get();
// At this point the task has been run, guaranteed only once (no matter if this is called concurrently)
var sameHttpClient = await _asyncSingleton.Get(); // This is the same instance of the httpClient above
}
}AsyncSingletonUseful in scenarios where you just need async single initialization, and you don't ever need to leverage an instance. Init() is the primary method.
using Microsoft.Extensions.Logging;
public class MyService
{
private readonly ILogger<MyService> _logger;
private readonly AsyncSingleton _singleExecution;
public MyService(ILogger<MyService> logger)
{
_logger = logger;
_singleExecution = new AsyncSingleton(async () =>
{
_logger.LogInformation("Initializing the singleton resource ...");
await Task.Delay(1000); // Simulates an async call
return new object(); // This object is needed for AsyncSingleton to recognize that initialization has occurred
});
}
public async ValueTask StartWork()
{
await _singleExecution.Init();
// At this point the task has been run, guaranteed only once (no matter if this is called concurrently)
await _singleExecution.Init(); // This will NOT execute the task, since it's already been called
}
}Tips:
CancellationToken to the Init(), and Get() method. This will cancel any locking occurring during initialization.AsyncSingleton that implements IDisposable or IAsyncDisposable, be sure to dispose of the AsyncSingleton instance. This will dispose the underlying instance.AsyncSingleton holds a reference to it, and will return those changes to further callers.SetInitialization() can be used to set the initialization function after the AsyncSingleton has been created. This can be useful in scenarios where the initialization function is not known at the time of creation.AsyncSingleton will block to maintain thread-safety.IAsyncDisposable, try to leverage AsyncSingleton.DisposeAsync(). Using AsyncSingleton.DisposeAsync() with an IDisposable underlying instance is fine.