Skip to content

Support open-generic decorators (and composites) #79

Description

@vbreuss

Problem

[Decorate<TDecorator, TService>] accepts only closed types. There is no typeof-form analogous to the open-generic lifetime registrations ([Transient(typeof(Repository<>), typeof(IRepository<>))]), so a decorator cannot be declared once for every closing of an open-generic service:

// Not possible today:
[Decorate(typeof(LoggingBehavior<>), typeof(IHandler<>))]

Decorating IHandler<T> / IValidator<T> / IRepository<T> across all closings is the primary decorator use case in modern .NET (pipeline behaviors à la MediatR). SimpleInjector, Autofac and Scrutor all support open-generic decoration; its absence is the most visible feature gap against them. The same limitation applies to [Composite<TComposite, TService>].

Proposed design

  1. Add a non-generic DecorateAttribute(Type decorator, Type service) (and CompositeAttribute(Type composite, Type service)) accepting unbound open generics, mirroring the existing typeof-form of the lifetime attributes. Reuse the AWT127 rule: the typeof-form must receive unbound open generics; a closed pair belongs on the generic attribute form.
  2. During open-generic expansion (which already synthesizes closed registrations on demand), each synthesized closing of the decorated service also closes the decorator with the same type arguments and appends it to that closed service's decorator chain. The chain builder already operates per closed service and per resolution key, so the wrapping machinery needs no structural change — only the chain collection must learn to contribute open-declared decorators to every matching closing.
  3. Ordering: open-declared and closed-declared decorators of the same closed service interleave by the existing (Order, declaration order) rule, so a closed-form decorator can wrap inside or outside an open-form one deterministically.

Diagnostics

  • Arity mismatch between the open decorator and the open service (analogue of AWT125).
  • A closing that violates the open decorator's type-parameter constraints: decide between skipping that closing silently, skipping with a warning (AWT141 precedent), or erroring (AWT126 precedent). Leaning toward error, matching how explicit registrations treat constraint violations — a decorator silently absent for one closing is a subtle behavioral hole.
  • The existing decorator diagnostics (AWT123 no registration to wrap, AWT124 no inner parameter, AWT135 [FromServices] inner) must fire per closing with the closing named in the message.

Acceptance criteria

  • [Decorate(typeof(LoggingBehavior<>), typeof(IHandler<>))] wraps every closed IHandler<T> registration, including scan matches, open-generic-synthesized registrations, keyed registrations and contextual (WhenInjectedInto) resolutions.
  • Collections (IEnumerable<IHandler<Foo>>) see the decorated members, matching closed-decorator behavior.
  • Interleaving with closed decorators of the same service respects Order.
  • Composites gain the same typeof-form.

Open questions

  • Should an open decorator apply to closings synthesized later in the pipeline (variance redirections)? Presumably yes — decoration should be indistinguishable from the closed-declared case.
  • Partial arity (decorator with fewer type parameters than the service) is out of scope for the first cut; require identical arity and declaration-order type parameters, as AWT128 already does for implementations.

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions