feat: answer IServiceProviderIsService from the container metadata - #133
Conversation
ASP.NET Core consults `IServiceProviderIsService` to decide whether a parameter comes from dependency injection or from the request, and MVC's controller activation consults it too. `AwaitenServiceProvider` did not offer it, so on the provider-replacement path a handler taking a container service was bound from the wrong place instead of failing. It now answers both that and `IServiceProviderIsKeyedService` from `IAwaitenContainerMetadata.Registrations`, constructing nothing. The container dispatches many more shapes than it advertises as registrations, and the rules are not obvious, so each is derived from what the container actually does rather than from a reading of the generator: - a synchronously resolvable registration, and `Task<T>` for an async-only one - the six collection interfaces, a vector array and `IAsyncEnumerable<T>` over a resolvable element type, suppressed when a collection shape of that element type is registered unkeyed - `Task<` collection `>`, which does not require the members to be synchronous - `IReadOnlyDictionary<TKey, T>` for a `string` or enum key, requiring every keyed registration to carry exactly that key type `ProbeAgreement` asserts the invariant that makes the probe worth having — that it agrees with `GetService` — over 22 shapes and 13 containers, on the root and on a scope. That harness found four defects during development that reasoning had missed, including two where the probe promised a service the container withholds. Three divergences remain, each pinned by a test in `AcceptedDivergences` so it cannot drift silently: a collection whose members are not all synchronous is over-reported, a disposable transient is over-reported on the root, and a variance-compatible closing of a generic interface is under-reported. The first is not fixable in the bridge — `Registrations` coalesces the implementations of one service type into a single entry, so a container with one synchronous implementation and one that adds an async-initialized sibling advertise byte-identical metadata and resolve differently. Closing the first and third needs the generator to advertise the shapes it dispatches, which it builds at compile time already. The projection path is unaffected: MS.DI stays the provider there and supplies its own implementation.
🚀 Benchmark ResultsDetails
Details
Details
|
The feature-detection review turned up three gaps in what the probe's contract is actually held to: - An open-generic closing that only a framework asks for is expanded per closing the container's own graph consumes, so it is neither advertised nor resolvable. The probe stays faithful to the container, but this is a fourth divergence from MS.DI (which answers true) and it lands on the silent side: a minimal API told false binds the parameter from the request body. Pinned in AcceptedDivergences and written up in the docs. - IServiceProviderIsKeyedService had no differential harness at all; ProbeAgreement only ever swept IsService. It now takes a key and compares against GetKeyedService, and the keyed surface is swept over the same wide shape list, including the keyed Task<T> projection. - SyncResolveAfterInit is the one input the RequiresAsync rules read, and nothing exercised it. A warmed container now pins the swap: the bare type becomes the service, its Task<T> projection stops being one. Registrations advertises user-keyed entries too, and has since the keyed resolution path, but its summary still said unkeyed - now load-bearing for the whole keyed probe, so it is corrected. Also drops IServiceProviderIsService from the provider's base list, which IServiceProviderIsKeyedService already derives from, matching how IKeyedServiceProvider covers IServiceProvider there. No API change.
The bridge inferred the probe's answer from `Registrations`, re-deriving the generator's synthesis rules in about 150 lines. That inference was where every defect in this feature lived, and two of its answers were not derivable at all: registrations coalesce, so collection members cannot be counted, and the runtime variance fallback resolves closings no registration mentions. `IAwaitenContainerMetadata` gains `IsResolvable(Type, object?)`, emitted from the same dispatch tables `TryResolve` reads, minus the invocation. Exact by construction rather than by inference, so it cannot drift from what resolution does. The bridge now delegates and the inference layer is gone. Three divergences close as a result: - a collection whose members are not all synchronously initializable is no longer over-reported, because the container knows what it synthesized - a variance-compatible closing of a variant generic interface is now reported, because the probe runs the same matching the fallback does - the relationship shapes (`Func<T>`, `Lazy<T>`, `Owned<T>`) are now reported. They were suppressed to mirror what MS.DI answers for shapes it does not have, but nothing reads a false as meaningful: a host told true takes the value from the provider, which here succeeds, so reporting them is strictly better A root-withheld disposable transient is still reported, deliberately: `IsService` answers whether the service exists, which is MS.DI's semantics, and minimal APIs consult the probe on the root at endpoint-build time, so answering false there would misbind every affected parameter. What changes is the other half. Resolving it on the root surfaced `null`; it now surfaces the container's guidance naming the fix, as MS.DI does for a scoping violation, so the failure lands at the cause. `VerifyAgainst` no longer treats a probe's negative as final. It preferred the probe and returned its answer, so an under-reported but resolvable shape became a startup failure instead of falling through to the resolution check. A positive is authoritative; a negative falls through, which costs nothing new. Two divergences from MS.DI remain, both pinned: an open-generic closing nothing in the graph asks for, and `IEnumerable<T>` of an unmentioned element type. In both the probe is faithful to this container, which has no resolution either, so matching MS.DI would mean building a collection for a type unknown at compile time.
GetKeyedService gains the same fallback GetService already has: a keyed service the probe reports but TryResolve declines (a root-withheld disposable transient under a key) now throws the container's guidance instead of returning a silent null the probe's answer contradicts. The variance matching loop moves into a shared __FindVariantMatch, used by the fallback, the resolvability probe (dropping its duplicated loop), and a new step in Resolve's miss path: a variant closing whose nearest candidate is withheld on the Root now surfaces that candidate's guidance rather than claiming no registration exists for a closing the container serves from a child scope. Also merges the accidentally doubled <summary> on EmitKeyedResolutionApi. Both behavior fixes are pinned in FeatureDetectionTests.
|
…` from the container metadata (#133) by Valentin Breuß
…` from the container metadata (#133) by Valentin Breuß



ASP.NET Core consults
IServiceProviderIsServiceto decide whether a parameter comes from dependency injection or from the request, and MVC's controller activation consults it too.AwaitenServiceProviderdid not offer it, so on the provider-replacement path a handler taking a container service was bound from the wrong place instead of failing. It now answers both that andIServiceProviderIsKeyedServicefromIAwaitenContainerMetadata.Registrations, constructing nothing.The container dispatches many more shapes than it advertises as registrations, and the rules are not obvious, so each is derived from what the container actually does rather than from a reading of the generator:
Task<T>for an async-only oneIAsyncEnumerable<T>over a resolvable element type, suppressed when a collection shape of that element type is registered unkeyedTask<collection>, which does not require the members to be synchronousIReadOnlyDictionary<TKey, T>for astringor enum key, requiring every keyed registration to carry exactly that key typeProbeAgreementasserts the invariant that makes the probe worth having — that it agrees withGetService— over 22 shapes and 13 containers, on the root and on a scope. That harness found four defects during development that reasoning had missed, including two where the probe promised a service the container withholds.Three divergences remain, each pinned by a test in
AcceptedDivergencesso it cannot drift silently: a collection whose members are not all synchronous is over-reported, a disposable transient is over-reported on the root, and a variance-compatible closing of a generic interface is under-reported. The first is not fixable in the bridge —Registrationscoalesces the implementations of one service type into a single entry, so a container with one synchronous implementation and one that adds an async-initialized sibling advertise byte-identical metadata and resolve differently. Closing the first and third needs the generator to advertise the shapes it dispatches, which it builds at compile time already.The projection path is unaffected: MS.DI stays the provider there and supplies its own implementation.