Skip to content

feat: add Func<T> and Lazy<T> relationship types#21

Merged
vbreuss merged 5 commits into
mainfrom
feat/relationship-types
Jun 27, 2026
Merged

feat: add Func<T> and Lazy<T> relationship types#21
vbreuss merged 5 commits into
mainfrom
feat/relationship-types

Conversation

@vbreuss

@vbreuss vbreuss commented Jun 27, 2026

Copy link
Copy Markdown
Member

Constructor parameters (and direct resolutions) typed as Func<TService> or Lazy<TService> are now satisfied automatically by synthesizing a factory or lazy that resolves TService from the owning container or scope. Func<T> returns a fresh resolve on each call, respecting the target's lifetime; Lazy<T> memoizes per owner instance. The underlying service must still be registered, otherwise AWT101 fires on it.

Relationship-typed parameters defer resolution, so they are excluded from the dependency graph used for cycle detection (AWT102) and captive-dependency analysis (AWT105): a Func<T>/Lazy<T> breaks what would otherwise be a cycle and does not capture a shorter-lived dependency. Constructor selection unwraps Func/Lazy so a relationship parameter counts as resolvable when its target is registered.

The constructor-parameter model gains a DependencyKind and is captured as ParameterModel rather than a bare service-type string, and the static dispatch table now also maps Func<T>/Lazy<T> over each service so they are resolvable directly, not only via constructor injection.

No new diagnostic is introduced. The prototype referenced this feature against AWK101/AWK102/AWK103; on main those map to AWT101 (missing dependency), AWT102 (cycle) and AWT105 (captive dependency).

Constructor parameters (and direct resolutions) typed as Func<TService> or Lazy<TService> are now satisfied automatically by synthesizing a factory or lazy that resolves TService from the owning container or scope. Func<T> returns a fresh resolve on each call, respecting the target's lifetime; Lazy<T> memoizes per owner instance. The underlying service must still be registered, otherwise AWT101 fires on it.

Relationship-typed parameters defer resolution, so they are excluded from the dependency graph used for cycle detection (AWT102) and captive-dependency analysis (AWT105): a Func<T>/Lazy<T> breaks what would otherwise be a cycle and does not capture a shorter-lived dependency. Constructor selection unwraps Func/Lazy so a relationship parameter counts as resolvable when its target is registered.

The constructor-parameter model gains a DependencyKind and is captured as ParameterModel rather than a bare service-type string, and the static dispatch table now also maps Func<T>/Lazy<T> over each service so they are resolvable directly, not only via constructor injection.

No new diagnostic is introduced. The prototype referenced this feature against AWK101/AWK102/AWK103; on main those map to AWT101 (missing dependency), AWT102 (cycle) and AWT105 (captive dependency).
@vbreuss vbreuss self-assigned this Jun 27, 2026
@vbreuss vbreuss added the enhancement New feature or request label Jun 27, 2026
@github-actions

github-actions Bot commented Jun 27, 2026

Copy link
Copy Markdown

Test Results

 18 files  ± 0   18 suites  ±0   29s ⏱️ -1s
 76 tests +12   75 ✅ +12  1 💤 ±0  0 ❌ ±0 
368 runs  +56  367 ✅ +56  1 💤 ±0  0 ❌ ±0 

Results for commit fc47d00. ± Comparison against base commit c39ceed.

♻️ This comment has been updated with latest results.

@github-actions

github-actions Bot commented Jun 27, 2026

Copy link
Copy Markdown

🚀 Benchmark Results

Details

BenchmarkDotNet v0.15.8, Linux Ubuntu 24.04.4 LTS (Noble Numbat)
Intel Xeon Platinum 8370C CPU 2.80GHz (Max: 2.79GHz), 1 CPU, 4 logical and 2 physical cores
.NET SDK 10.0.301
[Host] : .NET 10.0.9 (10.0.9, 10.0.926.27113), X64 RyuJIT x86-64-v4

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

Resolve Size Mean Error StdDev Ratio Allocated Alloc Ratio
baseline* 8 9.886 ns 0.0182 ns 0.0142 ns 1.26 - NA
Awaiten 8 7.876 ns 0.0323 ns 0.0286 ns 1.00 - NA
MsDI 8 6.909 ns 0.0067 ns 0.0062 ns 0.88 - NA
Autofac 8 101.821 ns 0.9938 ns 0.9296 ns 12.93 656 B NA
Jab 8 3.198 ns 0.0043 ns 0.0036 ns 0.41 - NA
PureDI 8 5.107 ns 0.0342 ns 0.0320 ns 0.65 - NA
DryIoc 8 7.388 ns 0.0136 ns 0.0128 ns 0.94 - NA
SimpleInjector 8 12.577 ns 0.1932 ns 0.1807 ns 1.60 - NA
baseline* 256 10.104 ns 0.0096 ns 0.0085 ns 0.92 - NA
Awaiten 256 11.039 ns 0.0245 ns 0.0229 ns 1.00 - NA
MsDI 256 6.928 ns 0.0109 ns 0.0097 ns 0.63 - NA
Autofac 256 103.341 ns 1.5242 ns 1.4257 ns 9.36 656 B NA
Jab 256 80.645 ns 0.0330 ns 0.0292 ns 7.31 - NA
PureDI 256 8.107 ns 0.0443 ns 0.0414 ns 0.73 - NA
DryIoc 256 7.230 ns 0.0055 ns 0.0051 ns 0.65 - NA
SimpleInjector 256 12.518 ns 0.0217 ns 0.0192 ns 1.13 - NA
Details

