-
-
Notifications
You must be signed in to change notification settings - Fork 835
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Partially decorating service implementing multiple interfaces throws #963
Comments
When you say "fails," is it an exception or is it failing to decorate the interface? What do you see vs. what do you expect? |
Hmmm, this is an interesting case. I think your expectation sounds right, that you should be able to decorate a particular interface but not have the system assume every interface on the registration is decorated. As a quick workaround, you could probably separate the registration: builder.RegisterType<Service>().As<IDecorable>();
builder.RegisterType<Service>().As<IService>();
builder.RegisterDecorator<Decorator, IDecorable>(); But that doesn't solve the problem, it just works around it. We'll have to see what we can do about this. |
I'm also having an issue that I believe is related. I have a class that closes an interface implementation twice. When wrapped with a generic decorator, only one of the two closed interfaces can be resolved: public class UnitTest1
{
[Fact]
public void CanResolveUseCaseImplementations()
{
// Arrange
var builder = new ContainerBuilder();
builder.RegisterType<MyServiceImpl>().AsImplementedInterfaces();
var container = builder.Build();
// Act
container.Resolve<IUseCase<MyServiceImpl.ConfirmRequest>>();
container.Resolve<IUseCase<MyServiceImpl.Request>>();
}
[Fact]
public void CanResolveFirstDecoratedService()
{
// Arrange
var builder = new ContainerBuilder();
builder.RegisterType<MyServiceImpl>().AsImplementedInterfaces();
builder.RegisterGenericDecorator(typeof(ProfilerDecorator<>), typeof(IUseCase<>));
var container = builder.Build();
// Act
container.Resolve<IUseCase<MyServiceImpl.ConfirmRequest>>();
}
[Fact]
public void CanResolveSecondDecoratedService()
{
// Arrange
var builder = new ContainerBuilder();
builder.RegisterType<MyServiceImpl>().AsImplementedInterfaces();
builder.RegisterGenericDecorator(typeof(ProfilerDecorator<>), typeof(IUseCase<>));
var container = builder.Build();
// Act
container.Resolve<IUseCase<MyServiceImpl.Request>>();
}
public interface IRequest { }
public interface IUseCase<TRequest>
where TRequest : class, IRequest
{
void Execute(TRequest request);
}
public class MyServiceImpl : IUseCase<MyServiceImpl.ConfirmRequest>,
IUseCase<MyServiceImpl.Request>
{
public void Execute(Request request) { }
public void Execute(ConfirmRequest request) { }
public class ConfirmRequest : IRequest { }
public class Request : IRequest { }
}
public class ProfilerDecorator<TRequest> : IUseCase<TRequest>
where TRequest : class, IRequest
{
private readonly IUseCase<TRequest> inner;
public ProfilerDecorator(IUseCase<TRequest> inner) => this.inner = inner;
public void Execute(TRequest request) { }
}
}
Message: System.InvalidCastException : Unable to cast object of type
'ProfilerDecorator`1[AutofacBugTest.UnitTest1+MyServiceImpl+ConfirmRequest]' to type
'IUseCase`1[AutofacBugTest.UnitTest1+MyServiceImpl+Request]'. |
Yeah, that sounds like the same thing - the decorator is trying to decorate the whole registration rather than just the appropriate service. |
I've definitely been able to repro this and noticed in our unit tests that the decorated interface was inheriting from the base service interface like this: public interface IService { }
public interface IDecoratedService : IService { } What that means is that if you have a decorator that implements Removing that relationship revealed the problem noted above. Now to see what to do about it. |
Hooooo boy. I figured out why this isn't working but I'm not sure how to fix it without some non-trivial internal changes. Granted, some of those changes may be well worthwhile to do, but it could involve breaking interfaces. The short version: By the time the decorator is getting chosen, we know which registration is being resolved, but not which service on that registration. That means a single registration exposing multiple services will try to decorate all of them. Longer explanation with details (in which I won't blame you if you fall asleep 😴 ): When you resolve a component from an
Later, deep in that chain of resolution, is basically where Autofac constructs (activates) all the concrete objects and handles decoration. Again, by the time we get way, way down into the stack there, we've long since lost the information about exactly which service (interface) the caller was trying to resolve so the approach is just to decorate whatever comes out of the whole registration. If that registration has multiple services exposed but only one of them is decorated, you'll get the exception because the decorator is trying to decorate anything coming out there, not just the one service. Hmmm. I think the solution here is to actually create a sort of The challenge with switching to that is it'll affect a lot of public interfaces. For example, public interface IComponentContext
{
IComponentRegistry ComponentRegistry { get; }
object ResolveComponent(IComponentRegistration registration, IEnumerable<Parameter> parameters);
} to public interface IComponentContext
{
IComponentRegistry ComponentRegistry { get; }
object ResolveComponent(ResolveContext context);
} That's a pretty big breaking change. It'd affect resolution extension methods and likely some integration packages that we both do and don't own. It may also affect memory usage and perf, but we'd have to benchmark it to see. I think that'd be a positive change because it would allow us to add data to the resolve pipeline that we didn't previously have; and adding/changing properties in that context object would be possible without breaking interfaces. I think there are quite a few things we could make better if we had all the information at all levels of the operation. But... it's not a five minute fix, so I won't be able to drop a quick 0.0.1 bug fix on this one. It's bigger than that. Again, the workaround appears to be separating the registrations: builder.RegisterType<Service>().As<IDecorable>();
builder.RegisterType<Service>().As<IService>();
builder.RegisterDecorator<Decorator, IDecorable>(); Which sucks, and isn't as pretty, and it makes some things harder, and I'm truly sorry about that. I think this would be a great thing to target for a 5.0 release where we can do some breaking changes. |
I managed to read through... I don't quite know the inner workings of autofac, but maybe another approach would be to decorate the registration in step 1, where you resolve it from the component registry and you know which service is being resolved. Simple suggestion, don't know if that makes any sense to you. |
I slept on this and realized something at like 3AM when all the important thoughts hit: This may not be fixable at all. It has to do with component lifetimes. Just for future readers of this chain...
In all the examples we've talked about so far, every component registration has been instance-per-dependency: Ask for an instance, get a new one. var builder = new ContainerBuilder();
builder.RegisterType<Component>();
var container = builder.Build();
var a = container.Resolve<Component>();
var b = container.Resolve<Component>();
Assert.NotSame(a, b); In that case, it makes total sense that something like this should work: var builder = new ContainerBuilder();
builder.RegisterType<Component>()
.As<IService1>()
.As<IService2>();
builder.RegisterDecorator<Decorator, IService1>();
var container = builder.Build();
var a = container.Resolve<IService1>();
var b = container.Resolve<IService2>();
Assert.IsInstanceOf<Decorator>(a);
Assert.IsInstanceOf<Component>(b); That is, when you ask for it as an However, let's say var builder = new ContainerBuilder();
builder.RegisterType<Component>()
.SingleInstance();
var container = builder.Build();
var a = container.Resolve<Component>();
var b = container.Resolve<Component>();
Assert.Same(a, b); Now you want to do the decoration again. var builder = new ContainerBuilder();
builder.RegisterType<Component>()
.As<IService1>()
.As<IService2>()
.SingleInstance();
builder.RegisterDecorator<Decorator, IService1>();
var container = builder.Build();
var a = container.Resolve<IService1>();
var b = container.Resolve<IService2>();
// This should still pass! It's a singleton!
Assert.Same(a, b);
// What type T in here would make this true?
Assert.IsInstanceOf<T>(a);
Assert.IsInstanceOf<T>(b); For the singleton case, the same component instance needs to fulfill all of the services. Which means if you want to decorate a singleton... the decorator also needs to fulfill all the services. In the above unit test illustration, the This same logic holds for instance-per-lifetime-scope (instance-per-request), I can imagine a really, really complex system of dynamic proxies and/or interceptors where we basically generate a hybrid wrapper for objects that uses the decorator for methods in the decorated service but passes through to the undecorated methods for things that aren't decorated, but that could get really complicated really quick, especially when you start layering decorators together. To be honest, I'm not really interested in building or maintaining that level of complexity and potential unreliability for this case. Basically, the only option I see here is to make the behavior and reasoning behind it more clear and make it easier to troubleshoot.
I still think the Sort of... Schrodinger's Decorator, if you will. |
… for now." This reverts commit 8ea04f0.
With generics, it appears as if the issue is predicated upon the registration order of the component's services and/or the order of interface declaration in the service implementation body. [Fact]
public void ResolveSecondDecoratedService_Fails1()
{
// Arrange
var builder = new ContainerBuilder();
builder.RegisterType<MyServiceImpl>().AsImplementedInterfaces().InstancePerLifetimeScope();
builder.RegisterGenericDecorator(typeof(ProfilerDecorator<>), typeof(IUseCase<>));
var container = builder.Build();
// Act
container.Resolve<IUseCase<MyServiceImpl.Request>>();
}
[Fact]
public void ResolveSecondDecoratedService_Fails2()
{
// Arrange
var builder = new ContainerBuilder();
builder.RegisterType<MyServiceImpl>().As<IUseCase<MyServiceImpl.ConfirmRequest>>()
.As<IUseCase<MyServiceImpl.Request>>()
.InstancePerLifetimeScope();
builder.RegisterGenericDecorator(typeof(ProfilerDecorator<>), typeof(IUseCase<>));
var container = builder.Build();
// Act
container.Resolve<IUseCase<MyServiceImpl.Request>>();
}
[Fact]
public void ResolveSecondDecoratedService_Succeeds()
{
// Arrange
var builder = new ContainerBuilder();
builder.RegisterType<MyServiceImpl>().As<IUseCase<MyServiceImpl.ConfirmRequest>>().InstancePerLifetimeScope();
builder.RegisterType<MyServiceImpl>().As<IUseCase<MyServiceImpl.Request>>().InstancePerLifetimeScope();
builder.RegisterGenericDecorator(typeof(ProfilerDecorator<>), typeof(IUseCase<>));
var container = builder.Build();
// Act
container.Resolve<IUseCase<MyServiceImpl.Request>>();
} If I switch the implementation order of MyServiceImpl such that instead of: public class MyServiceImpl : IUseCase<MyServiceImpl.ConfirmRequest>,
IUseCase<MyServiceImpl.Request>
{
public void Execute(Request request) { }
public void Execute(ConfirmRequest request) { }
public class ConfirmRequest : IRequest { }
public class Request : IRequest { }
} we have public class MyServiceImpl : IUseCase<MyServiceImpl.Request>,
IUseCase<MyServiceImpl.ConfirmRequest>
{
public void Execute(Request request) { }
public void Execute(ConfirmRequest request) { }
public class ConfirmRequest : IRequest { }
public class Request : IRequest { }
} Then builder.RegisterType<MyServiceImpl>().As<IUseCase<MyServiceImpl.Request>>()
.As<IUseCase<MyServiceImpl.ConfirmRequest>>()
.InstancePerLifetimeScope(); I'm finding it hard to correlate the reasoning behind the issues of the concrete decorator with that of the generic one. Thank you for taking time to address these issues. |
Just catching up on the action here. Early on when developing the feature I created a test that was trying to resolve the implementation type directly and checking that it wasn't decorated. [Fact(Skip ="Cannot currently determine requested resolve service type")]
public void DecoratedRegistrationCanIncludeImplementationType()
{
var builder = new ContainerBuilder();
builder.RegisterType<ImplementorA>().As<IDecoratedService>().AsSelf();
builder.RegisterDecorator<DecoratorA, IDecoratedService>();
var container = builder.Build();
Assert.IsType<ImplementorA>(container.Resolve<ImplementorA>());
} It never sat right that the one decorated component registration could exhibit different runtime behaviour depending on how you asked it to be resolved. The singleton instance is the perfect example, because if the logic in the decorated component assumed singleton behaviour, having one decorated instance and one non-decorated instance breaks that assumption. Unfortunately, I completely forgot about this while getting all the other features across the line and never came back to revist what the right behaviour should be. I think it would be possible for public object ResolveComponent(IComponentRegistration registration, IEnumerable<Parameter> parameters) I was torn between updating that method to get the extra information further down into the resolution process and possibly causing a massive breaking change. It doesn't seem like a common method to use directly, but it has been around since the very early days, so it's inevitable that some people will be using it one way or another. I'm sure I stashed those changes and might take another look at them to see if anything else comes back. If a breaking change is required then going down the context route might be nice as it should be easier to add things to it without introducing breaking changes. I'm still not sure that having a component that can be decorated or not depending on the service it is resolved by is a good idea. There is support in the new decorators to conditionally apply decorators that handles scenarios where the decorator chain might need to be adjusted based on runtime information. |
You are right about registration order playing a role in all this @jonathansapp. If the service order is swapped around it changes the behaviour. The example below no longer throws an exception because the decorator is not applied. Resolving var builder = new ContainerBuilder();
builder.RegisterType<Service>().As<IService>().As<IDecorable>();
builder.RegisterDecorator<Decorator, IDecorable>();
var container = builder.Build();
var foo = container.Resolve<IService>();
var bar = container.Resolve<IDecorable>(); It seems that not all services are considered when it comes to looking for decorators and that is indeed a bug. I found the problem and with the fix in place the order does not matter. You will receive the |
…terface was the first service assigned to the registration. Found while investigating #963.
For the singleton case, I tend to disagree with tillig's reasoning. I feel it would be natural as such :
In essence, Both Component would be the same single instance, but Decorator would only wrap Component when resolving IService1. |
The fix for the decorator only being applied if the decorator interface was the first service assigned to the registration is included in |
@tillig I think it's reasonable to break perfect theoretical model of Component and Services to support Service level decoration in order to give real flexibility. It just needs to be obvious and documented (which it is by the nature of decoration). |
I have refactored the decorator implementation to use |
This has been fixed in |
Greate job guys ! thanks. |
Autofac 4.9.1
When a service implements 2 interfaces and only one is decorated, it fails.
Failure : throws exception in comment
I assumed getting IDecorable would retrieve Service decorated by Decorator and retrieving IService would get same instance of Service not decorated
The text was updated successfully, but these errors were encountered: