Skip to content

feat: support lifecycle hooks on [Scan], with generic dispatch#118

Open
vbreuss wants to merge 4 commits into
mainfrom
feat/scan-lifecycle-hooks
Open

feat: support lifecycle hooks on [Scan], with generic dispatch#118
vbreuss wants to merge 4 commits into
mainfrom
feat/scan-lifecycle-hooks

Conversation

@vbreuss

@vbreuss vbreuss commented Jul 17, 2026

Copy link
Copy Markdown
Member

A [Scan] can now name OnActivated/OnRelease, applied to every match and resolved through the same pipeline as an explicit registration's hook.

For an open-generic marker the hook may be generic: the match's closed marker form binds its type argument, so [Scan(typeof(IView<>), OnActivated = nameof(WireView))] dispatches WireView(view, vm) with the matching view model resolved from the graph and no reflection. A match that closes the marker more than once is ambiguous and reported as AWT198.


A [Scan] can now name OnActivated/OnRelease, applied to every match and
resolved through the same pipeline as an explicit registration's hook.

For an open-generic marker the hook may be generic: the match's closed
marker form binds its type argument, so [Scan(typeof(IView<>), OnActivated
= nameof(WireView))] dispatches WireView<IMainViewModel>(view, vm) with the
matching view model resolved from the graph and no reflection. A match that
closes the marker more than once is ambiguous and reported as AWT198.
@vbreuss vbreuss self-assigned this Jul 17, 2026
@vbreuss vbreuss added the enhancement New feature or request label Jul 17, 2026
@github-actions

github-actions Bot commented Jul 17, 2026

Copy link
Copy Markdown

Test Results

   18 files  ±  0     18 suites  ±0   7m 24s ⏱️ - 1m 0s
1 169 tests + 26  1 168 ✅ + 26  1 💤 ±0  0 ❌ ±0 
5 652 runs  +106  5 651 ✅ +106  1 💤 ±0  0 ❌ ±0 

Results for commit 8b00af8. ± Comparison against base commit 8c555d8.

♻️ This comment has been updated with latest results.

@github-actions

github-actions Bot commented Jul 17, 2026

Copy link
Copy Markdown

🚀 Benchmark Results

Details

BenchmarkDotNet v0.15.8, Linux Ubuntu 24.04.4 LTS (Noble Numbat)
INTEL XEON PLATINUM 8573C 3.48GHz, 1 CPU, 4 logical and 2 physical cores
.NET SDK 10.0.302
[Host] : .NET 10.0.10 (10.0.10, 10.0.1026.32716), X64 RyuJIT x86-64-v4

Job=InProcess Toolchain=InProcessEmitToolchain IterationCount=15
LaunchCount=1 WarmupCount=10

Build Size Mean Error StdDev Ratio Allocated Alloc Ratio
baseline* 8 16.25 ns 0.081 ns 0.072 ns 1.17 144 B 1.00
Awaiten 8 13.894 ns 0.2738 ns 0.2428 ns 1.00 144 B 1.00
MsDI 8 1,204.012 ns 30.2961 ns 28.3390 ns 86.68 5688 B 39.50
Autofac 8 20,655.166 ns 197.2791 ns 174.8828 ns 1,487.03 33090 B 229.79
Jab 8 6.849 ns 0.0720 ns 0.0602 ns 0.49 96 B 0.67
PureDI 8 13.001 ns 0.4293 ns 0.3584 ns 0.94 128 B 0.89
DryIoc 8 548.237 ns 4.5027 ns 3.9915 ns 39.47 1528 B 10.61
SimpleInjector 8 9,441.745 ns 164.7137 ns 146.0144 ns 679.74 24760 B 171.94
baseline* 256 89.40 ns 0.449 ns 0.350 ns 0.97 2128 B 1.00
Awaiten 256 91.711 ns 1.0932 ns 1.0226 ns 1.00 2128 B 1.00
MsDI 256 10,065.798 ns 72.3875 ns 67.7113 ns 109.77 61016 B 28.67
Autofac 256 498,658.719 ns 14,809.3013 ns 13,852.6297 ns 5,437.94 741679 B 348.53
Jab 256 82.866 ns 0.3799 ns 0.2966 ns 0.90 2080 B 0.98
PureDI 256 94.103 ns 6.4494 ns 6.0328 ns 1.03 2112 B 0.99
DryIoc 256 31,926.212 ns 244.5439 ns 190.9238 ns 348.16 80092 B 37.64
SimpleInjector 256 282,556.840 ns 4,052.2131 ns 3,790.4426 ns 3,081.32 571190 B 268.42
Details

BenchmarkDotNet v0.15.8, Linux Ubuntu 24.04.4 LTS (Noble Numbat)
AMD EPYC 7763 2.45GHz, 1 CPU, 4 logical and 2 physical cores
.NET SDK 10.0.302
[Host] : .NET 10.0.10 (10.0.10, 10.0.1026.32716), X64 RyuJIT x86-64-v3

Job=InProcess Toolchain=InProcessEmitToolchain IterationCount=15
LaunchCount=1 WarmupCount=10

