feat: add async factory methods as a Task<T> async-init registration channel#34
Conversation
🚀 Benchmark ResultsDetails
Details
Details
|
…channel Adds a second, statically-visible async-registration channel parallel to IAsyncInitializable: a Factory method may return Task<T> or ValueTask<T> and is matched against the unwrapped result type T. An async-returning factory is an async-taint source independent of whether T implements IAsyncInitializable, so the container reaches its result only by awaiting the factory call. This lets a factory author declare async-ness honestly without leaking the concrete type, and makes the synchronous path structurally impossible (a Task cannot be unwrapped synchronously) so AWT119 and strict withholding fall out for free with no special-casing. ContainerRegistrations.FindFactoryCandidates now matches the AWT108 "returns the registered type" check against the unwrapped T (ProducedType / IsAsyncFactoryReturn), and BuildInstance computes disposability and IAsyncInitializable from that same produced T rather than the Task. The new InstanceModel.IsAsyncFactory seeds PropagateAsyncTaint alongside IsAsyncInitializable, and InstanceModel.IsAsyncSource generalizes the AWT119-vs-AWT120 split and the AWT121 parameterized guard to either taint source. The emitter awaits the factory call (await Create(...).ConfigureAwait(false)) only on the async construction path, which is the only path an async-tainted service is reached on; the existing memoized-Task machinery caches the awaited result for singletons and scoped services, and a transient async-factory is async-only. ValueTask<T> is included: it is matched by canonical metadata symbol (absent on netstandard2.0, where GetTypeByMetadataName returns null and the branch is skipped), and the emitted await is uniform for Task and ValueTask so no TFM gating of generated code is needed. A parameterized async factory (Task<T> with [Arg] parameters) reuses the broadened AWT121, since it is reachable only through a synchronous Func<TArg..., T> that cannot await.
b6df015 to
b6962f5
Compare
…ethods A factory method may now take a System.Threading.CancellationToken parameter; the container forwards the resolve-time token (the async creator's cancellationToken; default on a synchronous path) instead of treating it as a graph dependency. The new DependencyKind.CancellationToken contributes no dependency edge and no async taint, mirroring [Arg], and is excluded from the AWT101 missing-dependency check. Forwarding is scoped to factory methods - a constructor has no ambient resolve-time token, so its CancellationToken parameter stays an ordinary (and therefore unregistered) dependency. Also tighten the async-factory surface from review: the synchronous-resolution guidance message now names the async Task<T> factory channel alongside IAsyncInitializable, and a runtime test covers disposal of an async-factory-produced IDisposable hidden behind a non-disposable service interface (previously only asserted at the generated-source level).
…educe complexity Restrict resolve-time CancellationToken forwarding to asynchronous factory methods. Only an async factory is constructed on the async path where that token is in scope; a synchronous factory (like a constructor) has no ambient token, so a forwarded CancellationToken would always have received default silently. Such a parameter now stays an ordinary graph dependency and is reported as AWT101 when unregistered rather than compiling to a silent default. Also resolve two maintainability findings. Extract the Lazy/Func relationship classification out of ClassifyParameter into a ClassifyRelationship helper, dropping its cognitive complexity well below the threshold while preserving behavior on every path. And remove the now-dead synchronous branch of the emitted CancellationToken argument: a CancellationToken dependency only ever arises from an async factory, which is built solely on the async path, so the argument is always the in-scope token.
|
…sk<T> async-init registration channel (#34) by Valentin Breuß
…sk<T> async-init registration channel (#34) by Valentin Breuß



Adds a second, statically-visible async-registration channel parallel to
IAsyncInitializable: a Factory method may returnTask<T>orValueTask<T>and is matched against the unwrapped result typeT. An async-returning factory is an async-taint source independent of whetherTimplementsIAsyncInitializable, so the container reaches its result only by awaiting the factory call. This lets a factory author declare async-ness honestly without leaking the concrete type, and makes the synchronous path structurally impossible (aTaskcannot be unwrapped synchronously) so AWT119 and strict withholding fall out for free with no special-casing.ContainerRegistrations.FindFactoryCandidatesnow matches the AWT108 "returns the registered type" check against the unwrappedT(ProducedType/IsAsyncFactoryReturn), andBuildInstancecomputes disposability andIAsyncInitializablefrom that same producedTrather than theTask. The newInstanceModel.IsAsyncFactoryseedsPropagateAsyncTaintalongsideIsAsyncInitializable, andInstanceModel.IsAsyncSourcegeneralizes the AWT119-vs-AWT120 split and the AWT121 parameterized guard to either taint source. The emitter awaits the factory call (await Create(...).ConfigureAwait(false)) only on the async construction path, which is the only path an async-tainted service is reached on; the existing memoized-Task machinery caches the awaited result for singletons and scoped services, and a transient async-factory is async-only.ValueTask<T>is included: it is matched by canonical metadata symbol (absent on netstandard2.0, whereGetTypeByMetadataNamereturns null and the branch is skipped), and the emitted await is uniform forTaskandValueTaskso no TFM gating of generated code is needed. A parameterized async factory (Task<T>with[Arg]parameters) reuses the broadened AWT121, since it is reachable only through a synchronousFunc<TArg..., T>that cannot await.