Skip to content

Commit c2c4dc8

Browse files
Explain real-project use, pitfalls and alternatives in the usage scenarios
Every scenario in tests/Pure.DI.UsageTests now carries a header that says what the feature is and when to choose it, and a footer with where it fits in a real application, the pitfalls that silently break (lifetime capture, disposal ownership, threading, ordering) and a pointer to the better alternative. Only the $h= and $f= lines of the leading block comment change: no code, no other keys, no test bodies.
1 parent 1e93ac2 commit c2c4dc8

191 files changed

Lines changed: 1027 additions & 250 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎tests/Pure.DI.UsageTests/Advanced/AccumulatorScenario.cs‎

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,14 @@
33
$p=23
44
$d=Accumulators
55
$sa=Tracking disposable instances per a composition root
6-
$h=Accumulators allow you to accumulate instances of certain types and lifetimes.
7-
$h=Use this when you need an aggregated view of created dependencies (for diagnostics, telemetry, or registries).
8-
$f=Limitations: accumulation order depends on object creation order in the graph, so do not treat it as a stable business ordering.
6+
$h=Accumulators allow you to accumulate instances of certain types and lifetimes: `Accumulate<T, TAccumulator>(...)` adds every created `T` whose lifetime is in the list to a `TAccumulator` that needs only a parameterless constructor and an `Add(T)` method.
7+
$h=Use this when you need an aggregated view of created dependencies (for diagnostics, telemetry, or registries) instead of a single injected instance, and when the registry should not be threaded through constructors by hand.
8+
$h=The accumulator is an ordinary dependency of the composition, so a root returns it like any other service.
9+
$f=In a real project this is the shape of a start-up snapshot: collect the telemetry sources, plug-ins, handlers or validators the graph actually built, then hand the collected list to a diagnostics page or a health check.
10+
$f=Limitations: accumulation order depends on object creation order in the graph, so do not treat it as a stable business ordering; sort or key the result before you expose it.
911
$f=The dependencies of a `Singleton` or `Scoped` instance belong to that shared instance, so `Transient`, `PerBlock` and `PerResolve` accumulators do not collect them.
12+
$f=The accumulator itself is a `PerBlock` dependency: it is built for the root access that asks for it and describes the objects created in that access, so request it from the same root as the objects you want to observe.
13+
$f=Prefer [Enumerable](enumerable.md) when you only need every implementation injected as a collection, and a `Singleton` collector service when the view must outlive a single root access.
1014
$f=See also: [Enumerable](enumerable.md), [Lifetimes](transient.md).
1115
$r=Shouldly
1216
*/

‎tests/Pure.DI.UsageTests/Advanced/CompositionRootKindsScenario.cs‎

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,13 @@
66
$sa=Static root
77
$h=By default, a composition root is a public instance property, but the `kind` argument of `Root<T>(...)` lets you change that. Combine `RootKinds` flags to generate the root as a method instead of a property, adjust its visibility (`Public`, `Internal`, `Private`), or make it `Static` or `Partial`.
88
$h=A private partial root is useful when you want to wrap the generated code in your own hand-written member, as the `PaymentService` property does here.
9-
$f=>[!NOTE]
10-
$f=>Composition roots can be customized with different kinds to control accessibility and lifetime, enabling flexible API design patterns.
9+
$h=The kinds combine with `|`, so one composition can expose the same contract through several roots of different shapes.
10+
$f=Use kinds at the boundary of a library or an application: keep the public API to `Public | Property` roots, mark roots that only tests or internals need as `Internal`, and hide a root behind `Private | Partial` when a hand-written member must add validation, caching or a friendlier signature - that is what the `PaymentService` property does here.
11+
$f=A root becomes a method on its own as soon as it declares root arguments or generic type arguments, so `Method` is needed only for a parameterless method.
12+
$f=A `Partial` root is declared by the generator only: you must supply the matching partial member, otherwise the composition does not compile.
13+
$f=A `Static` root cannot reach instance members of the composition and gets its own per-resolve lock instead of the instance-wide one, so keep static roots free of composition state.
14+
$f=Two roots that would produce the same member name are reported as `DIE005`; keep names and name templates distinct when you combine kinds.
15+
$f=Prefer the default `Public | Property` spelling unless a consumer needs otherwise, and add `Light` for cheap, frequently resolved roots ([Light roots](light-roots.md)).
1116
$r=Shouldly
1217
*/
1318