BenchmarkDotNet v0.15.8, Linux Ubuntu 24.04.4 LTS (Noble Numbat)
Intel Xeon Platinum 8370C CPU 2.80GHz (Max: 2.79GHz), 1 CPU, 4 logical and 2 physical cores
.NET SDK 10.0.301
[Host] : .NET 10.0.9 (10.0.9, 10.0.926.27113), 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 24.945 ns 1.0158 ns 0.9501 ns 0.97 160 B 1.00
Awaiten 8 25.64 ns 0.621 ns 0.581 ns 1.00 160 B 1.00
MsDI 8 1,765.20 ns 29.636 ns 27.722 ns 68.87 5688 B 35.55
Autofac 8 24,555.46 ns 130.702 ns 122.259 ns 958.02 33092 B 206.82
Jab 8 11.37 ns 0.298 ns 0.279 ns 0.44 96 B 0.60
PureDI 8 17.42 ns 0.538 ns 0.477 ns 0.68 128 B 0.80
DryIoc 8 745.94 ns 2.602 ns 2.307 ns 29.10 1528 B 9.55
SimpleInjector 8 13,454.90 ns 134.310 ns 119.062 ns 524.93 24760 B 154.75
baseline* 256 95.150 ns 0.8314 ns 0.6943 ns 0.65 2144 B 1.00
Awaiten 256 146.66 ns 10.638 ns 9.431 ns 1.00 2144 B 1.00
MsDI 256 15,842.97 ns 129.009 ns 114.363 ns 108.42 61016 B 28.46
Autofac 256 567,886.68 ns 5,303.307 ns 4,140.473 ns 3,886.25 737260 B 343.87
Jab 256 179.59 ns 16.126 ns 15.084 ns 1.23 2080 B 0.97
PureDI 256 142.92 ns 10.151 ns 9.495 ns 0.98 2112 B 0.99
DryIoc 256 44,177.26 ns 154.676 ns 144.684 ns 302.32 80626 B 37.61
SimpleInjector 256 387,280.29 ns 3,408.793 ns 3,021.806 ns 2,650.30 572305 B 266.93

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

@github-actions

Copy link
Copy Markdown

👽 Mutation Results

Mutation testing badge

Awaiten

Details
File Score Killed Survived Timeout No Coverage Ignored Compile Errors Runtime Errors Total Detected Total Undetected Total Mutants

The final mutation score is NaN%

Coverage Thresholds: high:80 low:60 break:0

vbreuss added 4 commits June 27, 2026 19:18
…ship diagnostics

Building the static dispatch table emitted a Func<T> and Lazy<T> entry for every service unconditionally, so an explicitly registered relationship type (e.g. a registered Lazy<T>) collided with the synthetic entry, producing a duplicate dictionary key that threw from the static initializer at first resolve. BuildDispatchEntries now adds explicit service registrations first and skips any synthetic relationship entry whose key is already claimed, so explicit registrations win their slot and the table never contains a duplicate key.

ClassifyParameter unwrapped a single level of Func<T>/Lazy<T> regardless of what the inner type was, so a relationship over another relationship (Func<Func<T>>) was reported as a missing registration of the inner Func<T> even though that is resolvable. It now leaves a nested relationship as a direct dependency so the diagnostic names the full unregistered service type; one level of nesting is supported.

Display stripped only a leading global:: alias, leaking it into diagnostics for generic type arguments (System.Func<global::MyCode.Leaf>). It now strips every occurrence so messages render the alias-free type.

Adds tests for the duplicate-key fix, the nested-relationship diagnostic, and the documented behavior that a singleton's Func<Scoped> resolves from the root container rather than a later child scope.
Comments that explained why an assertion holds are folded into the assertion's .Because(...) reason so the rationale travels with the failure message instead of sitting in a detached code comment. Covers the relationship, dispatch-table and lifetime tests. Comments that describe arrange/setup or type declarations are left in place. Also reorders a few test members alphabetically with no behavioral change.
… entries

Service types are unique across instances because registrations are coalesced, so the seen.Add guard on the direct-registration pass was always true. Seed the seen set explicitly and add each entry unconditionally; the synthetic Func<T>/Lazy<T> pass still checks seen so an explicit relationship registration keeps its dispatch slot. Resolves the lone-if loop that the analyzer flagged without pushing a side-effecting predicate into a Where call.
@vbreuss vbreuss force-pushed the feat/relationship-types branch from fc47d00 to f0f1db5 Compare June 27, 2026 17:52
@vbreuss vbreuss merged commit d3d0f30 into main Jun 27, 2026
9 checks passed
@vbreuss vbreuss deleted the feat/relationship-types branch June 27, 2026 17:53
@sonarqubecloud

Copy link
Copy Markdown

github-actions Bot added a commit that referenced this pull request Jun 27, 2026
…ionship types (#21) by Valentin Breuß
github-actions Bot added a commit that referenced this pull request Jun 27, 2026
…ionship types (#21) by Valentin Breuß
@github-actions

Copy link
Copy Markdown

This is addressed in release v0.1.0.

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

Labels

enhancement New feature or request state: released

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant