Scanning
Some sets of services grow over time. Every drink on the menu implements IDrink. You do not want to add a registration line each time you add a drink. A scan registers every type that matches a marker, and it does it at compile time by reading metadata, so there is still no runtime reflection.
Scan for a marker
[Container]
[Scan<IDrink>]
public static partial class CoffeeShop;
Every concrete, public, non-generic type assignable to IDrink is registered. The typeof form is equivalent:
[Scan(typeof(IDrink))]
A type the container cannot name is skipped rather than registered: an implementation that is internal to another assembly (with no InternalsVisibleTo), or a private nested class. Since a match quietly vanishing from the container is almost never what you meant, the scan reports the skip as AWT193. Widening the type to public brings it back. Only a type your scan actually asked for is reported, so the internal plumbing of a scanned assembly stays quiet, as does anything you filtered out. Whether the interfaces a match is exposed under are accessible is a separate question, covered below.
Choose how they register
By default each match registers as itself. Use As to register under the marker interface instead, or both.
[Scan<IDrink>(As = ScanAs.Marker)]
ScanAs is a [Flags] enum: the three exposures are independent and combine with |.
ScanAs flag | Registers each match under |
|---|---|
Self (default) | The concrete type |
Marker | The marker interface |
MatchingInterface | The interface named I + its own name (Foo → IFoo) |
// Resolvable as itself, as a member of IEnumerable<IDrink>, and by its own IEspresso interface.
[Scan<IDrink>(As = ScanAs.Self | ScanAs.Marker | ScanAs.MatchingInterface)]
Match the Foo/IFoo convention
MatchingInterface registers each match under the interface it implements whose name is I + the match's own name, the common .NET convention where Foo implements IFoo. Unlike a wide "as implemented interfaces" registration, it never binds a match to an incidental interface like IDisposable, so the composition graph stays legible.
[Scan<IViewModel>(As = ScanAs.MatchingInterface)]
Here MainViewModel : IViewModel, IMainViewModel registers only under IMainViewModel. The match must genuinely implement the convention interface (it is selected from the type's implemented interfaces by name, not synthesized), and generic interfaces are not matched. A match that implements no such interface contributes no MatchingInterface registration; when the scan names a marker and the match would otherwise register nothing at all, that is a warning (AWT182), or — when the interface is implemented but inaccessible to the container — a warning naming it (AWT188). (Combined with Self or Marker, the other exposure still registers the match, so no warning is raised.) A match implementing several same-named interfaces with an own-namespace one prefers that one; with none, it registers under each, which a marker scan warns about (AWT187).
Choose the lifetime
Scans register as transient unless you say otherwise.
[Scan<IDrink>(Lifetime = AwaitenLifetime.Singleton)]
Scan other assemblies
By default a scan looks in the container's own assembly. Point it at others with InAssembliesOf.
[Scan(typeof(IDrink), InAssembliesOf = new[] { typeof(SeasonalDrinks) })]
Narrow the matches
Marker assignability is often wider than you want. Three optional filters narrow it, combined with AND:
[Scan<IDrink>(
NamePatterns = ["*Latte", "!Decaf*"], // ends in Latte, but not the Decaf ones
NamespacePatterns = ["CoffeeShop.Menu.**", "!**.Tests"], // under Menu, excluding test namespaces
Exclude = [typeof(DiscontinuedFlatWhite)])] // and never this exact type
NamePatternsglobs the simple type name.*matches any run of characters, so"*Latte"is ends-with,"Iced*"starts-with,"*Pumpkin*"contains, and"Latte"an exact name.NamespacePatternsglobs the namespace and is segment-aware on.:*matches one segment,**matches zero or more. So"CoffeeShop.Menu.**"is that namespace and everything nested beneath it (but not the siblingCoffeeShop.MenuLegacy),"CoffeeShop.Menu.*"its immediate children only, and"**.Tests"any namespace ending in aTestssegment.Excludedrops types by exact identity, so an entry survives a rename and never removes a same-named type elsewhere.
In either pattern list a bare entry includes and a !-prefixed entry excludes; a candidate passes an axis when it matches some include (or the list gives none) and no exclude. Matching is ordinal (case-sensitive). A filter set that removes every match warns with AWT172, a never-applied exclusion with AWT173, and a match-everything include (* or **) with AWT174.
Scan without a marker
Some conventions have no shared marker at all: every Foo has its own IFoo and nothing else in common. The parameterless [Scan] matches every concrete type instead of a marker, narrowed by the same filters.
[Scan(As = ScanAs.MatchingInterface, NamespacePatterns = ["MyApp.Services.**"])]
A markerless scan's As may not include Marker — there is no marker to register under — and it must carry at least one NamePatterns, NamespacePatterns or InAssembliesOf filter so it does not sweep every concrete type in scope. A wildcard-only pattern (*, **.*) does not count: it names nothing, so it does not narrow the sweep. Breaking either rule is an error (AWT183). Because it scans broadly, a type that does not follow the convention is simply skipped rather than warned; if the scan ends up registering nothing at all, that is a warning (AWT184).
Open generic markers
An unbound generic marker matches closed forms, the way Autofac's closed-types-of works. Each match registers under its closed marker interface.
[Scan(typeof(IView<>), As = ScanAs.Marker)]
Lifecycle hooks
A scan can name OnActivated and OnRelease hooks, applied to every match, so a whole family shares one activation or teardown routine without a registration line per type.
[Container]
[Scan<IDrink>(Lifetime = AwaitenLifetime.Singleton, OnActivated = nameof(Prime))]
public static partial class CoffeeShop
{
private static void Prime(IDrink drink) => drink.Prime();
}
The hook's first parameter is the match, so it must accept every one: type it as the scanned marker (or object). Parameters after it are resolved from the graph exactly as for an explicit registration's hook, and the same rules and diagnostics apply: an unusable hook name is AWT164, an unregistered parameter is AWT101.
When two scans match the same type, their hooks merge: one scan's OnActivated combines with another's OnRelease, and both naming the same method is fine. Two scans naming different methods for the same slot contradict each other, so the first scan's method is used and the contradiction is surfaced as AWT199. An explicit registration of a scanned type is different: it replaces the scan's hooks along with everything else, so name the hook on the explicit registration too if the special-cased type should keep it, and leave it off to deliberately opt that type out. The replacement follows the type, not the service: an explicit registration of just the concrete type, say [Transient<Espresso>] beside a marker scan, strips the scan's hooks from Espresso even where the scan still supplies its marker mapping.
Generic hooks on an open marker
An open generic marker knows each match's closed type argument at compile time, so a hook can be generic and receive it directly, with no reflection and no object. The classic case is a WPF-style view/view-model family: bind each view to its matching view model as it is activated.
[Container]
[Singleton<MainViewModel, IMainViewModel>]
[Scan(typeof(IView<>), As = ScanAs.Marker, OnActivated = nameof(WireView))]
public static partial class App
{
private static void WireView<TViewModel>(IView<TViewModel> view, TViewModel viewModel)
=> view.DataContext = viewModel;
}
For MainWindow : IView<IMainViewModel> the generator dispatches WireView<IMainViewModel>(mainWindow, viewModel), resolving the matching IMainViewModel from the graph. The type argument is visible to graph analysis, so an unregistered view model fails the build (AWT101) rather than at runtime. A non-generic method works too (its parameters typed as the marker or object); the type argument only applies when the method is generic.
Because a generic hook takes its type argument from a closed marker form, it needs exactly one it can bind. A match offering several, by closing the marker more than once (Dual : IView<A>, IView<B>), leaves that argument ambiguous and is reported as AWT198: register such a type explicitly with the hook it needs, or split the family so only one closed form binds it. Only forms the hook could actually bind count, so a constraint (where TViewModel : class) that rules out all closings but one settles the choice, as does accessibility: a closing at another assembly's internal type cannot be dispatched and yields to an accessible sibling. If the hook name is overloaded and another overload also accepts the match, the collision is between overloads rather than closings and is reported as AWT190 instead. A non-generic hook takes no type argument, so a match with several closings is fine for it.
Overriding a scanned type
An explicit registration of a scanned type wins over the scan, so you can special-case one drink while scanning the rest.
Note: a scan that matches nothing is a warning (AWT138), not an error, so an empty menu does not break the build.
Prefer explicit registrations. A scan trades away the property that makes the composition root useful: the whole graph visible in one place. Reach for a scan only for a large, uniform family that grows on its own, like message handlers, validators, or plug-ins, where listing each one adds churn without adding clarity. For a handful of services, spell them out, see Design principles.
Scan from a module
A [Scan] may also sit on a [Module], where it is compiled in the module's own build so a library can scan its internal implementations and expose them to consumers through their interfaces. See self-compiled scans.
OnActivated and OnRelease work on a module scan too, and the hook method may stay internal alongside the implementation. The module resolves the hook (closing a generic one over the match's marker) in its own build and emits a small public wrapper next to each factory, so a consumer runs the hook without ever naming the internal method or type. Only the hook's parameters after the instance carry a rule the container form does not: they are resolved from the consuming container's graph, so each type has to be nameable outside the module's assembly, the same requirement a scanned constructor parameter gets. An inaccessible hook dependency is AWT203, and one marked [FromKey] or [Inject] is AWT204, since the wrapper's bare signature cannot carry the attribute across the boundary.
Where to go next
- Modules to group registrations for reuse.
- Open generics for generic service families.