Skip to content

Commit f8d4095

Browse files
authored
Merge pull request #1484 from autofac/feature/issue-1459
Fix decorator parameter selection for derived-service dependencies (#1459)
2 parents 8f568b3 + 54eb4e5 commit f8d4095

5 files changed

Lines changed: 191 additions & 5 deletions

File tree

.pre-commit-config.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ repos:
88
- id: end-of-file-fixer
99
- id: trailing-whitespace
1010
- repo: https://github.com/igorshubovych/markdownlint-cli
11-
rev: "e72a3ca1632f0b11a07d171449fe447a7ff6795e" # frozen: v0.48.0
11+
rev: "c7c1c7640e610068e8e4754e9f1bf109bd987dc7" # post-v0.48.0 with patches
1212
hooks:
1313
- id: markdownlint
1414
args:

default.proj

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
<Project DefaultTargets="All" xmlns="http://schemas.microsoft.com/developer/msbuild/2003" ToolsVersion="Current">
33
<PropertyGroup>
44
<!-- Increment the overall semantic version here. -->
5-
<Version>9.1.0</Version>
5+
<Version>9.1.1</Version>
66
<SolutionName>Autofac</SolutionName>
77
<Configuration Condition="'$(Configuration)'==''">Release</Configuration>
88
<ArtifactDirectory>$([System.IO.Path]::Combine($(MSBuildProjectDirectory),"artifacts"))</ArtifactDirectory>

src/Autofac/Features/Decorators/DecoratorMiddleware.cs

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -85,10 +85,21 @@ public void Execute(ResolveRequestContext context, Action<ResolveRequestContext>
8585
// decorators assume an exact typed parameter but the multi-service
8686
// registered decorators need the resolved version that can determine
8787
// compatibility by casting.
88-
var typedServiceParameter = new TypedParameter(serviceType, context.DecoratorContext.CurrentInstance);
88+
//
89+
// Issue 1459: The compatible parameter must only supply the decorated
90+
// instance to constructor parameters that the instance can actually be
91+
// assigned to. A parameter typed as a more derived service than the one
92+
// being decorated (for example, a sub-interface) satisfies
93+
// IsAssignableFrom on the parameter type, but the decorated instance may
94+
// not actually be of that more derived type. In that case the parameter
95+
// should fall through to normal autowiring rather than receiving the
96+
// decorated instance (which would otherwise throw an InvalidCastException).
97+
var currentInstance = context.DecoratorContext.CurrentInstance;
98+
var typedServiceParameter = new TypedParameter(serviceType, currentInstance);
8999
var compatibleServiceParameter = new ResolvedParameter(
90-
(pi, ctx) => serviceType.IsAssignableFrom(pi.ParameterType),
91-
(pi, ctx) => context.DecoratorContext.CurrentInstance);
100+
(pi, ctx) => serviceType.IsAssignableFrom(pi.ParameterType)
101+
&& pi.ParameterType.IsInstanceOfType(currentInstance),
102+
(pi, ctx) => currentInstance);
92103
var contextParameter = new TypedParameter(typeof(IDecoratorContext), context.DecoratorContext);
93104

94105
Parameter[] resolveParameters;

test/Autofac.Test/Features/Decorators/DecoratorTests.cs

Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -225,6 +225,125 @@ public void DecorateProvidedInstanceActivatorWithPropertyInjection()
225225
Assert.True(service.NestedServiceIsNotNull());
226226
}
227227

