Skip to main content

Relationships

A dependency does not have to be the service itself. It can be a way to get the service later, or more than once. These wrapper shapes are called relationships. A barista does not hold a hundred cups. It holds a way to make a cup when an order comes in.

Func: build on demand

Ask for Func<T> to get a service each time you call it. A transient gives you a fresh one per call. A singleton gives you the same one.

public sealed class Barista(Func<Cup> cups)
{
public Cup ServeNext() => cups(); // a fresh cup every order
}

Lazy: build once, on first use

Lazy<T> defers construction until the first access, then memoizes it. Good for something expensive that might not be needed.

public sealed class Register(Lazy<MenuBoard> menu)
{
public string Today() => menu.Value.Special; // built on first read
}

Task: the async shape

Task<T> is how a synchronous service holds an async one. See Async initialization for the full story.

public sealed class Barista(Task<EspressoMachine> machine)
{
public async Task<Cup> PullAsync() => (await machine).Pull();
}

Relationships bind to their owner

A relationship resolves from the scope that built the consumer, not from wherever it is later called. A singleton that holds a Func<Order> resolves that Order from the root, always. It is never rebound to some child scope that happens to call it later. This keeps lifetimes predictable.

Reaching shorter-lived services

Relationships are the clean way for a long-lived service to reach a shorter-lived one. A singleton cannot depend on a scoped service directly (AWT105), but it can hold a Func<T> or Lazy<T> and pull it when needed.

Note: in the strict default, a root-owned Func<T> over a disposable transient is withheld, because it would pile up undisposed instances on the root. Use Owned instead, or switch to Loose lifetime safety.

Where to go next

  • Runtime arguments for Func<TArg, T> with values you pass in.
  • Owned for disposable services you rent and return.