feat: wrap registered services in decorator chains via [Decorate<TDecorator, TService>]#40
Conversation
…vice, TDecorator>] A service can now be decorated with [Decorate<IService, LoggingDecorator>], so every consumer of IService receives LoggingDecorator(inner) instead of the bare implementation: the decorator's single constructor parameter assignable to the service type is handed the decorated registration, its other parameters resolve from the graph as usual, and the decorator inherits the decorated registration's lifetime. Multiple decorators of one service chain in declaration order — last declared is outermost — so [Decorate<IService, D1>] then [Decorate<IService, D2>] yields D2(D1(Real)); an optional Order property positions a decorator explicitly rather than by declaration order. Decorators are pure model-building over the existing keyed-resolution plumbing — no new resolution emission. A new BuildDecoratorChains step runs after coalescing: for each decorated service it locates the base implementation(s), moves each base impl onto a synthetic key (__dec:S:k:0), registers every decorator as an instance whose inner parameter is redirected to the next-lower link's synthetic key, and rewrites the public unkeyed winner to the outermost decorator. The existing keyed resolver and Names emission then produce new D2(new D1(new Real())) for free, and the inner edge is an ordinary direct dependency so AWT102 cycle and AWT105 captive analysis work unchanged. Decorators are unbypassable through collections. Because serviceMembers is populated during coalescing from the unkeyed base registration while BuildDecoratorChains runs afterwards and moves the base impl onto a synthetic key, the step rewrites collection membership: a single base impl is replaced by the chain's outermost decorator, so a collection view yields exactly one element — the full chain — never the bare implementation; a service with several registrations is decorated member by member, so IEnumerable<IService> over [Real1, Real2] decorated by D resolves to [D(Real1), D(Real2)] while the public single-dispatch winner is the decorated first-wins impl. Wrapping several base implementations needs distinct instances of one decorator type, which the single-instance-per-type model could not express, so InstanceModel gains an EmitType: a decorator chain link carries a synthetic ImplementationType identity (so it is keyed, cached and collection-tracked as a distinct instance) while EmitType holds the real decorator type to construct. The emitter constructs and types instances through the new ConstructedType (EmitType ?? ImplementationType); every other use of ImplementationType remains the instance identity, so ordinary registrations are unaffected. Two diagnostics guard the form. AWT123 (error) reports a [Decorate] that names a service with no registration to decorate. AWT124 (error) reports a decorator whose constructor has no — or more than one ambiguous — parameter assignable to the decorated service type, so Awaiten cannot tell which parameter receives the inner instance. AWK->AWT mapping: the prototype's AWK201/AWK202 land here as AWT123/AWT124 (the next free ids on main). Adds runtime behavior tests across all target frameworks, source-generator tests for the emitted chained dispatch, AWT123 and AWT124, and updates the public-API approval baselines for the new DecorateAttribute<,>.
🚀 Benchmark ResultsDetails
Details
Details
|
Resolve a base-typed inner parameter through the decorated service key rather than the parameter's own declared type: chain links are registered only under the decorated service type, so a decorator whose inner parameter is typed as a base of the service (e.g. Dec(IBase inner) decorating IService) previously looked up a (baseType, syntheticKey) pair that was never registered and crashed the generator with a KeyNotFoundException. DecoratorInner now carries the decorated service type and the redirect rewrites both the parameter's service type and key. Validate the decorator against the same constructor the container actually builds by delegating to SelectConstructor, instead of an independent public-only greediest-by-arity choice. A divergent choice could leave the inner parameter un-redirected and the link resolving itself into a cycle; the shared routine also accepts internal same-assembly constructors. Disambiguate the inner parameter as the most-derived assignable constructor parameter rather than rejecting any decorator with more than one parameter implicitly convertible from the service. A plain object (or shared base) sibling parameter no longer falsely reports AWT124; only a genuine tie between equally-derived assignable parameters is ambiguous. Document that the decorator and the implementation it wraps are each owned by the container and disposed independently, outermost first, so a decorator that also disposes its inner double-disposes it. Add runtime tests for scoped-lifetime, async-initializable, base-typed-inner and disposal-order decorators, and generator tests for the object-sibling and internal-constructor cases.
…-parameter order Extract the decorator-chain construction out of the single deeply nested BuildDecoratorChains method into a DecoratorChainBuilder that holds the mutable graph state as fields, so each step reads as a small focused method (GroupByService, ProcessService, ResolveInnerParameterTypes, WrapBaseImpl, AddChainLink, MoveBaseToSyntheticKey, RewriteCollectionMembership). This drops the method's cognitive complexity below the threshold and its parameter count from eight to seven. Extract the decorator inner-parameter redirect from ClassifyParameters into RedirectDecoratorInner to bring that method back under the complexity threshold too. Flip the type-parameter order of [Decorate] from <TService, TDecorator> to <TDecorator, TService> so the concrete type comes first, matching the lifetime attributes ([Transient<TImplementation, TService>] and friends). Consistency across the registration surface outweighs matching the service-first convention some other libraries use, and the feature is unreleased so the change costs nothing downstream. Update the generator's positional read of the attribute type arguments, all decorator registrations in the tests, and the public API baselines accordingly. Suppress S2326 on DecorateAttribute (its type parameters are the generator's input, read via Roslyn symbols) following the same pattern as the lifetime attributes, and give the test Clock an instance field so its Now() stays a genuine instance member rather than a static utility.
…aking into diagnostics Move SingleInnerParameterType, DecoratorKey and DecoratorIdentity - all used only by DecoratorChainBuilder - inside that class, so the chain-building surface is self-contained rather than spread across free-standing statics on the generator. SingleInnerParameterType becomes an instance method reading the builder's existing _containerSymbol/_serviceToImpl/_compilation fields instead of re-passing them. DecoratorKeyPrefix stays on the enclosing type because the shared DisplayInstance helper needs it too and an enclosing type cannot see a nested type's private member. Render decorator chain links by their real type in diagnostics. A chain link carries a synthetic '<type>@__dec:...' identity, which previously leaked verbatim into AWT101 and AWT102 messages (e.g. 'MyCode.Deco@__dec:MyCode.IService:0:1'); a new DisplayInstance helper trims the synthetic suffix so an error names 'MyCode.Deco'. Exclude a [FromKey] parameter from inner-parameter selection. A keyed parameter deliberately selects a specific keyed registration, so it is a separate dependency, never the chain inner (which is redirected by key and would ignore the [FromKey] anyway). A decorator whose only service-assignable parameter is [FromKey]-ed now reports a clear AWT124 rather than resolving the inner to an unregistered keyed service and surfacing a confusing AWT101 against the synthetic identity, and a decorator may now legitimately take a keyed service as a side dependency alongside its unkeyed inner. Document that only the unkeyed registration of a service is decorated. Add generator tests for the keyed-inner AWT124 and for diagnostics naming the real decorator type rather than the synthetic identity.
…tic, not just AWT101/AWT102 The prior change routed AWT101 and AWT102 through DisplayInstance but missed the other diagnostics that can name a decorator chain link: a link inherits its base's lifetime (so a singleton decorator with a scoped side-dependency reports AWT105 captive dependency), and a link is a collection member and can be async-tainted (so it can surface in the AWT120/AWT122 async-path diagnostics). Route every instance-identity render that can name a link through DisplayInstance so the synthetic '@__dec:' identity never leaks. The two remaining Display(info.ImplementationType) sites (AWT100 no-accessible-constructor, AWT119 parameterized-lifetime) are left untouched because a chain link cannot reach them - its constructor is validated before the chain is built and it is never a parameterized [Arg] service. DisplayInstance is a no-op on ordinary identities, so non-decorator diagnostics are unaffected. Add a generator test that a singleton decorator captively holding a scoped side-dependency reports AWT105 naming the real decorator type with no synthetic identity leaked.
[Decorate<TService, TDecorator>][Decorate<TDecorator, TService>]
…coratorInner When RedirectDecoratorInner was extracted out of ClassifyParameters, it was inserted directly beneath the ClassifyParameters XML-doc summary, leaving that method with two stacked <summary> blocks - the first describing parameter classification and AWT101, the second describing the inner-parameter redirect - and leaving ClassifyParameters itself undocumented. Move the classification summary back down onto ClassifyParameters so each method carries only its own summary. Documentation only; no behavior change.
|
…ator chains via `[Decorate<TDecorator, TService>]` (#40) by Valentin Breuß
…ator chains via `[Decorate<TDecorator, TService>]` (#40) by Valentin Breuß



A service can now be decorated with
[Decorate<LoggingDecorator, IService>], so every consumer ofIServicereceivesLoggingDecorator(inner)instead of the bare implementation: the decorator's single constructor parameter assignable to the service type is handed the decorated registration, its other parameters resolve from the graph as usual, and the decorator inherits the decorated registration's lifetime. Multiple decorators of one service chain in declaration order — last declared is outermost — so[Decorate<IService, D1>]then[Decorate<IService, D2>]yieldsD2(D1(Real)); an optional Order property positions a decorator explicitly rather than by declaration order.Decorators are pure model-building over the existing keyed-resolution plumbing — no new resolution emission. A new
BuildDecoratorChainsstep runs after coalescing: for each decorated service it locates the base implementation(s), moves each base impl onto a synthetic key, registers every decorator as an instance whose inner parameter is redirected to the next-lower link's synthetic key, and rewrites the public unkeyed winner to the outermost decorator. The existing keyed resolver and Names emission then producenew D2(new D1(new Real()))for free, and the inner edge is an ordinary direct dependency so AWT102 cycle and AWT105 captive analysis work unchanged.Decorators are unbypassable through collections. Because serviceMembers is populated during coalescing from the unkeyed base registration while
BuildDecoratorChainsruns afterwards and moves the base impl onto a synthetic key, the step rewrites collection membership: a single base impl is replaced by the chain's outermost decorator, so a collection view yields exactly one element — the full chain — never the bare implementation; a service with several registrations is decorated member by member, soIEnumerable<IService>over[Real1, Real2]decorated byDresolves to[D(Real1), D(Real2)]while the public single-dispatch winner is the decorated first-wins impl.Wrapping several base implementations needs distinct instances of one decorator type, which the single-instance-per-type model could not express, so InstanceModel gains an EmitType: a decorator chain link carries a synthetic ImplementationType identity (so it is keyed, cached and collection-tracked as a distinct instance) while EmitType holds the real decorator type to construct. The emitter constructs and types instances through the new ConstructedType (EmitType ?? ImplementationType); every other use of ImplementationType remains the instance identity, so ordinary registrations are unaffected.
Two diagnostics guard the form. AWT123 (error) reports a
[Decorate]that names a service with no registration to decorate. AWT124 (error) reports a decorator whose constructor has no — or more than one ambiguous — parameter assignable to the decorated service type, so Awaiten cannot tell which parameter receives the inner instance.AWK->AWT mapping: the prototype's AWK201/AWK202 land here as AWT123/AWT124 (the next free ids on main). Adds runtime behavior tests across all target frameworks, source-generator tests for the emitted chained dispatch, AWT123 and AWT124, and updates the public-API approval baselines for the new
DecorateAttribute<,>.