Summary
Let [Scan] carry OnActivated and OnRelease, and let the hook be generic over an open generic marker's type arguments.
[Container]
[Scan(typeof(IView<>),
As = ScanAs.Self | ScanAs.Marker | ScanAs.MatchingInterface,
Lifetime = AwaitenLifetime.Scoped,
OnActivated = nameof(WireView))]
public static partial class App
{
private static void WireView<TViewModel>(IView<TViewModel> view, TViewModel viewModel)
=> view.DataContext = viewModel;
}
Motivation
Lifecycle hooks are a per-registration feature. Since #110 their parameters resolve from the graph, which makes them capable of real wiring rather than just calling a method on the instance. [Scan] cannot reach them. Its properties are AssignableTo, Lifetime, As, SkipUnconstructable, InAssembliesOf, NamePatterns, NamespacePatterns and Exclude.
So a scanned family where every match needs the same activation step has no scan-shaped answer. The two available options are:
- One explicit registration per type, each repeating the same
OnActivated. This is the per-type list the scan exists to avoid, and it is worse than the list the scan replaced, because now every line carries a hook name too.
- Move the step out of the container into a hand-written factory. This puts composition logic somewhere the graph analysis cannot see, and it has to rediscover by reflection what the container already knew.
Neither is good, and the family stops being scannable for a reason that has nothing to do with how it is registered.
The motivating case
A WPF application binds each view to its view model. Every view implements IView<TViewModel>, so the view model is the marker's type argument.
Under Autofac this is an activation handler on the scan:
builder.RegisterAssemblyTypes(assembly)
.AsClosedTypesOf(typeof(IView<>))
.AsSelf()
.AsImplementedInterfaces()
.OnActivating(OnActivatingView)
.InstancePerLifetimeScope();
private static void OnActivatingView(IActivatingEventArgs<object> args)
{
Type viewModelType = args.Instance.GetType()
.GetInterfaces()
.Where(x => x.IsGenericType && x.GetGenericTypeDefinition() == typeof(IView<>))
.Select(x => x.GenericTypeArguments.Single())
.Single();
object viewModel = args.Context.Resolve(viewModelType);
((IView)args.Instance).DataContext = viewModel;
}
That code reflects over the instance to recover TViewModel, then resolves it out of the activation context. Both halves are things Awaiten already knows at compile time and would rather nobody did at runtime.
The generic hook
The interesting half of this request is the second sentence of the summary, not the first.
ScanAs.Marker over an unbound generic already computes each match's closed marker interface. For MainWindow : IView<IMainViewModel> the generator knows the closing is IView<IMainViewModel> and therefore that TViewModel is IMainViewModel. So it can bind a generic hook to that closing and emit a direct call:
private static void WireView<TViewModel>(IView<TViewModel> view, TViewModel viewModel)
=> view.DataContext = viewModel;
per match, as WireView<IMainViewModel>(mainWindow, resolvedMainViewModel). No reflection, no activation context, no cast.
This is strictly better than the Autofac version it replaces, and not only because it is faster:
TViewModel becomes a real graph edge. Cycle analysis, captive dependency analysis and async taint analysis all cover it. Under Autofac the view to view model dependency is invisible to the container, because it is a Resolve call inside a delegate.
- A view whose view model is unregistered fails with AWT101 at build time rather than at navigation time.
- The reflective
.Single() above throws at runtime for a view implementing IView<> twice. The generic binding makes that a build error instead.
Without the generic form, a scan hook still helps a family whose activation step needs nothing from the marker's type arguments, and the doc's existing advice to type the parameter as object when one hook serves several implementations would apply. But the generic form is what makes the feature answer the open generic scan, which is the case that most wants it, since an open generic marker is precisely the shape where every match differs only by a type argument the generator already resolved.
Why this fits
The scanning page says to reach for a scan for "a large, uniform family that grows on its own". Uniformity is exactly what makes a shared activation hook correct: the step is the same for every match because the family is uniform. A scan that cannot express the family's uniform activation step forces that family back onto a per-type list, which is the churn the feature exists to remove.
Nothing here weakens the composition root. The hook is named on the scan and its body sits on the container, next to the registration, so the wiring stays as visible as any other. It is the reflective handler in the Autofac version that is opaque, and this replaces it.
Diagnostics
Most of what is needed already exists.
- AWT166 already covers conflicting coalesced
OnActivated/OnRelease/Eager directives across registrations of one implementation, which is the shape of an explicit registration and a scan disagreeing about a match's hook.
- AWT189 (a hook parameter cannot be a runtime
[Arg]) and AWT191 (a release hook parameter cannot be Func<T>/Lazy<T>) apply unchanged.
- AWT101 covers an unresolvable hook parameter.
New cases worth a diagnostic:
- A generic hook whose arity does not match the marker's, mirroring AWT125 for decorators.
- A closing whose type arguments violate the hook's generic constraints. AWT126 skips such a closing for decorators with a warning; the same treatment seems right here, since the rest of the family still registers.
- A non-generic hook named on an open generic scan is fine and should stay legal, since a hook that needs nothing from the type arguments is a real case.
Open questions
- Should the first parameter be typed as the marker's closing (
IView<TViewModel>), the concrete match, or object? Allowing all three, as the per-registration hook already does, seems right, but the concrete match cannot be named by a single hook across a family, so in practice it is the other two.
- Should a per-type explicit registration's hook replace the scan's hook or run in addition to it? Replacement matches the existing rule that an explicit registration wins single resolution, and AWT166 already exists to catch the ambiguity, so replacement with a diagnostic is probably right.
- Does
OnRelease on a scan pull its weight, or is OnActivated alone the real request? Symmetry argues for both, but the motivating case only needs activation, and a scanned family of release hooks has lifetime safety consequences (the disposal page notes a release hook makes a transient behave like a disposable one for strict lifetime safety) that are worth thinking about separately.
- Should a scan hook be allowed to be generic when the marker is not generic? There is nothing to bind the type parameter to, so presumably not.
Context
Encountered while evaluating Awaiten as a replacement for Autofac in a WPF application. The view to view model binding above is the last piece of that container's wiring with no Awaiten equivalent, and it is on the critical path of every view creation, so it is not something the application can route around cheaply.
That is the motivating case rather than the justification. The argument for the feature is that activation is a property of a family, not of a registration syntax, and that a scan which cannot express it sends uniform families back to per-type lists for no reason the user can see.
Summary
Let
[Scan]carryOnActivatedandOnRelease, and let the hook be generic over an open generic marker's type arguments.Motivation
Lifecycle hooks are a per-registration feature. Since #110 their parameters resolve from the graph, which makes them capable of real wiring rather than just calling a method on the instance.
[Scan]cannot reach them. Its properties areAssignableTo,Lifetime,As,SkipUnconstructable,InAssembliesOf,NamePatterns,NamespacePatternsandExclude.So a scanned family where every match needs the same activation step has no scan-shaped answer. The two available options are:
OnActivated. This is the per-type list the scan exists to avoid, and it is worse than the list the scan replaced, because now every line carries a hook name too.Neither is good, and the family stops being scannable for a reason that has nothing to do with how it is registered.
The motivating case
A WPF application binds each view to its view model. Every view implements
IView<TViewModel>, so the view model is the marker's type argument.Under Autofac this is an activation handler on the scan:
That code reflects over the instance to recover
TViewModel, then resolves it out of the activation context. Both halves are things Awaiten already knows at compile time and would rather nobody did at runtime.The generic hook
The interesting half of this request is the second sentence of the summary, not the first.
ScanAs.Markerover an unbound generic already computes each match's closed marker interface. ForMainWindow : IView<IMainViewModel>the generator knows the closing isIView<IMainViewModel>and therefore thatTViewModelisIMainViewModel. So it can bind a generic hook to that closing and emit a direct call:per match, as
WireView<IMainViewModel>(mainWindow, resolvedMainViewModel). No reflection, no activation context, no cast.This is strictly better than the Autofac version it replaces, and not only because it is faster:
TViewModelbecomes a real graph edge. Cycle analysis, captive dependency analysis and async taint analysis all cover it. Under Autofac the view to view model dependency is invisible to the container, because it is aResolvecall inside a delegate..Single()above throws at runtime for a view implementingIView<>twice. The generic binding makes that a build error instead.Without the generic form, a scan hook still helps a family whose activation step needs nothing from the marker's type arguments, and the doc's existing advice to type the parameter as
objectwhen one hook serves several implementations would apply. But the generic form is what makes the feature answer the open generic scan, which is the case that most wants it, since an open generic marker is precisely the shape where every match differs only by a type argument the generator already resolved.Why this fits
The scanning page says to reach for a scan for "a large, uniform family that grows on its own". Uniformity is exactly what makes a shared activation hook correct: the step is the same for every match because the family is uniform. A scan that cannot express the family's uniform activation step forces that family back onto a per-type list, which is the churn the feature exists to remove.
Nothing here weakens the composition root. The hook is named on the scan and its body sits on the container, next to the registration, so the wiring stays as visible as any other. It is the reflective handler in the Autofac version that is opaque, and this replaces it.
Diagnostics
Most of what is needed already exists.
OnActivated/OnRelease/Eagerdirectives across registrations of one implementation, which is the shape of an explicit registration and a scan disagreeing about a match's hook.[Arg]) and AWT191 (a release hook parameter cannot beFunc<T>/Lazy<T>) apply unchanged.New cases worth a diagnostic:
Open questions
IView<TViewModel>), the concrete match, orobject? Allowing all three, as the per-registration hook already does, seems right, but the concrete match cannot be named by a single hook across a family, so in practice it is the other two.OnReleaseon a scan pull its weight, or isOnActivatedalone the real request? Symmetry argues for both, but the motivating case only needs activation, and a scanned family of release hooks has lifetime safety consequences (the disposal page notes a release hook makes a transient behave like a disposable one for strict lifetime safety) that are worth thinking about separately.Context
Encountered while evaluating Awaiten as a replacement for Autofac in a WPF application. The view to view model binding above is the last piece of that container's wiring with no Awaiten equivalent, and it is on the critical path of every view creation, so it is not something the application can route around cheaply.
That is the motivating case rather than the justification. The argument for the feature is that activation is a property of a family, not of a registration syntax, and that a scan which cannot express it sends uniform families back to per-type lists for no reason the user can see.