‎tests/Pure.DI.UsageTests/Advanced/ConsumerTypesScenario.cs‎

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,13 @@
33
$p=26
44
$d=Consumer types
55
$sa=Consumer type
6-
$h=`ConsumerTypes` is used to get the list of consumer types of a given dependency. It contains an array of types and guarantees that it will contain at least one element. The use of `ConsumerTypes` is demonstrated on the example of [Serilog library](https://serilog.net/):
7-
$h=Use this when one dependency must adapt behavior based on the concrete consuming type.
6+
$h=`ConsumerTypes` is used to get the list of consumer types of a given dependency. It contains an array of types, ordered from the immediate consumer down to the composition type, and guarantees that it will contain at least one element; `ConsumerType` gives just the immediate consumer. The use of `ConsumerTypes` is demonstrated on the example of [Serilog library](https://serilog.net/):
7+
$h=Use this when one dependency must adapt behavior based on the concrete consuming type - a logger that names its `SourceContext`, a metric tagged with the caller, a tracing span.
8+
$f=This is how every consumer gets its own infrastructure identity without changing the consumers: the factory is rewritten per injection site, so each class receives a value built with its own type.
89
$f=Limitations: consumer-aware configuration increases coupling to composition details; use it for infrastructure concerns (logging, tracing), not core domain behavior.
10+
$f=Under a `Singleton` lifetime only one instance exists, so the type names whichever consumer built it; keep such adapters `Transient` when each consumer needs its own value.
11+
$f=Every injection site adds its own generated factory code, so keep this to one small adapter binding instead of spreading consumer awareness through the graph.
12+
$f=Prefer an explicit `[Tag(...)]` or `ctx.RootName` when the grouping must follow an entry point rather than the injecting class.
913
$f=See also: [Interception](interception.md), [Factory](factory.md).
1014
$r=Shouldly;Serilog.Core;Serilog.Events
1115
*/

‎tests/Pure.DI.UsageTests/Advanced/CustomDisposeStrategyScenario.cs‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,14 @@
55
$sa=Tracking disposable instances per a composition root
66
$sa=Custom dispose strategy for Owned<T>
77
$h=The composition calls `Dispose()` on tracked singleton and scoped instances. Some types — most notably WCF clients that implement `ICommunicationObject` — require a specific shutdown sequence (try `Close()`, fall back to `Abort()` on a timeout or when the channel is faulted). Implement the partial `OnDispose` / `OnDisposeAsync` methods on the composition, perform the custom shutdown logic and return `true` to skip the default disposal call (or `false` to let the composition dispose the instance as usual).
8+
$f=Reach for it when the composition owns long-lived clients whose shutdown is more than `Dispose()`: message-broker or channel clients that must close gracefully and abort on failure, connection pools, native handles.
9+
$f=The hook runs once per tracked instance while the composition is disposed, in reverse creation order, and it answers for every disposable in the graph: switch on the concrete type, handle the types you know and return `false` for the rest.
10+
$f=Implement both `OnDispose` and `OnDisposeAsync` when the graph has both kinds of resources: the synchronous path calls `OnDispose`, the asynchronous path calls `OnDisposeAsync`.
11+
$f=An exception thrown by the hook is caught like one from `Dispose()`, passed to `OnDisposeException`, and cleanup continues with the remaining resources.
812
$f=>[!IMPORTANT]
913
$f=>`OnDispose` and `OnDisposeAsync` are partial methods. They are not invoked when not implemented, so there is zero overhead for compositions that do not need a custom disposal strategy.
14+
$f=
15+
$f=Prefer the per-owner hook ([`Owned<T>`](custom-dispose-strategy-for-ownedt.md)) when ownership is per operation rather than per composition.
1016
$r=Shouldly
1117
*/
1218

‎tests/Pure.DI.UsageTests/Advanced/CustomOwnedDisposeStrategyScenario.cs‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,13 @@
55
$sa=Tracing exceptions during Owned disposal
66
$sa=Custom dispose strategy
77
$h=`Owned<T>` calls `Dispose()` on every tracked resource when the owner is disposed. Some types — most notably WCF clients that implement `ICommunicationObject` — require a specific shutdown sequence (try `Close()`, fall back to `Abort()` on a timeout or when the channel is faulted). Implement the partial `OnDispose` / `OnDisposeAsync` hooks on the non-generic `Pure.DI.Owned` accumulator to perform the custom shutdown logic and set `skipDefaultDispose` to `true` to skip the default disposal call (or leave it `false` to let the owner dispose the instance as usual).
8+
$f=This is the per-operation counterpart of the composition hook: it fires when an `Owned<T>` is disposed, so a unit of work can close its own client instead of waiting for application shutdown.
89
$f=>[!IMPORTANT]
910
$f=>The hook belongs to the non-generic `Pure.DI.Owned` accumulator used internally by every `Owned<T>`, so its partial implementation must be declared in the `Pure.DI` namespace.
11+
$f=
12+
$f=One hook serves every owner in the compilation, so it cannot tell which operation owns the resource; record the attribution yourself if you need it in the log.
13+
$f=Set `skipDefaultDispose` only after you have released the instance; leaving it `false` lets the accumulator call `Dispose()` as usual.
14+
$f=Prefer the composition hook ([custom dispose strategy](custom-dispose-strategy.md)) for application-wide clients, and this one for per-transaction or per-message resources.
1015
$r=Shouldly
1116
*/
1217

‎tests/Pure.DI.UsageTests/Advanced/DependentCompositionsScenario.cs‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,12 @@
1010
$h=- `CompositionKind.Internal` - the composition class will not be created, but that composition can be used to create other compositions by calling the `DependsOn` method with its name
1111
$h=- `CompositionKind.Global` - the composition class will also not be created, but that composition will automatically be used to create other compositions
1212
$h=Use dependent compositions to split large object graphs into reusable setup layers.
13+
$h=An `Internal` setup is the right choice for a reusable layer that must not expose a composition class of its own; `Global` is for settings that every composition in the project should inherit, such as hints.
14+
$f=This is how a multi-project solution keeps one setup per assembly: each library declares an `Internal` setup, the executable composes them with `.DependsOn("Namespace.Composition")` and exposes named roots.
15+
$f=Setups are matched by name, so use `nameof(Composition)` rather than a string literal to keep `DependsOn` refactor-safe.
16+
$f=A dependent setup contributes its bindings and roots, not its generated members: the composition that declares `DependsOn` owns the generated class and its roots.
1317
$f=Limitations: too many setup layers can make graph ownership unclear; keep boundaries explicit and naming consistent.
18+
$f=Prefer plain class inheritance ([Inheritance of compositions](inheritance-of-compositions.md)) when the layers are in one project and share hand-written members; use `DependsOn` across assemblies and projects.
1419
*/
1520

1621
// ReSharper disable ClassNeverInstantiated.Local

‎tests/Pure.DI.UsageTests/Advanced/DependentCompositionsWithContextScenario.cs‎

Lines changed: 5 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -7,15 +7,11 @@
77
$h=When this occurs: you need base setup state (e.g., Unity-initialized fields) inside a dependent composition.
88
$h=What it solves: avoids missing instance members in dependent compositions and keeps state access explicit.
99
$h=How it is solved in the example: uses DependsOn(setupName, kind, name) and passes the base setup instance into the dependent composition.
10-
$f=
11-
$f=What it shows:
12-
$f=- Explicit setup context injection for dependent compositions.
13-
$f=
14-
$f=Important points:
15-
$f=- The dependent composition receives the base setup instance via a constructor argument.
16-
$f=
17-
$f=Useful when:
18-
$f=- Base setup has instance members initialized externally (e.g., Unity).
10+
$h=The context is then available in the constructor, so the dependent composition keeps a normal parameterless setup method and still reads live values from the base instance.
11+
$f=Use this when the base setup holds state that lives outside the container - options read at start-up, a host object, or settings assigned after construction - and its bindings must see the current values.
12+
$f=The dependent composition receives the base setup instance via a constructor argument, and the instance is stored, so assign base state before you create the dependent composition.
13+
$f=Because the bindings call instance members, they read the value on every resolution instead of capturing it once.
14+
$f=Prefer `SetupContextKind.Members` ([members context](dependent-compositions-with-setup-context-members.md)) when nothing extra must be created, and plain `DependsOn(setupName)` when the base setup has no instance members - otherwise DIW007 reports the instance member that would be missing.
1915
$f=
2016
*/
2117

‎tests/Pure.DI.UsageTests/Advanced/DependentCompositionsWithMembersContextScenario.cs‎

Lines changed: 5 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -7,26 +7,12 @@
77
$h=When this occurs: you want to reuse base setup state without passing a separate context instance.
88
$h=What it solves: lets dependent compositions access base setup members directly (Unity-friendly, no constructor args).
99
$h=How it is solved in the example: uses DependsOn(..., SetupContextKind.Members) and sets members on the composition instance. The name parameter is optional; methods are declared partial and implemented by the user.
10-
$f=
11-
$f=What it shows:
12-
$f=- Setup context members copied into the dependent composition.
13-
$f=- Realistic scenario: configuring database connection and logging for a data service.
14-
$f=
15-
$f=Important points:
16-
$f=- The composition remains parameterless and can be configured via its own members.
17-
$f=
18-
$f=Example demonstrates:
19-
$f= 1. BaseComposition provides database connection settings, default timeout, and a protected diagnostics field
20-
$f= 2. Dependent Composition adds logging level and max retries configuration
21-
$f= 3. DataService uses all settings including the protected field for conditional output
22-
$f= 4. Protected field 'EnableDiagnostics' from BaseComposition is accessible in Composition via DependsOn
23-
$f= 5. Composition's constructor sets EnableDiagnostics to true to enable detailed status
24-
$f=
10+
$h=The composition stays parameterless, so a host that instantiates it by convention - a UI framework, a test fixture - can still configure it through properties.
11+
$f=BaseComposition provides database connection settings, default timeout, and a protected diagnostics field; Composition adds a logging level and a max retry count; DataService reads all of them, including the protected field copied by the context.
12+
$f=Use this when a base setup's state is assigned after construction: the copied members become properties of the dependent composition, so the object initializer in the test is the normal way to supply configuration.
13+
$f=Members are copied by declaration, so the dependent composition needs a partial declaration of every referenced method that the base implements itself - here `GetDefaultTimeout`.
2514
$f=Note: SetupContextKind.Members copies both public and protected members to the dependent composition.
26-
$f=
27-
$f=Useful when:
28-
$f=- Base setup has instance members initialized by the host or framework.
29-
$f=- You need to extend configuration with additional settings in derived compositions.
15+
$f=Prefer `SetupContextKind.Argument` ([setup context](dependent-compositions-with-setup-context.md)) when the base instance must stay the single owner of the state, because copied members are independent values on the dependent composition.
3016
$f=
3117
*/
3218

‎tests/Pure.DI.UsageTests/Advanced/DependentCompositionsWithMembersPropertyAccessorsScenario.cs‎

Lines changed: 6 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -7,24 +7,12 @@
77
$h=When this occurs: you need base setup properties with logic, but the dependent composition must remain parameterless.
88
$h=What it solves: keeps Unity-friendly composition while letting the user implement property logic.
99
$h=How it is solved in the example: uses DependsOn(..., SetupContextKind.Members) and implements partial get_ methods.
10-
$f=
11-
$f=What it shows:
12-
$f=- Custom property logic via partial accessor methods.
13-
$f=- Properties with simple field-backed accessors (no logic).
14-
$f=
15-
$f=Important points:
16-
$f=- Accessor logic is not copied; the user provides partial implementations.
17-
$f=- Simple property accessors (field-backed) can be used without partial methods.
18-
$f=
19-
$f=Example demonstrates:
20-
$f= 1. BaseComposition provides connection string and max connections properties
21-
$f= 2. ConnectionString has simple field-backed accessor (no logic)
22-
$f= 3. MaxConnections has custom getter logic via partial method
23-
$f= 4. Dependent Composition implements custom accessor logic for MaxConnections
24-
$f=
25-
$f=Useful when:
26-
$f=- Properties include custom logic and are referenced by bindings in a dependent setup.
27-
$f=- Some properties are simple field-backed while others have custom logic.
10+
$h=The property itself is declared by the generator as a partial member, so the dependent composition decides what the getter does while the base setup keeps its own body.
11+
$f=BaseComposition provides a connection string with a plain field-backed accessor and a max connections property with logic; the dependent composition implements `get__MaxConnections` and leaves `ConnectionString` alone.
12+
$f=This is the escape hatch for a host that builds the composition from a default constructor but where some configuration needs normalization - clamping, defaults, unit conversion - before it reaches a service.
13+
$f=Accessor logic is not copied; the user provides partial implementations, and a property whose accessor is not overridden keeps the copied member.
14+
$f=Simple property accessors (field-backed) can be used without partial methods.
15+
$f=Prefer copying values through `SetupContextKind.Argument` when the accessor only needs to read another object: partial accessors are per composition and are easy to forget when the base property changes.
2816
$f=
2917
*/
3018

‎tests/Pure.DI.UsageTests/Advanced/DependentCompositionsWithRootArgumentScenario.cs‎

Lines changed: 5 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -7,15 +7,11 @@
77
$h=When this occurs: you need external state from the base setup but cannot use a constructor (e.g., Unity MonoBehaviour).
88
$h=What it solves: keeps the dependent composition safe while avoiding constructor arguments.
99
$h=How it is solved in the example: uses DependsOn(..., SetupContextKind.RootArgument, name) and passes the base setup instance to the root method.
10-
$f=
11-
$f=What it shows:
12-
$f=- Passing setup context into a root method.
13-
$f=
14-
$f=Important points:
15-
$f=- The composition itself can still be created with a parameterless constructor.
16-
$f=
17-
$f=Useful when:
18-
$f=- The host (like Unity) creates the composition instance.
10+
$h=The context is a parameter of the generated root, so one composition instance can serve several calls with different base state.
11+
$f=Use this when the base state is only known at the moment of a resolution, for example when a long-lived host creates a short-lived scope per screen or per request.
12+
$f=The composition itself can still be created with a parameterless constructor, so a host that instantiates it by convention keeps working.
13+
$f=The parameter appears on every root of that composition that needs the base setup, which makes the call site explicit about what it passes.
14+
$f=Prefer `SetupContextKind.Argument` ([setup context](dependent-compositions-with-setup-context.md)) when the state is fixed for the composition lifetime, so callers of the roots do not have to thread the context through.
1915
*/
2016

2117
// ReSharper disable ClassNeverInstantiated.Local

0 commit comments

Comments
 (0)