Skip to content
Merged
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
51 changes: 51 additions & 0 deletions Docs/pages/09-diagnostics.md
Original file line number Diff line number Diff line change
Expand Up @@ -448,6 +448,57 @@ public static partial class CoffeeShop
}
```

### AWT189

:::danger[Error]
A lifecycle hook parameter (after the instance) is marked `[Arg]`, but a hook resolves its parameters from the graph.
:::

```csharp
[Container]
[Singleton<EspressoMachine>(OnActivated = nameof(Calibrate))]
public static partial class CoffeeShop
{
private static void Calibrate(EspressoMachine machine, [Arg] int count) { } // no Func<…> call site supplies an [Arg]
}
```

### AWT190

:::danger[Error]
A lifecycle hook (`OnActivated` / `OnRelease`) names an overloaded method, so the container cannot choose which one to call.
:::

The container reaches a hook by simple name, so two accepting overloads leave the choice (and the graph dependencies the extra parameters resolve) order-dependent. Give the hook a unique name, exactly as a factory method must be unambiguous.

```csharp
[Container]
[Singleton<EspressoMachine>(OnActivated = nameof(Calibrate))]
public static partial class CoffeeShop
{
private static void Calibrate(EspressoMachine machine) { }
private static void Calibrate(EspressoMachine machine, Settings settings) { } // which one runs?
}
```

### AWT191

:::danger[Error]
An `OnRelease` hook parameter is a `Func`/`Lazy` relationship, which would defer resolution past the owner's teardown.
:::

A release dependency is captured at construction, but a `Func<T>` or `Lazy<T>` captures only a resolver delegate. The hook runs while its owner is being disposed, so invoking the delegate there always throws. Take the dependency directly instead: it is resolved at construction and, released in reverse creation order, still alive when the hook uses it. An `OnActivated` hook may take `Func`/`Lazy` parameters freely, since it runs while the owner is alive.

```csharp
[Container]
[Singleton<BufferPool>]
[Transient<Buffer>(OnRelease = nameof(ReturnToPool))]
public static partial class CoffeeShop
{
private static void ReturnToPool(Buffer buffer, Func<BufferPool> pool) { } // pool() would throw during teardown; take BufferPool directly
}
```

## Runtime arguments

### AWT113
Expand Down
22 changes: 22 additions & 0 deletions Docs/pages/lifetime/05-lifecycle-hooks.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,28 @@ You can type the parameter as `object` instead when one hook serves several impl

`OnActivated` runs once, right after construction. For an async service it runs before `InitializeAsync`. `OnRelease` runs when the owner or scope is disposed, before the instance's own `Dispose`, and in reverse creation order.

## Hook parameters

Both hooks may declare parameters after the instance. The first parameter is always the instance; every parameter after it is resolved from the container graph, exactly like a constructor or factory parameter. Calibrate the machine from injected settings; return a pooled buffer to the pool it came from.

```csharp
[Container]
[Singleton<Settings>]
[Singleton<BufferPool>]
[Singleton<EspressoMachine>(OnActivated = nameof(Calibrate))]
[Transient<Buffer>(Factory = nameof(Rent), OnRelease = nameof(ReturnToPool))]
public static partial class CoffeeShop
{
private static void Calibrate(EspressoMachine machine, Settings settings) => machine.Calibrate(settings);
private static Buffer Rent(BufferPool pool) => pool.Rent();
private static void ReturnToPool(Buffer buffer, BufferPool pool) => pool.Return(buffer);
}
```

An activation dependency is resolved inline as the hook is called. A release dependency is resolved at construction and captured by value into the queued closure, so the hook holds the instance it was queued for even if a later resolve fails and rolls back. Reverse creation-order teardown keeps a captured dependency (a singleton pool, say) alive until after the release that uses it has run.

Hook parameters participate in the graph like any other dependency: an unregistered one is [AWT101](../diagnostics#awt101), and they are covered by cycle, captive-dependency and async-taint analysis. A hook parameter cannot be a runtime `[Arg]` (there is no `Func<…>` call site to supply one), which is [AWT189](../diagnostics#awt189). A release hook parameter also cannot be a `Func<T>` or `Lazy<T>`: those capture a resolver delegate rather than a value, and by the time the hook runs the owner is already tearing down, so invoking the delegate would throw. That is [AWT191](../diagnostics#awt191); an activation hook may take them freely.

## Ordering and failure

Activation happens before initialization and before the instance is ever handed out, so a concurrent resolve never sees a service that has not been activated. If `OnActivated` throws, the instance is not published. A later resolve builds a fresh one and tries again. Release is only queued once activation succeeds.
Expand Down
3 changes: 3 additions & 0 deletions Source/Awaiten.SourceGenerators/AnalyzerReleases.Unshipped.md
Original file line number Diff line number Diff line change
Expand Up @@ -90,3 +90,6 @@
AWT186 | Awaiten | Error | An Owned<T> relationship (or its Func/Task forms) targets a service produced by a requesting-type factory, which has no owner scope
AWT187 | Awaiten | Warning | A [Scan(As = ScanAs.MatchingInterface)] matched a type implementing several same-named convention interfaces, so it registers under each
AWT188 | Awaiten | Warning | A scan match's only exposure interface is inaccessible to the generated container, so the match is not registered
AWT189 | Awaiten | Error | A lifecycle hook parameter (after the instance) is marked [Arg], but a hook resolves its parameters from the graph
AWT190 | Awaiten | Error | A lifecycle hook (OnActivated / OnRelease) names an overloaded method, so the container cannot choose which one to call
AWT191 | Awaiten | Error | An OnRelease hook parameter is a Func/Lazy relationship, which would defer resolution past the owner's teardown
6 changes: 5 additions & 1 deletion Source/Awaiten.SourceGenerators/AwaitenAnalyzer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -225,8 +225,12 @@ private static void AddAccumulatingFuncs(
DiagnosticDescriptor descriptor = strict ? Diagnostics.RootAccumulatingFactoryStrict : Diagnostics.RootAccumulatingFactory;
DiagnosticSeverity? severity = strict ? DiagnosticSeverity.Error : null;

// A lifecycle hook's Func<T> parameter is rooted too - an activation hook of a root-owned instance can
// invoke it, each call tracking a fresh disposable on the root - so an accumulating fresh-disposable Func
// reached through one is reported like a constructor parameter's. (A release hook's Func is rejected
// outright by AWT191, so only activation hooks reach here with one in practice.)
InstanceModel holder = graph.Instances[node];
foreach (ParameterModel parameter in holder.ConstructorParameters.AsArray())
foreach (ParameterModel parameter in holder.ConstructorParameters.AsArray().Concat(holder.HookParameters()))
{
if (!IsRootAccumulatingFunc(graph, serviceToIndex, membership, parameter) || !reported.Add($"{node}|{parameter.ServiceType}|{parameter.Key}"))
{
Expand Down
58 changes: 31 additions & 27 deletions Source/Awaiten.SourceGenerators/AwaitenGenerator.Analysis.cs
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,10 @@ private static void PushFreshTransientDependencies(
CollectionMembership membership,
Stack<int> stack)
{
foreach (ParameterModel parameter in instance.ConstructorParameters.AsArray())
// A lifecycle hook's graph-resolved parameters are built as part of constructing the instance too (an
// activation dependency inline, a release dependency captured), so a transient disposable reached through
// one is a fresh disposable of this build like a constructor parameter's.
foreach (ParameterModel parameter in instance.ConstructorParameters.AsArray().Concat(instance.HookParameters()))
{
if (parameter.Kind is DependencyKind.Enumerable or DependencyKind.AsyncEnumerable or DependencyKind.AwaitedEnumerable)
{
Expand Down Expand Up @@ -282,6 +285,15 @@ private static Dictionary<int, List<int>> BuildEdges(
AddParameterEdges(member.Dependency, serviceToImpl, implToIndex, serviceMembers, keyedMembers, includeEagerBare, nodeEdges);
}

// A lifecycle hook's parameters (after the instance) are graph dependencies too: the activation hook
// resolves them during the owner's construction (an inline argument) and the release hook captures them
// then. Both are resolved eagerly like a Direct constructor parameter, so AddParameterEdges classifies
// them identically - contributing to cycle (AWT102), captive (AWT105) and async-taint analysis.
foreach (ParameterModel parameter in instances[i].HookParameters())
{
AddParameterEdges(parameter, serviceToImpl, implToIndex, serviceMembers, keyedMembers, includeEagerBare, nodeEdges);
}

edges[i] = nodeEdges;
}

Expand Down Expand Up @@ -437,15 +449,12 @@ private static void DetectSynchronousAsyncResolution(
{
for (int i = 0; i < instances.Count; i++)
{
foreach (ParameterModel parameter in instances[i].ConstructorParameters.AsArray())
// A constructor parameter, an injected [Inject] member and a lifecycle hook parameter all resolve a
// synchronous relationship's target without awaiting initialization, so all are checked alike.
foreach (ParameterModel parameter in instances[i].WalkedDependencies())
{
CheckDependency(i, parameter);
}

foreach (MemberModel member in instances[i].InjectedMembers.AsArray())
{
CheckDependency(i, member.Dependency);
}
}

void CheckDependency(int consumer, ParameterModel parameter)
Expand Down Expand Up @@ -509,15 +518,12 @@ private static void DetectOwnedOverRequestingTypeFactory(
{
for (int i = 0; i < instances.Count; i++)
{
foreach (ParameterModel parameter in instances[i].ConstructorParameters.AsArray())
// A constructor parameter, an injected [Inject] member and a lifecycle hook parameter share the same
// structural incompatibility (the emit path has no owned form for any of them), so all are checked alike.
foreach (ParameterModel parameter in instances[i].WalkedDependencies())
{
CheckDependency(i, parameter);
}

foreach (MemberModel member in instances[i].InjectedMembers.AsArray())
{
CheckDependency(i, member.Dependency);
}
}

void CheckDependency(int consumer, ParameterModel parameter)
Expand Down Expand Up @@ -577,17 +583,13 @@ private static void DetectSynchronousAsyncCollection(

for (int i = 0; i < instances.Count; i++)
{
foreach (ParameterModel parameter in instances[i].ConstructorParameters.AsArray())
// A constructor parameter, an injected [Inject] member (deferred or not) and a lifecycle hook
// parameter all materialize a collection through the same synchronous expression, so all are
// checked alike.
foreach (ParameterModel parameter in instances[i].WalkedDependencies())
{
CheckCollection(i, parameter);
}

// An injected [Inject] collection member (deferred or not) is materialized through the same
// synchronous expression as a constructor parameter, so it is checked the same way.
foreach (ParameterModel dependency in instances[i].InjectedMembers.AsArray().Select(member => member.Dependency))
{
CheckCollection(i, dependency);
}
}

void CheckCollection(int consumer, ParameterModel dependency)
Expand Down Expand Up @@ -710,8 +712,9 @@ private static void ValidateRuntimeArguments(

// A parameterized async service is built fresh per call AND must await initialization, so its path is
// Func<TArg…, Task<T>>. Misuse is caught at the consumption site (AWT119 or AWT115), not the
// registration, so there is no registration-time diagnostic for [Arg]-plus-async.
foreach (ParameterModel parameter in instance.ConstructorParameters.AsArray())
// registration, so there is no registration-time diagnostic for [Arg]-plus-async. A hook parameter is a
// graph dependency too, so a plain hook parameter over a parameterized target is AWT115 like a constructor's.
foreach (ParameterModel parameter in instance.ConstructorParameters.AsArray().Concat(instance.HookParameters()))
{
// A collection resolves to a set of members, not a single registration whose [Arg] parameters could
// be supplied, so runtime-argument matching does not apply (excluded explicitly). Guard the
Expand Down Expand Up @@ -841,13 +844,14 @@ private static void ReportCapturedScoped(
}
}

// The service key the parent's constructor used to reach this dependency (the alias the developer wrote,
// including any [FromKey]). Falls back to the first service key, or the implementation type when the
// dependency exposes no service of its own, so the diagnostic still names it.
// The service key the parent used to reach this dependency (the alias the developer wrote, including any
// [FromKey]) - across its constructor parameters and lifecycle hook parameters. Falls back to the first
// service key, or the implementation type when the dependency exposes no service of its own, so the
// diagnostic still names it.
static ServiceKey ReferencedService(InstanceModel parent, InstanceModel dependency)
{
ServiceKey[] dependencyServices = dependency.Services.AsArray();
foreach (ParameterModel parameter in parent.ConstructorParameters.AsArray())
foreach (ParameterModel parameter in parent.ConstructorParameters.AsArray().Concat(parent.HookParameters()))
{
ServiceKey key = KeyOf(parameter);
if (dependencyServices.Contains(key))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -315,6 +315,13 @@ private static void ReportUnconsumedExternalServices(
{
RecordExternalConsumption(member.Dependency, consumed);
}

// A lifecycle hook parameter consumes an external dependency too, so an import used only by a hook is
// not unconsumed (AWT176 would otherwise wrongly advise removing it).
foreach (ParameterModel parameter in instance.HookParameters())
{
RecordExternalConsumption(parameter, consumed);
}
}

LocationInfo? location = LocationInfo.From(containerSymbol.Locations.FirstOrDefault());
Expand Down
Loading
Loading