Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
113 changes: 100 additions & 13 deletions Docs/pages/09-diagnostics.md
Original file line number Diff line number Diff line change
Expand Up @@ -1332,19 +1332,7 @@ public static class EquipmentModule

### AWT154

:::danger[Error]
An imported module declares a `[Scan]`, which is not collected from modules.
:::

```csharp
[Module]
[Scan<IDrink>] // scans are not gathered from modules
public static class MenuModule;

[Container]
[Import(typeof(MenuModule))]
public static partial class CoffeeShop;
```
*Retired.* A `[Scan]` on a `[Module]` is now supported: the module compiles its own scan in its own build (see [self-compiled module scans](./registration/modules#self-compiled-scans)), so it is no longer rejected on import. The self-compilation constraints are reported as [AWT194](#awt194)–[AWT197](#awt197).

### AWT155

Expand All @@ -1367,6 +1355,105 @@ public static class ModuleB;
public static partial class CoffeeShop;
```

### AWT194

:::danger[Error]
A `[Module]` that declares a `[Scan]` is not `partial`, so its scan cannot be self-compiled.
:::

```csharp
[Module]
[Scan<IPlugin>(As = ScanAs.MatchingInterface)]
public static class PluginModule; // must be partial to receive the generated factories
```

A [self-compiled module scan](./registration/modules#self-compiled-scans) emits a factory method and a registration attribute into the module's partial. Add the `partial` modifier. Reported in the module's own build.

### AWT195

:::danger[Error]
A self-compiled module `[Scan]` match has a constructor parameter of a type inaccessible outside the module's assembly.
:::

```csharp
internal sealed class Secret;
internal sealed class Roaster(Secret secret) : IRoaster; // Secret is internal

[Module]
[Scan<IRoaster>(As = ScanAs.MatchingInterface)]
public static partial class PluginModule;
```

The generated factory is a `public` method whose parameters are resolved from the *consuming* container's graph, so each parameter type has to be nameable by the consumer. Widen the parameter type's accessibility, or exclude the match. (A v1 limitation: a self-compiled scan cannot construct a match through an inaccessible parameter.)

### AWT196

:::danger[Error]
A self-compiled module `[Scan]` match has no exposure interface accessible outside the module's assembly.
:::

```csharp
internal sealed class Roaster : IPlugin;

[Module]
[Scan<IPlugin>(As = ScanAs.Self)] // Self exposes the internal type, which a consumer cannot name
public static partial class PluginModule;
```

A consumer resolves a self-compiled match only through an accessible interface. `ScanAs.Self` over an `internal` implementation exposes nothing nameable; expose it through a public interface (`ScanAs.MatchingInterface` or `ScanAs.Marker`), or exclude the match.

### AWT197

:::danger[Error]
A self-compiled module `[Scan]` match would be exposed under more than one interface.
:::

```csharp
internal sealed class Roaster : IPlugin, IRoaster;

[Module]
[Scan<IPlugin>(As = ScanAs.Marker | ScanAs.MatchingInterface)] // IPlugin and IRoaster both apply
public static partial class PluginModule;
```

A self-compiled match is reached through a generated factory that returns a single accessible interface, so a shared instance across several interfaces cannot be expressed (unlike a container `[Scan]`, whose matches coalesce on the concrete type). Narrow the exposure to a single interface (typically `ScanAs.MatchingInterface`), or exclude the match. This is a v1 limitation.

### AWT200

:::danger[Error]
A self-compiled module `[Scan]` match carries injection metadata the generated factory cannot mirror.
:::

```csharp
internal sealed class Roaster : IRoaster
{
public Roaster(IClock clock) { }

[Inject] // keys, optionality and deferral live on this attribute
public IGrinder Grinder { get; set; }
}

[Module]
[Scan<IRoaster>(As = ScanAs.MatchingInterface)]
public static partial class PluginModule;
```

The generated factory reduces a match to a plain parameter list resolved from the consuming container's graph. An `[Inject]` property, or a constructor parameter marked `[Inject]` or `[Arg]`, carries per-dependency semantics that plain parameters cannot express, so the consumer would silently construct the match differently than a container `[Scan]` would. Remove the attribute, exclude the match, or register the type through a hand-written module factory.

### AWT201

:::danger[Error]
A generic `[Module]` (or one nested in a generic type) declares a `[Scan]`.
:::

```csharp
[Module]
[Scan<IPlugin>(As = ScanAs.MatchingInterface)]
public static partial class PluginModule<T>; // no closed PluginModule<T> exists to [Import]
```

A consumer imports a module by `typeof`, so there is no single closed module type to import from a generic declaration, and the generated partial could not re-open it by its bare name. Move the `[Scan]` onto a non-generic module.

## Keyed collections

### AWT159
Expand Down
4 changes: 4 additions & 0 deletions Docs/pages/registration/07-scanning.md
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,10 @@ An explicit registration of a scanned type wins over the scan, so you can specia
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](../design-principles#when-power-becomes-a-smell).
:::

## 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](./modules#self-compiled-scans).

## Where to go next

- [Modules](./modules) to group registrations for reuse.
Expand Down
23 changes: 22 additions & 1 deletion Docs/pages/registration/08-modules.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,9 +54,30 @@ public static class ProductionModule
}
```

## Self-compiled scans

A library often keeps its implementations `internal` and exposes only interfaces. A consuming container cannot construct an inaccessible type, so it could never register one — unless the library hand-wrote a factory per type. A `[Scan]` on a `[Module]` closes that gap: the module compiles its own scan **in its own build**, emitting a factory per match that constructs the implementation (which its own assembly can see) and returns the accessible interface. A consuming container reads each match like a container [`[Scan]`](./scanning) match, so a self-compiled scan behaves as close to a container scan as the assembly boundary allows.

```csharp
public interface IClock;
public interface IPlugin;
public interface IRoaster;
internal sealed class Roaster(IClock clock) : IPlugin, IRoaster; // stays internal

[Module]
[Scan<IPlugin>(As = ScanAs.MatchingInterface, Lifetime = AwaitenLifetime.Singleton)]
public static partial class PluginModule; // partial, so the generator can add the factory
```

A consuming container `[Import]`s the module and resolves `IRoaster` without ever naming `Roaster`. Each match is registered like a container scan match: an explicit registration of the same service in the container (or another module) **overrides** it, and when several matches expose the **same** interface they **collect** — a `[Scan<IPlugin>(As = ScanAs.Marker)]` over two internal plug-ins resolves as `IEnumerable<IPlugin>`, exactly as it would on a container. Because diagnostics are reported in the *library's* build, the library author — not the consumer — sees any problem.

The module must be `partial` ([AWT194](../diagnostics#awt194)). A few v1 limitations apply, each reported at the library's source: a single match exposes through exactly one accessible interface ([AWT196](../diagnostics#awt196)/[AWT197](../diagnostics#awt197)) — several matches under one interface still collect, but one match cannot share a single instance across several interfaces the way a container scan can — and a match's constructor parameters must be types a consumer can name ([AWT195](../diagnostics#awt195)) without carrying `[Inject]`/`[Arg]` metadata the factory could not mirror ([AWT200](../diagnostics#awt200)). The module itself must be non-generic ([AWT201](../diagnostics#awt201)).

When the module lives in the **same assembly as the container**, there is no assembly boundary to bridge: the container expands the module's `[Scan]` directly, exactly as if it were declared on the container itself, with full container-scan semantics (multi-interface exposure, `SkipUnconstructable`, direct construction of `internal` matches). The self-compiled factories are still generated into the module, so the same module keeps working for any *other* assembly that imports it; but note that the v1 limitations above are checked in the module's build regardless, since the module cannot know whether a cross-assembly consumer exists.

## One level deep

Imports are not transitive. A module's own `[Import]` is not followed, and a `[Scan]` inside a module is not collected. Keep the container as the single place that composes modules.
Imports are not transitive. A module's own `[Import]` is not followed. Keep the container as the single place that composes modules. (A module's own `[Scan]`, by contrast, does reach the importing container: expanded directly by the container for a same-assembly module, or self-compiled in the module's build across assemblies — see above.)

*Note: importing a type that is not a `[Module]` is an error ([AWT149](../diagnostics#awt149)), and a module must be static ([AWT152](../diagnostics#awt152)).*

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,6 @@
AWT151 | Awaiten | Warning | An imported module declares no registrations
AWT152 | Awaiten | Error | An imported [Module] class is not declared static
AWT153 | Awaiten | Error | A module Factory/Instance member is not accessible from the generated container
AWT154 | Awaiten | Error | An imported module declares a [Scan], which is not collected from modules
AWT155 | Awaiten | Warning | Two imported modules strongly register the same service with different implementations
AWT156 | Awaiten | Warning | A generated Root/Scope is disposed synchronously although its container owns a service that implements IAsyncDisposable but not IDisposable
AWT157 | Awaiten | Error | An [Inject(Optional = true)] property is required and cannot be omitted from the object initializer
Expand Down Expand Up @@ -95,3 +94,9 @@
AWT191 | Awaiten | Error | An OnRelease hook parameter is a Func/Lazy relationship, which would defer resolution past the owner's teardown
AWT192 | Awaiten | Error | SuppressDisposal is set on a pre-built Instance, which the container does not own or dispose, so it has no effect
AWT193 | Awaiten | Warning | A [Scan] matched a type that is inaccessible to the generated container, so it is not registered
AWT194 | Awaiten | Error | A [Module] that declares a [Scan] is not partial, so its scan cannot be self-compiled
AWT195 | Awaiten | Error | A self-compiled module [Scan] match has a constructor parameter type inaccessible outside the module's assembly
AWT196 | Awaiten | Error | A self-compiled module [Scan] match has no exposure interface accessible outside the module's assembly
AWT197 | Awaiten | Error | A self-compiled module [Scan] match would be exposed under multiple interfaces, which a single-exposure factory cannot express
AWT200 | Awaiten | Error | A self-compiled module [Scan] match carries [Inject]/[Arg] metadata the generated factory cannot mirror
AWT201 | Awaiten | Error | A generic [Module] (or one nested in a generic type) declares a [Scan], so no closed module exists for a consumer to import
Loading
Loading