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 for an overview of how the features fit together, and follow the links for the details.

The services​

using Awaiten;

// Shared equipment. The machine warms up asynchronously, through an interface of the domain.
public interface IWarmUp { Task WarmUpAsync(CancellationToken ct); }

public sealed class EspressoMachine : IWarmUp
{
public bool IsHot { get; private set; }
public async Task WarmUpAsync(CancellationToken ct)
{
await HeatBoilerAsync(ct);
IsHot = true;
}
public Shot Pull() => new();
}

public sealed class Grinder;
public sealed class Shot;
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(AsyncSafety = AsyncSafety.SyncAfterInit)]
[AsyncInitializer<IWarmUp>(nameof(IWarmUp.WarmUpAsync))]
[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();
}

The [AsyncInitializer<IWarmUp>] convention makes every IWarmUp of the shop async. A Cup needs the async EspressoMachine, so in the strict default it could only be resolved asynchronously. The order below takes its cup through a synchronous Func<string, Cup>, so the container opts into AsyncSafety.SyncAfterInit. Once the machine is warmed at startup, that synchronous resolve returns immediately.

Opening the shop​

Create the root and warm the equipment once at startup.

await using CoffeeShop.Root shop = new();
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");

IEnumerable<ISyrup> syrups = order.Resolve<IEnumerable<ISyrup>>();
IMilk 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, and that the decorated terminal has something to decorate. A mistake in any of those would have failed the build. The one check this container relaxes keeps the async machine off synchronous paths: with SyncAfterInit, a synchronous resolve before warm-up blocks instead of failing the build, so warming the machine at startup is up to you.

Where to go next​