Realistic Mean Error StdDev Ratio Allocated Alloc Ratio
baseline* 288.2 ns 0.71 ns 0.63 ns 1.21 568 B 1.00
Awaiten 237.8 ns 1.28 ns 1.14 ns 1.00 568 B 1.00
MsDI 606.0 ns 6.86 ns 6.08 ns 2.55 1104 B 1.94
Autofac 6,576.5 ns 20.72 ns 19.38 ns 27.66 10784 B 18.99
Jab 180.3 ns 1.00 ns 0.94 ns 0.76 432 B 0.76
DryIoc 392.8 ns 1.67 ns 1.56 ns 1.65 944 B 1.66
SimpleInjector 738.9 ns 4.03 ns 3.77 ns 3.11 1096 B 1.93
PureDI 180.7 ns 0.84 ns 0.79 ns 0.76 632 B 1.11
Details

BenchmarkDotNet v0.15.8, Linux Ubuntu 24.04.4 LTS (Noble Numbat)
AMD EPYC 7763 2.45GHz, 1 CPU, 4 logical and 2 physical cores
.NET SDK 10.0.302
[Host] : .NET 10.0.10 (10.0.10, 10.0.1026.32716), X64 RyuJIT x86-64-v3

Job=InProcess Toolchain=InProcessEmitToolchain IterationCount=15
LaunchCount=1 WarmupCount=10

Resolve Size Mean Error StdDev Ratio Allocated Alloc Ratio
baseline* 8 5.738 ns 0.6620 ns 0.6193 ns 0.82 1.01 -
Awaiten 8 7.003 ns 0.0232 ns 0.0193 ns 1.00 - NA
MsDI 8 7.275 ns 0.0122 ns 0.0102 ns 1.04 - NA
Autofac 8 144.395 ns 2.5723 ns 2.4061 ns 20.62 656 B NA
Jab 8 2.843 ns 0.0105 ns 0.0093 ns 0.41 - NA
PureDI 8 5.444 ns 0.0071 ns 0.0063 ns 0.78 - NA
DryIoc 8 8.313 ns 0.0398 ns 0.0353 ns 1.19 - NA
SimpleInjector 8 10.506 ns 0.0193 ns 0.0171 ns 1.50 - NA
baseline* 256 6.043 ns 0.0063 ns 0.0059 ns 0.73 1.00 -
Awaiten 256 8.284 ns 0.0423 ns 0.0375 ns 1.00 - NA
MsDI 256 7.225 ns 0.0128 ns 0.0113 ns 0.87 - NA
Autofac 256 136.407 ns 1.4379 ns 1.3450 ns 16.47 656 B NA
Jab 256 43.004 ns 0.0902 ns 0.0844 ns 5.19 - NA
PureDI 256 7.376 ns 0.0278 ns 0.0246 ns 0.89 - NA
DryIoc 256 8.126 ns 0.0246 ns 0.0218 ns 0.98 - NA
SimpleInjector 256 13.980 ns 0.0163 ns 0.0145 ns 1.69 - NA

baseline* rows show the corresponding Awaiten benchmark from the most recent successful main branch build with results, for regression comparison.

vbreuss added 3 commits July 17, 2026 17:05
…t falsely rejected

A generic lifecycle hook on an open-generic [Scan] marker binds its type
argument from the match's single closed marker form. AWT198 (ambiguous marker)
was decided during scan expansion, where only the hook name is known, so it
fired for any match closing the marker more than once even when the named hook
was non-generic and needed no type argument, and it kept only the first-seen
closing per implementation.

Move the AWT198 decision to ResolveHook, where the method arity is known: a
generic hook with more than one distinct closing is ambiguous, a non-generic
one is unaffected. Collect the closed marker forms in ScanHookMarkers instead
of judging them, and union them across every registration of an implementation
in coalescing, so two open-generic scans binding the same generic hook to
different closings surface as AWT198 rather than an order-dependent silent
dispatch.
…s (AWT199)

Review of the scan lifecycle hook feature surfaced several coalescing and binding defects, fixed here:

- Track closed marker forms per hook slot (OnActivated/OnRelease) instead of one per-implementation union, so a generic hook is only considered ambiguous among registrations naming the same hook in the same slot. Two scans hooking different slots of the same type no longer trigger a false AWT198.
- Merge scan hooks across scans: a later scan fills a slot the winning scan left unset, so an OnActivated scan and an OnRelease scan matching the same type both apply. Same-slot conflicts with different methods now report the new AWT199 warning (first scan wins), mirroring AWT142.
- Validate generic constraints before constructing a hook method, so a constraint-violating closing falls through to AWT164 instead of leaking a CS error into the generated source.
- Decide AWT198 from actual bindability: closings whose arity, constraints, or first parameter cannot bind no longer count, so unusable generic hooks report AWT164 and constraints can settle a formerly ambiguous match down to a single bindable form.
- Reword the AWT198 message to name the hook and the actual set of bindable closed forms, which is accurate for both the single-marker double-closing and the cross-marker case.

Explicit-over-scan override intentionally stays diagnostic-free: an explicit registration replaces a scan registration wholesale, hooks included, which is the sanctioned remediation path for AWT198. The scanning docs now state this and no longer claim AWT166 covers hook conflicts.

Adds regression tests for each fix plus runtime coverage for generic OnRelease hooks, arity-2 markers, base-class open markers, and the two-scan hook merge.
…plicit override corner

A review follow-up pass: add a generator test proving that two markers closing to the identical type argument dedupe into a single constructed hook dispatch with no AWT198, and extend the scanning docs to state that an explicit registration of just the concrete type strips the scan hooks even where the scan still supplies its marker mapping.
@sonarqubecloud

Copy link
Copy Markdown

Quality Gate Failed Quality Gate failed

Failed conditions
3 New issues

See analysis details on SonarQube Cloud

💡 Need a hand with PR review? Try Gitar by Sonar!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant