Composites
A composite is one service that stands in for many. The consumer asks for a single service, and behind it the composite fans the call out to every other implementation. It is the classic way to treat "notify the customer on all channels" as one call.
Compose a service
Register the real implementations, then add [Composite<TComposite, TService>]. The composite takes a collection of the service in its constructor.
public sealed class CompositeReceiptChannel(IEnumerable<IReceiptChannel> channels) : IReceiptChannel
{
public Task SendAsync(Receipt receipt)
=> Task.WhenAll(channels.Select(c => c.SendAsync(receipt)));
}
[Container]
[Transient<PrinterChannel, IReceiptChannel>]
[Transient<EmailChannel, IReceiptChannel>]
[Transient<SmsChannel, IReceiptChannel>]
[Composite<CompositeReceiptChannel, IReceiptChannel>]
public static partial class CoffeeShop;
Asking for the service gives you the composite
IReceiptChannel channel = shop.Resolve<IReceiptChannel>(); // the composite
await channel.SendAsync(receipt); // printer, email, and SMS
The composite never contains itself. Its own collection parameter receives only the other members. A consumer that asks for IReceiptChannel[] still gets the bare channels, so you can reach them directly if you need to.
Front a key
Set Key to front the registrations under one key instead of the unkeyed ones. A resolve under that key, or a [FromKey] parameter naming it, gets the composite, and the composite's collection parameter receives the registrations under that key. The unkeyed registrations are left alone.
[Container]
[Transient<PrinterChannel, IReceiptChannel>]
[Transient<KitchenDisplayChannel, IReceiptChannel>(Key = "kitchen")]
[Composite<CompositeReceiptChannel, IReceiptChannel>(Key = "kitchen")]
public static partial class CoffeeShop;
// Resolve<IReceiptChannel>("kitchen") returns CompositeReceiptChannel over KitchenDisplayChannel
// Resolve<IReceiptChannel>() returns PrinterChannel
The collection parameter is a plain collection of the service, never a keyed dictionary. Like an unkeyed composite, a keyed one stays out of every collection, so the keyed dictionary still holds the bare registration under its key. To pick an implementation by key at run time, inject IReadOnlyDictionary<TKey, TService> into an ordinary service instead.
Compose every closing of an open generic
To front every closing of an open generic service with a matching composite, use the typeof form with unbound generics (the generic [Composite<,>] form only takes closed types):
public sealed class CompositeHandler<T>(IEnumerable<IHandler<T>> inner) : IHandler<T>
{
public Task HandleAsync(T request) => Task.WhenAll(inner.Select(h => h.HandleAsync(request)));
}
[Container]
[Transient(typeof(EmailHandler<>), typeof(IHandler<>))]
[Transient(typeof(SmsHandler<>), typeof(IHandler<>))]
[Composite(typeof(CompositeHandler<>), typeof(IHandler<>))]
public static partial class CoffeeShop;
// Resolve<IHandler<Receipt>>() returns CompositeHandler<Receipt> fanning out to the Receipt handlers
Awaiten synthesizes a closed composite for every closing present in the graph. Each closing then follows the closed rules above: one façade per closing, excluded from its own fan-out. With Key, only the closings registered under that key get a composite. The composite's arity must match the service's, and it must expose the service with its type parameters in declaration order (CompositeHandler<T> : IHandler<T>).
Lifetime
A composite is transient by default. Set Lifetime to change it.
[Composite<CompositeReceiptChannel, IReceiptChannel>(Lifetime = Lifetime.Singleton)]
With no members registered, the composite still resolves and fans out to an empty collection.
Only one composite may front a given service and key (AWT132), a Key must match a registration (AWT212), and the composite's collection parameter must be of the composed service itself (AWT130, AWT133). The open generic typeof form additionally requires an unbound generic (AWT127) of matching arity (AWT125), and skips a closing whose type arguments violate the composite's constraints (AWT126).
Where to go next
- Decorators to wrap a single service instead of merging many.
- Collections for resolving every implementation directly.