228+
// Issue 1459: A decorator constructor may take a dependency typed as a more
229+
// derived service than the one being decorated. The decorated instance must
230+
// not be force-injected into that parameter; it should be resolved normally.
231+
private interface IBase
232+
{
233+
}
234+
235+
private interface IDerived : IBase
236+
{
237+
}
238+
239+
private class BaseImpl : IBase
240+
{
241+
}
242+
243+
private class DerivedImpl : IDerived
244+
{
245+
}
246+
247+
private class DerivedDependencyDecorator : IBase
248+
{
249+
public DerivedDependencyDecorator(IDerived derived, IBase decorated)
250+
{
251+
Derived = derived;
252+
Decorated = decorated;
253+
}
254+
255+
public IDerived Derived
256+
{
257+
get;
258+
}
259+
260+
public IBase Decorated
261+
{
262+
get;
263+
}
264+
}
265+
266+
private class BaseDecorator : IBase
267+
{
268+
public BaseDecorator(IBase decorated)
269+
{
270+
Decorated = decorated;
271+
}
272+
273+
public IBase Decorated
274+
{
275+
get;
276+
}
277+
}
278+
279+
[Fact]
280+
public void DecoratorWithMoreDerivedServiceDependencyResolvesDependencyNormally()
281+
{
282+
// Issue 1459: The decorated service (IBase) should be supplied to the
283+
// "Decorated" parameter, while the more-derived "Derived" (IDerived)
284+
// parameter must be resolved from the container rather than receiving
285+
// the decorated IBase instance (which is not an IDerived).
286+
var builder = new ContainerBuilder();
287+
builder.RegisterType<BaseImpl>().As<IBase>();
288+
builder.RegisterType<DerivedImpl>().As<IDerived>();
289+
builder.RegisterDecorator<DerivedDependencyDecorator, IBase>();
290+
291+
var container = builder.Build();
292+
293+
var resolved = container.Resolve<IBase>();
294+
295+
var decorator = Assert.IsType<DerivedDependencyDecorator>(resolved);
296+
Assert.IsType<BaseImpl>(decorator.Decorated);
297+
Assert.IsType<DerivedImpl>(decorator.Derived);
298+
}
299+
300+
[Fact]
301+
public void DecoratorWithMoreDerivedServiceDependencyResolvesDependencyNormallyInChain()
302+
{
303+
// Issue 1459: When decorators are chained, the decorated instance seen by
304+
// the outer decorator is the inner decorator's output (via
305+
// DecoratorContext.UpdateContext), which is an IBase but not an IDerived.
306+
// The outer decorator's more-derived "Derived" (IDerived) parameter must
307+
// still be resolved from the container rather than receiving that chained
308+
// instance, while "Decorated" receives the inner decorator.
309+
var builder = new ContainerBuilder();
310+
builder.RegisterType<BaseImpl>().As<IBase>();
311+
builder.RegisterType<DerivedImpl>().As<IDerived>();
312+
313+
// Registered first => innermost decorator.
314+
builder.RegisterDecorator<BaseDecorator, IBase>();
315+
builder.RegisterDecorator<DerivedDependencyDecorator, IBase>();
316+
317+
var container = builder.Build();
318+
319+
var resolved = container.Resolve<IBase>();
320+
321+
var outer = Assert.IsType<DerivedDependencyDecorator>(resolved);
322+
Assert.IsType<DerivedImpl>(outer.Derived);
323+
324+
var inner = Assert.IsType<BaseDecorator>(outer.Decorated);
325+
Assert.IsType<BaseImpl>(inner.Decorated);
326+
}
327+
328+
[Fact]
329+
public void DecoratorWithUnregisteredMoreDerivedServiceDependencyThrowsResolutionException()
330+
{
331+
// Issue 1459: When the more-derived "Derived" (IDerived) parameter is not
332+
// registered, the parameter falls through to normal autowiring, which
333+
// cannot satisfy it. The result should be a clean DependencyResolutionException
334+
// rather than the previous InvalidCastException from force-injecting the
335+
// decorated instance.
336+
var builder = new ContainerBuilder();
337+
builder.RegisterType<BaseImpl>().As<IBase>();
338+
339+
// IDerived is intentionally not registered.
340+
builder.RegisterDecorator<DerivedDependencyDecorator, IBase>();
341+
342+
var container = builder.Build();
343+
344+
Assert.Throws<DependencyResolutionException>(() => container.Resolve<IBase>());
345+
}
346+
228347
private abstract class Decorator : IDecoratedService
229348
{
230349
protected Decorator(IDecoratedService decorated)

test/Autofac.Test/Features/Decorators/OpenGenericDecoratorTests.cs

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -923,6 +923,62 @@ public void ResolvesMultipleDecoratedServicesWhenResolvedByOtherServices()
923923
});
924924
}
925925

926+
// Issue 1459: A decorator constructor may take a dependency typed as a more
927+
// derived service than the one being decorated. The decorated instance must
928+
// not be force-injected into that parameter; it should be resolved normally.
929+
// ReSharper disable once UnusedTypeParameter
930+
private interface IDerivedService<T> : IService<T>
931+
{
932+
}
933+
934+
private class PlainService<T> : IService<T>
935+
{
936+
}
937+
938+
private class DerivedServiceImpl<T> : IDerivedService<T>
939+
{
940+
}
941+
942+
private class DerivedDependencyDecorator<T> : IService<T>
943+
{
944+
public DerivedDependencyDecorator(IDerivedService<T> derived, IService<T> decorated)
945+
{
946+
Derived = derived;
947+
Decorated = decorated;
948+
}
949+
950+
public IDerivedService<T> Derived
951+
{
952+
get;
953+
}
954+
955+
public IService<T> Decorated
956+
{
957+
get;
958+
}
959+
}
960+
961+
[Fact]
962+
public void DecoratorWithMoreDerivedServiceDependencyResolvesDependencyNormally()
963+
{
964+
// Issue 1459: The decorated service (IService<T>) should be supplied to
965+
// the "Decorated" parameter, while the more-derived "Derived"
966+
// (IDerivedService<T>) parameter must be resolved from the container
967+
// rather than receiving the decorated instance (which is only an
968+
// IService<T> here, not an IDerivedService<T>).
969+
var builder = new ContainerBuilder();
970+
builder.RegisterGeneric(typeof(PlainService<>)).As(typeof(IService<>));
971+
builder.RegisterGeneric(typeof(DerivedServiceImpl<>)).As(typeof(IDerivedService<>));
972+
builder.RegisterGenericDecorator(typeof(DerivedDependencyDecorator<>), typeof(IService<>));
973+
var container = builder.Build();
974+
975+
var resolved = container.Resolve<IService<int>>();
976+
977+
var decorator = Assert.IsType<DerivedDependencyDecorator<int>>(resolved);
978+
Assert.IsType<PlainService<int>>(decorator.Decorated);
979+
Assert.IsType<DerivedServiceImpl<int>>(decorator.Derived);
980+
}
981+
926982
private interface ICommandHandler<T>
927983
{
928984
void Handle(T command);

0 commit comments

Comments
 (0)