Skip to content

Commit 73f692d

Browse files
committed
Better readme
1 parent 99338a6 commit 73f692d

File tree

4 files changed

+76
-32
lines changed

4 files changed

+76
-32
lines changed

README.md

Lines changed: 71 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -3,59 +3,102 @@
33
[![](https://img.shields.io/nuget/dt/Soenneker.Utils.AsyncSingleton.svg?style=for-the-badge)](https://www.nuget.org/packages/Soenneker.Utils.AsyncSingleton/)
44

55
# ![](https://user-images.githubusercontent.com/4441470/224455560-91ed3ee7-f510-4041-a8d2-3fc093025112.png) Soenneker.Utils.AsyncSingleton
6-
### An externally initializing singleton that uses double-check asynchronous locking, with optional async and sync disposal
6+
7+
`AsyncSingleton` 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.
8+
9+
## Features
10+
11+
- **Lazy Initialization**: The instance is created only upon the first call of `Get()`, `GetAsync()`, `Init()` or `InitSync()`.
12+
- **Thread-safe**: Uses asynchronous locking for coordinated initialization in concurrent environments.
13+
- **Multiple Initialization Patterns**:
14+
- Sync and async initialization
15+
- With or without parameters (`params object[]`)
16+
- With or without `CancellationToken`
17+
- **Re-initialization Guard**: Once the singleton is initialized (or has begun initializing), further initialization reconfigurations are disallowed.
718

819
## Installation
920

1021
```
1122
dotnet add package Soenneker.Utils.AsyncSingleton
1223
```
1324

14-
## Example
25+
There are two different types: `AsyncSingleton`, and `AsyncSingleton<T>`:
1526

16-
The example below is a long-living `HttpClient` implementation using `AsyncSingleton`. It avoids the additional overhead of `IHttpClientFactory`, and doesn't rely on short-lived clients.
27+
### `AsyncSingleton<T>`
28+
Useful in scenarios where you need a result of the initialization. `Get()` is the primary method.
1729

1830
```csharp
19-
public class HttpRequester : IDisposable, IAsyncDisposable
31+
using Microsoft.Extensions.Logging;
32+
33+
public class MyService
2034
{
21-
private readonly AsyncSingleton<HttpClient> _client;
35+
private readonly ILogger<MyService> _logger;
36+
private readonly AsyncSingleton<HttpClient> _asyncSingleton;
2237

23-
public HttpRequester()
38+
public MyService(ILogger<MyService> logger)
2439
{
25-
// This func will lazily execute once it's retrieved the first time.
26-
// Other threads calling this at the same moment will asynchronously wait,
27-
// and then utilize the HttpClient that was created from the first caller.
28-
_client = new AsyncSingleton<HttpClient>(() =>
40+
_logger = logger;
41+
42+
_asyncSingleton = new AsyncSingleton(async () =>
2943
{
30-
var socketsHandler = new SocketsHttpHandler
31-
{
32-
PooledConnectionLifetime = TimeSpan.FromMinutes(10),
33-
MaxConnectionsPerServer = 10
34-
};
44+
_logger.LogInformation("Initializing the singleton resource synchronously...");
45+
await Task.Delay(1000);
3546

36-
return new HttpClient(socketsHandler);
47+
return new HttpClient();
3748
});
3849
}
3950

40-
public async ValueTask Get()
51+
public async ValueTask StartWork()
4152
{
42-
// retrieve the singleton async, thus not blocking the calling thread
43-
await (await _client.Get()).GetAsync("https://google.com");
53+
var httpClient = await _asyncSingleton.Get();
54+
55+
// At this point the task has been run, guaranteed only once (no matter if this is called concurrently)
56+
57+
var sameHttpClient = await _asyncSingleton.Get(); // This is the same instance of the httpClient above
4458
}
59+
}
60+
```
4561

46-
// Disposal is not necessary for AsyncSingleton unless the type used is IDisposable/IAsyncDisposable
47-
public ValueTask DisposeAsync()
62+
### `AsyncSingleton`
63+
Useful in scenarios where you just need async single initialization, and you don't ever need to leverage an instance. `Init()` is the primary method.
64+
65+
```csharp
66+
using Microsoft.Extensions.Logging;
67+
68+
public class MyService
69+
{
70+
private readonly ILogger<MyService> _logger;
71+
private readonly AsyncSingleton _singleExecution;
72+
73+
public MyService(ILogger<MyService> logger)
4874
{
49-
GC.SuppressFinalize(this);
75+
_logger = logger;
76+
77+
_singleExecution = new AsyncSingleton(async () =>
78+
{
79+
_logger.LogInformation("Initializing the singleton resource ...");
80+
await Task.Delay(1000); // Simulates an async call
5081
51-
return _client.DisposeAsync();
82+
return new object(); // This object is needed for AsyncSingleton to recognize that initialization has occurred
83+
});
5284
}
5385

54-
public void Dispose()
86+
public async ValueTask StartWork()
5587
{
56-
GC.SuppressFinalize(this);
57-
58-
_client.Dispose();
88+
await _singleExecution.Init();
89+
90+
// At this point the task has been run, guaranteed only once (no matter if this is called concurrently)
91+
92+
await _singleExecution.Init(); // This will NOT execute the task, since it's already been called
5993
}
6094
}
61-
```
95+
```
96+
97+
Tips:
98+
- If you need to cancel the initialization, pass a `CancellationToken` to the `Init()`, and `Get()` method. This will cancel any locking occurring during initialization.
99+
- If you use a type of `AsyncSingleton` that implements `IDisposable` or `IAsyncDisposable`, be sure to dispose of the `AsyncSingleton` instance. This will dispose the underlying instance.
100+
- Be careful about updating the underlying instance directly, as `AsyncSingleton` holds a reference to it, and will return those changes to further callers.
101+
- `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.
102+
- Try not to use an asynchronous initialization method, and then retrieve it synchronously. If you do so, `AsyncSingleton` will block to maintain thread-safety.
103+
- Using a synchronous initialization method with asynchronous retrieval will not block, and will still provide thread-safety.
104+
- Similarly, if the underlying instance is `IAsyncDisposable`, try to leverage `AsyncSingleton.DisposeAsync()`. Using `AsyncSingleton.DisposeAsync()` with an `IDisposable` underlying instance is fine.

src/Abstract/IAsyncSingleton.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ public interface IAsyncSingleton : IDisposable, IAsyncDisposable
2626
/// <remarks>The initialization func needs to be set before calling this, either in the ctor or via the other methods</remarks>
2727
/// <exception cref="ObjectDisposedException"></exception>
2828
/// <exception cref="NullReferenceException"></exception>
29-
ValueTask Init(CancellationToken cancellationToken, object[] objects);
29+
ValueTask Init(CancellationToken cancellationToken, params object[] objects);
3030

3131
/// <summary>
3232
/// <see cref="Init(System.Threading.CancellationToken,object[])"/> should be used instead of this if possible. This method can block the calling thread! It's lazy; it's initialized only when retrieving. <para/>
@@ -44,7 +44,7 @@ public interface IAsyncSingleton : IDisposable, IAsyncDisposable
4444
/// <remarks>The initialization func needs to be set before calling this, either in the ctor or via the other methods</remarks>
4545
/// <exception cref="ObjectDisposedException"></exception>
4646
/// <exception cref="NullReferenceException"></exception>
47-
void InitSync(CancellationToken cancellationToken, object[] objects);
47+
void InitSync(CancellationToken cancellationToken, params object[] objects);
4848

4949
/// <see cref="SetInitialization(Func{object})"/>
5050
void SetInitialization(Func<CancellationToken, object[], ValueTask<object>> func);

src/Abstract/IAsyncSingleton{T}.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ public interface IAsyncSingleton<T> : IDisposable, IAsyncDisposable
2929
/// <exception cref="ObjectDisposedException"></exception>
3030
/// <exception cref="NullReferenceException"></exception>
3131
[Pure]
32-
ValueTask<T> Get(CancellationToken cancellationToken, object[] objects);
32+
ValueTask<T> Get(CancellationToken cancellationToken, params object[] objects);
3333

3434
/// <summary>
3535
/// <see cref="Get"/> should be used instead of this if possible. This method can block the calling thread! It's lazy; it's initialized only when retrieving. <para/>
@@ -49,7 +49,7 @@ public interface IAsyncSingleton<T> : IDisposable, IAsyncDisposable
4949
/// <exception cref="ObjectDisposedException"></exception>
5050
/// <exception cref="NullReferenceException"></exception>
5151
[Pure]
52-
T GetSync(CancellationToken cancellationToken, object[] objects);
52+
T GetSync(CancellationToken cancellationToken, params object[] objects);
5353

5454
/// <see cref="SetInitialization(Func{T})"/>
5555
void SetInitialization(Func<CancellationToken, object[], ValueTask<T>> func);

src/AsyncSingleton{T}.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
namespace Soenneker.Utils.AsyncSingleton;
1010

1111
///<inheritdoc cref="IAsyncSingleton{T}"/>
12+
// ReSharper disable once ClassWithVirtualMembersNeverInherited.Global
1213
public class AsyncSingleton<T> : IAsyncSingleton<T>
1314
{
1415
private T? _instance;

0 commit comments

Comments
 (0)