From 1fc857a234cf332c573c0d110a5d08e8cc0a74da Mon Sep 17 00:00:00 2001 From: Travis Illig Date: Mon, 15 Jun 2026 11:56:29 -0700 Subject: [PATCH 1/5] Fix decorator parameter selection for derived-service dependencies (#1459) The compatibleServiceParameter added for #1330 matched any decorator constructor parameter whose type was assignable-from the service type (serviceType.IsAssignableFrom(pi.ParameterType)). When a decorator took a dependency typed as a more-derived service (e.g. a sub-interface of the decorated service), that parameter matched the predicate and received the decorated instance even though the instance was not actually of the more derived type, throwing InvalidCastException. Additionally require pi.ParameterType.IsInstanceOfType(currentInstance) so the decorated instance is only injected where it is genuinely assignable; otherwise the parameter falls through to normal autowiring. This narrows the match set, preserving #1330 behavior. --- .../Decorators/DecoratorMiddleware.cs | 17 ++++++-- .../Features/Decorators/DecoratorTests.cs | 42 +++++++++++++++++++ 2 files changed, 56 insertions(+), 3 deletions(-) diff --git a/src/Autofac/Features/Decorators/DecoratorMiddleware.cs b/src/Autofac/Features/Decorators/DecoratorMiddleware.cs index f7ce087b3..7bf9094ec 100644 --- a/src/Autofac/Features/Decorators/DecoratorMiddleware.cs +++ b/src/Autofac/Features/Decorators/DecoratorMiddleware.cs @@ -85,10 +85,21 @@ public void Execute(ResolveRequestContext context, Action // decorators assume an exact typed parameter but the multi-service // registered decorators need the resolved version that can determine // compatibility by casting. - var typedServiceParameter = new TypedParameter(serviceType, context.DecoratorContext.CurrentInstance); + // + // Issue 1459: The compatible parameter must only supply the decorated + // instance to constructor parameters that the instance can actually be + // assigned to. A parameter typed as a more derived service than the one + // being decorated (for example, a sub-interface) satisfies + // IsAssignableFrom on the parameter type, but the decorated instance may + // not actually be of that more derived type. In that case the parameter + // should fall through to normal autowiring rather than receiving the + // decorated instance (which would otherwise throw an InvalidCastException). + var currentInstance = context.DecoratorContext.CurrentInstance; + var typedServiceParameter = new TypedParameter(serviceType, currentInstance); var compatibleServiceParameter = new ResolvedParameter( - (pi, ctx) => serviceType.IsAssignableFrom(pi.ParameterType), - (pi, ctx) => context.DecoratorContext.CurrentInstance); + (pi, ctx) => serviceType.IsAssignableFrom(pi.ParameterType) + && pi.ParameterType.IsInstanceOfType(currentInstance), + (pi, ctx) => currentInstance); var contextParameter = new TypedParameter(typeof(IDecoratorContext), context.DecoratorContext); Parameter[] resolveParameters; diff --git a/test/Autofac.Test/Features/Decorators/DecoratorTests.cs b/test/Autofac.Test/Features/Decorators/DecoratorTests.cs index b06a51da0..7bec45d46 100644 --- a/test/Autofac.Test/Features/Decorators/DecoratorTests.cs +++ b/test/Autofac.Test/Features/Decorators/DecoratorTests.cs @@ -225,6 +225,48 @@ public void DecorateProvidedInstanceActivatorWithPropertyInjection() Assert.True(service.NestedServiceIsNotNull()); } + // Issue 1459: A decorator constructor may take a dependency typed as a more + // derived service than the one being decorated. The decorated instance must + // not be force-injected into that parameter; it should be resolved normally. + private interface IBase + { + } + + private interface IDerived : IBase + { + } + + private class BaseImpl : IBase + { + } + + private class DerivedImpl : IDerived + { + } + + private record DerivedDependencyDecorator(IDerived Derived, IBase Decorated) : IBase; + + [Fact] + public void DecoratorWithMoreDerivedServiceDependencyResolvesDependencyNormally() + { + // Issue 1459: The decorated service (IBase) should be supplied to the + // "Decorated" parameter, while the more-derived "Derived" (IDerived) + // parameter must be resolved from the container rather than receiving + // the decorated IBase instance (which is not an IDerived). + var builder = new ContainerBuilder(); + builder.RegisterType().As(); + builder.RegisterType().As(); + builder.RegisterDecorator(); + + var container = builder.Build(); + + var resolved = container.Resolve(); + + var decorator = Assert.IsType(resolved); + Assert.IsType(decorator.Decorated); + Assert.IsType(decorator.Derived); + } + private abstract class Decorator : IDecoratedService { protected Decorator(IDecoratedService decorated) From 2b4a81d53d6b37bb9dae405dc5554f540bb93f5f Mon Sep 17 00:00:00 2001 From: Travis Illig Date: Mon, 15 Jun 2026 11:57:19 -0700 Subject: [PATCH 2/5] Patch pre-commit. --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 806661866..527a30d81 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -8,7 +8,7 @@ repos: - id: end-of-file-fixer - id: trailing-whitespace - repo: https://github.com/igorshubovych/markdownlint-cli - rev: "e72a3ca1632f0b11a07d171449fe447a7ff6795e" # frozen: v0.48.0 + rev: "c7c1c7640e610068e8e4754e9f1bf109bd987dc7" # post-v0.48.0 with patches hooks: - id: markdownlint args: From 6b18b9382a1b368f4541fad44cdd0b9696eeff40 Mon Sep 17 00:00:00 2001 From: Travis Illig Date: Mon, 15 Jun 2026 12:03:59 -0700 Subject: [PATCH 3/5] Add open-generic regression test for #1459 Mirrors the closed-generic DecoratorTests case: an open-generic decorator whose constructor takes a more-derived service dependency (IDecoratedService) than the decorated service (IService) must resolve that dependency from the container rather than receiving the decorated instance. Fails with InvalidCastException against the pre-fix middleware. --- .../Decorators/OpenGenericDecoratorTests.cs | 47 +++++++++++++++++++ 1 file changed, 47 insertions(+) diff --git a/test/Autofac.Test/Features/Decorators/OpenGenericDecoratorTests.cs b/test/Autofac.Test/Features/Decorators/OpenGenericDecoratorTests.cs index cbb73df30..9462dc5b6 100644 --- a/test/Autofac.Test/Features/Decorators/OpenGenericDecoratorTests.cs +++ b/test/Autofac.Test/Features/Decorators/OpenGenericDecoratorTests.cs @@ -923,6 +923,53 @@ public void ResolvesMultipleDecoratedServicesWhenResolvedByOtherServices() }); } + // Issue 1459: A decorator constructor may take a dependency typed as a more + // derived service than the one being decorated. The decorated instance must + // not be force-injected into that parameter; it should be resolved normally. + private class PlainService : IService + { + } + + private class DerivedDependencyDecorator : IService + { + public DerivedDependencyDecorator(IDecoratedService derived, IService decorated) + { + Derived = derived; + Decorated = decorated; + } + + public IDecoratedService Derived + { + get; + } + + public IService Decorated + { + get; + } + } + + [Fact] + public void DecoratorWithMoreDerivedServiceDependencyResolvesDependencyNormally() + { + // Issue 1459: The decorated service (IService) should be supplied to + // the "Decorated" parameter, while the more-derived "Derived" + // (IDecoratedService) parameter must be resolved from the container + // rather than receiving the decorated instance (which is only an + // IService here, not an IDecoratedService). + var builder = new ContainerBuilder(); + builder.RegisterGeneric(typeof(PlainService<>)).As(typeof(IService<>)); + builder.RegisterGeneric(typeof(ImplementorA<>)).As(typeof(IDecoratedService<>)); + builder.RegisterGenericDecorator(typeof(DerivedDependencyDecorator<>), typeof(IService<>)); + var container = builder.Build(); + + var resolved = container.Resolve>(); + + var decorator = Assert.IsType>(resolved); + Assert.IsType>(decorator.Decorated); + Assert.IsType>(decorator.Derived); + } + private interface ICommandHandler { void Handle(T command); From 1cccb637a6e95a9d16f7ac4bd3ee96a802b63f44 Mon Sep 17 00:00:00 2001 From: Travis Illig Date: Mon, 15 Jun 2026 12:21:09 -0700 Subject: [PATCH 4/5] Bump version to 9.1.1 for #1459 fix --- default.proj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/default.proj b/default.proj index ecdc5534a..bc0d3e391 100644 --- a/default.proj +++ b/default.proj @@ -2,7 +2,7 @@ - 9.1.0 + 9.1.1 Autofac Release $([System.IO.Path]::Combine($(MSBuildProjectDirectory),"artifacts")) From 54eb4e53066b40bae20e12eac45e425ce2742957 Mon Sep 17 00:00:00 2001 From: Travis Illig Date: Mon, 15 Jun 2026 12:46:22 -0700 Subject: [PATCH 5/5] Address review feedback on #1459 tests - Convert the closed-generic DerivedDependencyDecorator from a record to a plain class for consistency with the other decorator types in the file. - Add a decorator-chain regression test exercising the DecoratorContext UpdateContext path: the outer decorator's more-derived dependency must still resolve from the container when the chained instance (inner decorator output) is not assignable to it. Fails with InvalidCastException against the pre-fix middleware. - Add a test asserting a clean DependencyResolutionException when the more-derived dependency is unregistered (rather than InvalidCastException). - Make the open-generic test self-contained with a purpose-built IDerivedService sub-interface instead of reusing shared test infra. --- .../Features/Decorators/DecoratorTests.cs | 79 ++++++++++++++++++- .../Decorators/OpenGenericDecoratorTests.cs | 21 +++-- 2 files changed, 93 insertions(+), 7 deletions(-) diff --git a/test/Autofac.Test/Features/Decorators/DecoratorTests.cs b/test/Autofac.Test/Features/Decorators/DecoratorTests.cs index 7bec45d46..bdb78596e 100644 --- a/test/Autofac.Test/Features/Decorators/DecoratorTests.cs +++ b/test/Autofac.Test/Features/Decorators/DecoratorTests.cs @@ -244,7 +244,37 @@ private class DerivedImpl : IDerived { } - private record DerivedDependencyDecorator(IDerived Derived, IBase Decorated) : IBase; + private class DerivedDependencyDecorator : IBase + { + public DerivedDependencyDecorator(IDerived derived, IBase decorated) + { + Derived = derived; + Decorated = decorated; + } + + public IDerived Derived + { + get; + } + + public IBase Decorated + { + get; + } + } + + private class BaseDecorator : IBase + { + public BaseDecorator(IBase decorated) + { + Decorated = decorated; + } + + public IBase Decorated + { + get; + } + } [Fact] public void DecoratorWithMoreDerivedServiceDependencyResolvesDependencyNormally() @@ -267,6 +297,53 @@ public void DecoratorWithMoreDerivedServiceDependencyResolvesDependencyNormally( Assert.IsType(decorator.Derived); } + [Fact] + public void DecoratorWithMoreDerivedServiceDependencyResolvesDependencyNormallyInChain() + { + // Issue 1459: When decorators are chained, the decorated instance seen by + // the outer decorator is the inner decorator's output (via + // DecoratorContext.UpdateContext), which is an IBase but not an IDerived. + // The outer decorator's more-derived "Derived" (IDerived) parameter must + // still be resolved from the container rather than receiving that chained + // instance, while "Decorated" receives the inner decorator. + var builder = new ContainerBuilder(); + builder.RegisterType().As(); + builder.RegisterType().As(); + + // Registered first => innermost decorator. + builder.RegisterDecorator(); + builder.RegisterDecorator(); + + var container = builder.Build(); + + var resolved = container.Resolve(); + + var outer = Assert.IsType(resolved); + Assert.IsType(outer.Derived); + + var inner = Assert.IsType(outer.Decorated); + Assert.IsType(inner.Decorated); + } + + [Fact] + public void DecoratorWithUnregisteredMoreDerivedServiceDependencyThrowsResolutionException() + { + // Issue 1459: When the more-derived "Derived" (IDerived) parameter is not + // registered, the parameter falls through to normal autowiring, which + // cannot satisfy it. The result should be a clean DependencyResolutionException + // rather than the previous InvalidCastException from force-injecting the + // decorated instance. + var builder = new ContainerBuilder(); + builder.RegisterType().As(); + + // IDerived is intentionally not registered. + builder.RegisterDecorator(); + + var container = builder.Build(); + + Assert.Throws(() => container.Resolve()); + } + private abstract class Decorator : IDecoratedService { protected Decorator(IDecoratedService decorated) diff --git a/test/Autofac.Test/Features/Decorators/OpenGenericDecoratorTests.cs b/test/Autofac.Test/Features/Decorators/OpenGenericDecoratorTests.cs index 9462dc5b6..5a88baa2f 100644 --- a/test/Autofac.Test/Features/Decorators/OpenGenericDecoratorTests.cs +++ b/test/Autofac.Test/Features/Decorators/OpenGenericDecoratorTests.cs @@ -926,19 +926,28 @@ public void ResolvesMultipleDecoratedServicesWhenResolvedByOtherServices() // Issue 1459: A decorator constructor may take a dependency typed as a more // derived service than the one being decorated. The decorated instance must // not be force-injected into that parameter; it should be resolved normally. + // ReSharper disable once UnusedTypeParameter + private interface IDerivedService : IService + { + } + private class PlainService : IService { } + private class DerivedServiceImpl : IDerivedService + { + } + private class DerivedDependencyDecorator : IService { - public DerivedDependencyDecorator(IDecoratedService derived, IService decorated) + public DerivedDependencyDecorator(IDerivedService derived, IService decorated) { Derived = derived; Decorated = decorated; } - public IDecoratedService Derived + public IDerivedService Derived { get; } @@ -954,12 +963,12 @@ public void DecoratorWithMoreDerivedServiceDependencyResolvesDependencyNormally( { // Issue 1459: The decorated service (IService) should be supplied to // the "Decorated" parameter, while the more-derived "Derived" - // (IDecoratedService) parameter must be resolved from the container + // (IDerivedService) parameter must be resolved from the container // rather than receiving the decorated instance (which is only an - // IService here, not an IDecoratedService). + // IService here, not an IDerivedService). var builder = new ContainerBuilder(); builder.RegisterGeneric(typeof(PlainService<>)).As(typeof(IService<>)); - builder.RegisterGeneric(typeof(ImplementorA<>)).As(typeof(IDecoratedService<>)); + builder.RegisterGeneric(typeof(DerivedServiceImpl<>)).As(typeof(IDerivedService<>)); builder.RegisterGenericDecorator(typeof(DerivedDependencyDecorator<>), typeof(IService<>)); var container = builder.Build(); @@ -967,7 +976,7 @@ public void DecoratorWithMoreDerivedServiceDependencyResolvesDependencyNormally( var decorator = Assert.IsType>(resolved); Assert.IsType>(decorator.Decorated); - Assert.IsType>(decorator.Derived); + Assert.IsType>(decorator.Derived); } private interface ICommandHandler