Skip to content

Commit 0688896

Browse files
authored
Merge pull request #1486 from autofac/feature/issue-1402
Honor ExternallyOwned ownership for decorators (#1402)
2 parents b75ee0b + 44e06bc commit 0688896

2 files changed

Lines changed: 206 additions & 1 deletion

File tree

src/Autofac/Core/Resolving/Middleware/DisposalTrackingMiddleware.cs

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,13 @@ public void Execute(ResolveRequestContext context, Action<ResolveRequestContext>
2828
{
2929
next(context);
3030

31-
if (context.Registration.Ownership == InstanceOwnership.OwnedByLifetimeScope)
31+
// When a decorator is being resolved, context.DecoratorTarget is the underlying component
32+
// registration. If the underlying component is ExternallyOwned, the decorator should also
33+
// not be tracked for disposal - the caller opted out of lifetime management for the entire
34+
// decorated chain. See https://github.com/autofac/Autofac/issues/1402.
35+
var effectiveOwnership = context.DecoratorTarget?.Ownership ?? context.Registration.Ownership;
36+
37+
if (effectiveOwnership == InstanceOwnership.OwnedByLifetimeScope)
3238
{
3339
// The fact this adds instances for disposal agnostic of the activator is
3440
// important. The ProvidedInstanceActivator will NOT dispose of the provided

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

Lines changed: 199 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -675,6 +675,144 @@ public void DecoratorAndDecoratedBothDisposedWhenSingleInstance()
675675
Assert.Equal(1, decorated.DisposeCallCount);
676676
}
677677

678+
[Fact]
679+
public void DecoratorNotDisposedWhenDecoratedServiceIsExternallyOwned()
680+
{
681+
// #1402: A decorator wrapping an ExternallyOwned service should itself
682+
// not be disposed by the lifetime scope.
683+
var builder = new ContainerBuilder();
684+
builder.RegisterType<DisposableImplementor>()
685+
.As<IDecoratedService>()
686+
.ExternallyOwned();
687+
builder.RegisterDecorator<DisposableDecorator, IDecoratedService>();
688+
var container = builder.Build();
689+
690+
DisposableDecorator decorator;
691+
DisposableImplementor decorated;
692+
693+
using (var scope = container.BeginLifetimeScope())
694+
{
695+
var instance = scope.Resolve<IDecoratedService>();
696+
decorator = (DisposableDecorator)instance;
697+
decorated = (DisposableImplementor)instance.Decorated;
698+
}
699+
700+
Assert.Equal(0, decorator.DisposeCallCount);
701+
Assert.Equal(0, decorated.DisposeCallCount);
702+
}
703+
704+
[Fact]
705+
public void StackedDecoratorsNotDisposedWhenDecoratedServiceIsExternallyOwned()
706+
{
707+
// #1402: When several decorators are stacked over an ExternallyOwned
708+
// service, the ownership of the underlying component governs the entire
709+
// chain - none of the decorators should be disposed by the scope.
710+
var builder = new ContainerBuilder();
711+
builder.RegisterType<DisposableImplementor>()
712+
.As<IDecoratedService>()
713+
.ExternallyOwned();
714+
builder.RegisterDecorator<DisposableDecorator, IDecoratedService>();
715+
builder.RegisterDecorator<DisposableDecoratorB, IDecoratedService>();
716+
var container = builder.Build();
717+
718+
DisposableDecoratorB outer;
719+
DisposableDecorator inner;
720+
DisposableImplementor decorated;
721+
722+
using (var scope = container.BeginLifetimeScope())
723+
{
724+
var instance = scope.Resolve<IDecoratedService>();
725+
outer = (DisposableDecoratorB)instance;
726+
inner = (DisposableDecorator)outer.Decorated;
727+
decorated = (DisposableImplementor)inner.Decorated;
728+
}
729+
730+
Assert.Equal(0, outer.DisposeCallCount);
731+
Assert.Equal(0, inner.DisposeCallCount);
732+
Assert.Equal(0, decorated.DisposeCallCount);
733+
}
734+
735+
[Fact]
736+
public void StackedDecoratorsAllDisposedWhenDecoratedServiceOwnedByLifetimeScope()
737+
{
738+
// #1402 regression guard: stacked decorators over a normally-owned
739+
// service must continue to be disposed along with the decorated instance.
740+
var builder = new ContainerBuilder();
741+
builder.RegisterType<DisposableImplementor>()
742+
.As<IDecoratedService>();
743+
builder.RegisterDecorator<DisposableDecorator, IDecoratedService>();
744+
builder.RegisterDecorator<DisposableDecoratorB, IDecoratedService>();
745+
var container = builder.Build();
746+
747+
DisposableDecoratorB outer;
748+
DisposableDecorator inner;
749+
DisposableImplementor decorated;
750+
751+
using (var scope = container.BeginLifetimeScope())
752+
{
753+
var instance = scope.Resolve<IDecoratedService>();
754+
outer = (DisposableDecoratorB)instance;
755+
inner = (DisposableDecorator)outer.Decorated;
756+
decorated = (DisposableImplementor)inner.Decorated;
757+
}
758+
759+
Assert.Equal(1, outer.DisposeCallCount);
760+
Assert.Equal(1, inner.DisposeCallCount);
761+
Assert.Equal(1, decorated.DisposeCallCount);
762+
}
763+
764+
[Fact]
765+
public void GenericDecoratorNotDisposedWhenDecoratedServiceIsExternallyOwned()
766+
{
767+
// #1402: The open-generic decorator path also runs through the decorator
768+
// middleware, so an ExternallyOwned decorated component must prevent the
769+
// generic decorator from being disposed by the scope.
770+
var builder = new ContainerBuilder();
771+
builder.RegisterGeneric(typeof(DisposableGenericComponent<>))
772+
.As(typeof(IDisposableGenericService<>))
773+
.ExternallyOwned();
774+
builder.RegisterGenericDecorator(typeof(DisposableGenericDecorator<>), typeof(IDisposableGenericService<>));
775+
var container = builder.Build();
776+
777+
DisposableGenericDecorator<int> decorator;
778+
DisposableGenericComponent<int> decorated;
779+
780+
using (var scope = container.BeginLifetimeScope())
781+
{
782+
var instance = scope.Resolve<IDisposableGenericService<int>>();
783+
decorator = (DisposableGenericDecorator<int>)instance;
784+
decorated = (DisposableGenericComponent<int>)decorator.Decorated;
785+
}
786+
787+
Assert.Equal(0, decorator.DisposeCallCount);
788+
Assert.Equal(0, decorated.DisposeCallCount);
789+
}
790+
791+
[Fact]
792+
public void GenericDecoratorAndDecoratedBothDisposedWhenOwnedByLifetimeScope()
793+
{
794+
// #1402 regression guard: open-generic decorator over a normally-owned
795+
// service must continue to be disposed along with the decorated instance.
796+
var builder = new ContainerBuilder();
797+
builder.RegisterGeneric(typeof(DisposableGenericComponent<>))
798+
.As(typeof(IDisposableGenericService<>));
799+
builder.RegisterGenericDecorator(typeof(DisposableGenericDecorator<>), typeof(IDisposableGenericService<>));
800+
var container = builder.Build();
801+
802+
DisposableGenericDecorator<int> decorator;
803+
DisposableGenericComponent<int> decorated;
804+
805+
using (var scope = container.BeginLifetimeScope())
806+
{
807+
var instance = scope.Resolve<IDisposableGenericService<int>>();
808+
decorator = (DisposableGenericDecorator<int>)instance;
809+
decorated = (DisposableGenericComponent<int>)decorator.Decorated;
810+
}
811+
812+
Assert.Equal(1, decorator.DisposeCallCount);
813+
Assert.Equal(1, decorated.DisposeCallCount);
814+
}
815+
678816
[Fact]
679817
public void DecoratorAppliedOnlyOnceToComponentWithExternalRegistrySource()
680818
{
@@ -1413,6 +1551,25 @@ public void Dispose()
14131551
}
14141552
}
14151553

1554+
// ReSharper disable once ClassNeverInstantiated.Local
1555+
private sealed class DisposableDecoratorB : Decorator, IDisposable
1556+
{
1557+
public DisposableDecoratorB(IDecoratedService decorated)
1558+
: base(decorated)
1559+
{
1560+
}
1561+
1562+
public int DisposeCallCount
1563+
{
1564+
get; private set;
1565+
}
1566+
1567+
public void Dispose()
1568+
{
1569+
DisposeCallCount++;
1570+
}
1571+
}
1572+
14161573
// ReSharper disable once ClassNeverInstantiated.Local
14171574
private sealed class DisposableImplementor : IDecoratedService, IDisposable
14181575
{
@@ -1533,6 +1690,48 @@ public IGenericService<T> Decorated
15331690
}
15341691
}
15351692

1693+
private interface IDisposableGenericService<T>
1694+
{
1695+
}
1696+
1697+
// ReSharper disable once ClassNeverInstantiated.Local
1698+
private sealed class DisposableGenericComponent<T> : IDisposableGenericService<T>, IDisposable
1699+
{
1700+
public int DisposeCallCount
1701+
{
1702+
get; private set;
1703+
}
1704+
1705+
public void Dispose()
1706+
{
1707+
DisposeCallCount++;
1708+
}
1709+
}
1710+
1711+
// ReSharper disable once ClassNeverInstantiated.Local
1712+
private sealed class DisposableGenericDecorator<T> : IDisposableGenericService<T>, IDisposable
1713+
{
1714+
public DisposableGenericDecorator(IDisposableGenericService<T> decorated)
1715+
{
1716+
Decorated = decorated;
1717+
}
1718+
1719+
public IDisposableGenericService<T> Decorated
1720+
{
1721+
get;
1722+
}
1723+
1724+
public int DisposeCallCount
1725+
{
1726+
get; private set;
1727+
}
1728+
1729+
public void Dispose()
1730+
{
1731+
DisposeCallCount++;
1732+
}
1733+
}
1734+
15361735
private class ConditionalShouldDecorate
15371736
{
15381737
public bool ShouldDecorate

0 commit comments

Comments
 (0)