Skip to content

Support interceptors on decorator registrations #66

Description

@tillig

Summary

Allow a decorator registration to be intercepted using the same fluent syntax we already use for normal registrations:

builder.RegisterType<StringMethodInterceptor>();

// The decorator gets intercepted, using familiar chained syntax.
builder
    .RegisterDecorator<Decorator, IService>()
    .EnableInterfaceInterceptors()
    .InterceptedBy(typeof(StringMethodInterceptor));

// The decorated type can be intercepted independently (existing syntax).
builder
    .RegisterType<Implementation>()
    .EnableInterfaceInterceptors()
    .InterceptedBy(typeof(SomeOtherInterceptor))
    .As<IService>();

Background / motivation

Since Autofac v5, the resolve pipeline changed such that an interceptor attached to a service registration wraps the innermost (decorated) instance, not the outermost decorator. This is discussed at length in autofac/Autofac#1416. We are not proposing to change that order-of-operations behavior. Instead we want to let people attach interception directly to the decorator registration, so the interception point is explicit and reads naturally ("the decorator is intercepted by X") rather than relying on interceptors "magically" moving during decoration.

PR #55 attempted this with a standalone builder.EnableInterfaceInterceptors<TService>() call disconnected from the decorator registration. That syntax was rejected in review as confusing/inconsistent — it isn't clear what is intercepting what, or how it relates to RegisterType interception.

Why this works mechanically

Tracing the core resolve pipeline: when DecoratorMiddleware resolves the decorator, it does so via a new ResolveRequest that preserves the decorator's own registration, and RegistrationPipelineInvokeMiddleware invokes context.Registration.ResolvePipeline. Interface interception attaches its middleware at the Activation phase on the registration pipeline. So if interception middleware is present on the decorator registration's pipeline, it executes and wraps the (outermost) decorator instance. No core runtime change is needed — only a way to configure that pipeline.

The blocker (requires a core Autofac change)

The modern RegisterDecorator<TDecorator, TService>() returns void and builds + snapshots the decorator registration internally (CreateRegistration() clones the pipeline). There is no IRegistrationBuilder to chain .EnableInterfaceInterceptors() onto, and the pipeline is frozen by the time the method returns.

This needs a core change so decorator registration returns a configurable builder. The plan for that is captured in autofac/Autofac#1416. Once core exposes a decorator builder with ConfigurePipeline (and metadata for InterceptedBy), the work in this repo is small, since EnableInterfaceInterceptors/InterceptedBy are already extension methods.

Scope & open questions for the implementation

  • Closed-type decorators first. Open-generic decorators (RegisterGenericDecorator) build the closed decorator registration lazily at resolve time (OpenGenericDecoratorMiddlewareSource), so there's no registration-time builder to configure. Recommend deferring open-generic support to a follow-up.
  • Feature parity to aim for (from the PR Add intercept on decorator #55 review):
    • ProxyGenerationOptions support on the decorator
    • [Intercept] attribute selection on the decorator type
    • A different interceptor for the decorator vs. the decorated type (good acceptance test)
    • Sensible behavior for class-vs-interface interception combinations
  • Reusable cleanup regardless of this feature: extract the duplicated proxy-application logic out of RegistrationExtensions into a shared ProxyHelpers (as PR Add intercept on decorator #55 did). This can land independently.
  • Process note: new tests should use NSubstitute, not Moq.

Dependencies

Blocked on the core Autofac change tracked in autofac/Autofac#1416, which is a breaking change and must ship in a major core release. This issue tracks the downstream feature; PR #55 is the original prototype.

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