Skip to main content

Complete example

This page assembles a full coffee shop from the pieces in the rest of the docs. It shows a realistic container: async equipment, per-order scopes, keyed milk, a decorated payment terminal, and a module. Read it top to bottom and you have seen how the features fit together.

The services​

using Awaiten;

// Shared equipment. The machine warms up asynchronously.
public sealed class EspressoMachine : IAsyncInitializable
{
public bool IsHot { get; private set; }
public async Task InitializeAsync(CancellationToken ct)
{
await HeatBoilerAsync(ct);
IsHot = true;
}
public Shot Pull() => new();
}

public sealed class Grinder;
public sealed class MenuBoard { public static MenuBoard Load() => new(); }

// One order at a time.
public sealed class Order;
public sealed class Tab(Order order);

// Made fresh, sized at request time.
public sealed class Cup
{
public Cup(EspressoMachine machine, [Arg] string size) => Size = size;
public string Size { get; }
}

// Keyed milk.
public interface IMilk;
public sealed class OatMilk : IMilk;
public sealed class WholeMilk : IMilk;

// Several syrups, resolved as a collection.
public interface ISyrup;
public sealed class VanillaSyrup : ISyrup;
public sealed class CaramelSyrup : ISyrup;

// Payment, wrapped with logging.
public interface IPaymentTerminal { Task ChargeAsync(decimal amount); }
public sealed class CardTerminal : IPaymentTerminal
{
public Task ChargeAsync(decimal amount) => Task.CompletedTask;
}
public sealed class LoggingTerminal(IPaymentTerminal inner) : IPaymentTerminal
{
public Task ChargeAsync(decimal amount) => inner.ChargeAsync(amount);
}

The payment module​

Group payment wiring so it can be reused and tested on its own.

[Module]
[Singleton<CardTerminal, IPaymentTerminal>]
[Decorate<LoggingTerminal, IPaymentTerminal>]
public static class PaymentModule;

The container​

[Container(SyncResolveAfterInit = true)]
[Import(typeof(PaymentModule))]

// equipment
[Singleton<EspressoMachine>]
[Singleton<Grinder>]
[Singleton<MenuBoard>(Instance = nameof(Menu))]

// per order
[Scoped<Order>]
[Scoped<Tab>]

// made to order
[Transient<Cup>]

// keyed milk
[Singleton<OatMilk, IMilk>(Key = "Oat")]
[Singleton<WholeMilk, IMilk>(Key = "Whole")]

// syrups as a set
[Singleton<VanillaSyrup, ISyrup>]
[Singleton<CaramelSyrup, ISyrup>]
public static partial class CoffeeShop
{
private static MenuBoard Menu { get; } = MenuBoard.Load();
}

Opening the shop​

Create the root and warm the equipment once at startup.

await using var shop = new CoffeeShop.Root();
await shop.InitializeAsync(); // the espresso machine heats its boiler

Serving an order​

Each order runs in its own scope, so its Order and Tab are isolated and disposed when the order is done.

using (IAwaitenScope order = shop.CreateScope())
{
Tab tab = order.Resolve<Tab>();
Cup cup = order.Resolve<Func<string, Cup>>()("Large");

var syrups = order.Resolve<IEnumerable<ISyrup>>();
var milk = order.Resolve<IReadOnlyDictionary<string, IMilk>>()["Oat"];

IPaymentTerminal terminal = order.Resolve<IPaymentTerminal>(); // LoggingTerminal(CardTerminal)
await terminal.ChargeAsync(4.50m);
} // the order, its tab, and the cup are disposed here

Closing up​

Disposing the root disposes the singletons in reverse creation order. Because the root is await using, any async-disposable service is drained through DisposeAsync.

What the compiler checked​

None of this wiring is trusted at runtime. Before the app ran, Awaiten verified that every dependency is registered, that no singleton captures a scoped service, that the async machine is never reached synchronously, and that the decorated terminal has something to decorate. A mistake in any of those would have failed the build.

Where to go next​