Specifying IAsyncEnumerable<T> as the injection type allows instances of all bindings implementing type T to be injected in an asynchronous-lazy manner - the instances will be provided one at a time, in an order corresponding to the sequence of the bindings.
using Shouldly;
using Pure.DI;
DI.Setup(nameof(Composition))
.Bind<IHealthCheck>().To<MemoryCheck>()
.Bind<IHealthCheck>("External").To<ExternalServiceCheck>()
.Bind<IHealthService>().To<HealthService>()
// Composition root
.Root<IHealthService>("HealthService");
var composition = new Composition();
var healthService = composition.HealthService;
var checks = await healthService.GetChecksAsync();
checks[0].ShouldBeOfType<MemoryCheck>();
checks[1].ShouldBeOfType<ExternalServiceCheck>();
interface IHealthCheck;
class MemoryCheck : IHealthCheck;
class ExternalServiceCheck : IHealthCheck;
interface IHealthService
{
Task<IReadOnlyList<IHealthCheck>> GetChecksAsync();
}
class HealthService(IAsyncEnumerable<IHealthCheck> checks) : IHealthService
{
public async Task<IReadOnlyList<IHealthCheck>> GetChecksAsync()
{
var results = new List<IHealthCheck>();
await foreach (var check in checks)
{
results.Add(check);
}
return results;
}
}Running this code sample locally
- Make sure you have the .NET SDK 10.0 or later installed
dotnet --list-sdk- Create a net10.0 (or later) console application
dotnet new console -n Sampledotnet add package Pure.DI
dotnet add package Shouldly- Copy the example code into the Program.cs file
You are ready to run the example 🚀
dotnet runNote
IAsyncEnumerable provides efficient lazy enumeration for scenarios where you need to process many instances without loading them all into memory at once.
Each element is created when the consumer reaches it, so a health check that fails early can stop the loop and the later checks are never constructed. The order follows the binding declaration order, exactly as with IEnumerable<T>, and here the consumer collects the results into a list of its own.
The sequence itself carries no cancellation token: pass the token to the consumer's method and stop the await foreach, as GetChecksAsync does by returning a materialized list. Do not keep the enumerable in a field and enumerate it from several threads.
Tip
Prefer IAsyncEnumerable<T> only when element creation or consumption really awaits work; for plain handlers IEnumerable<T> is cheaper and easier to test.
The following partial class will be generated
partial class Composition
{
public IHealthService HealthService
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
get
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
async IAsyncEnumerable<IHealthCheck> EnumerationOf_transientIAsyncEnumerableIHealthCheck()
{
yield return new MemoryCheck();
yield return new ExternalServiceCheck();
await Task.CompletedTask;
}
return new HealthService(EnumerationOf_transientIAsyncEnumerableIHealthCheck());
}
}
}Class diagram:
---
config:
class:
hideEmptyMembersBox: true
---
classDiagram
MemoryCheck --|> IHealthCheck
ExternalServiceCheck --|> IHealthCheck : "External"
HealthService --|> IHealthService
Composition ..> HealthService : IHealthService HealthService
HealthService *-- IAsyncEnumerableᐸIHealthCheckᐳ : IAsyncEnumerableᐸIHealthCheckᐳ
IAsyncEnumerableᐸIHealthCheckᐳ *-- MemoryCheck : IHealthCheck
IAsyncEnumerableᐸIHealthCheckᐳ *-- ExternalServiceCheck : "External" IHealthCheck
namespace Pure.DI.UsageTests.BCL.AsyncEnumerableScenario {
class Composition {
<<partial>>
+IHealthService HealthService
}
class ExternalServiceCheck {
<<class>>
+ExternalServiceCheck()
}
class HealthService {
<<class>>
+HealthService(IAsyncEnumerableᐸIHealthCheckᐳ checks)
}
class IHealthCheck {
<<interface>>
}
class IHealthService {
<<interface>>
}
class MemoryCheck {
<<class>>
+MemoryCheck()
}
}
namespace System.Collections.Generic {
class IAsyncEnumerableᐸIHealthCheckᐳ {
<<interface>>
}
}
See also: