Autofac: Partially decorating service implementing multiple interfaces throws
Autofac 4.9.1
When a service implements 2 interfaces and only one is decorated, it fails.
public interface IDecorable
{
}
public interface IService
{
}
public class Service : IDecorable, IService
{
}
public class Decorator : IDecorable
{
public Decorator(IDecorable service)
{
}
}
[Fact]
public void Test()
{
var builder = new ContainerBuilder();
builder.RegisterType<Service>().As<IDecorable>().As<IService>();
builder.RegisterDecorator<Decorator, IDecorable>();
var container = builder.Build();
var decoratedService = container.Resolve<IDecorable>();
var nondecoratedService = container.Resolve<IService>(); // fails Unable to cast object of type 'Decorator' to type 'IService'.
}
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
About this issue
- Original URL
- State: closed
- Created 5 years ago
- Reactions: 4
- Comments: 17 (11 by maintainers)
Commits related to this issue
- Updated decorator tests to handle issue #963 caveats. Skipped for now. — committed to autofac/Autofac by tillig 5 years ago
- Revert "Updated decorator tests to handle issue #963 caveats. Skipped for now." This reverts commit 8ea04f0a15fa3de72011ef080b841271e561f688. — committed to autofac/Autofac by tillig 5 years ago
- Clarified multi-service decoration tests in light of #963. — committed to autofac/Autofac by tillig 5 years ago
- Fixed an issue where a decorator was only applied if the decorator interface was the first service assigned to the registration. Found while investigating #963. — committed to autofac/Autofac by alexmg 5 years ago
- Added skipped/failing test for #963. — committed to autofac/Autofac by tillig 5 years ago
- update test case for issue #963 — committed to RaymondHuy/Autofac by RaymondHuy 4 years ago
- Fix Issue#963: Partially decorating service implementing multiple interfaces throws (#1072) * update test case for issue #963 * Add target component to be decorated when applying decorating proces... — committed to autofac/Autofac by RaymondHuy 4 years ago
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
IComponentContext
(like a container or a lifetime scope) two things happen:MyServicImpl
) to the set of interfaces you registered (IService
,IOtherService
).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
ResolveContext
object that gets passed around during resolution that has all the information we need, sort of like howHttpContext
has all the request/response info during a web request.The challenge with switching to that is it’ll affect a lot of public interfaces. For example,
IComponentContext
would change fromto
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.
IComponentContext
is one of the most prominent interfaces we have. (ILifetimeScope
andIContainer
both inheritIComponentContext
and any resolution operations run through thatResolveComponent
method… so this would breakILifetimeScope
andIContainer
, too.)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:
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.
For the singleton case, I tend to disagree with tillig’s reasoning. I feel it would be natural as such :
In essence, resolving IService1 would generate Decorator decorating Component resolving IService2 would generate Component
Both Component would be the same single instance, but Decorator would only wrap Component when resolving IService1.
This has been fixed in
5.1.0
which is now on NuGet.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
IService
will returnService
, and resolvingIDecorable
will also returnService
.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
InvalidCastException
when resolvingIService
regardless of the order. I’ll get that issue sorted out first so that it is easier to reason about what is going on.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…
RegisterType<T>
, this is the T.As<X>
, this is the X.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.
In that case, it makes total sense that something like this should work:
That is, when you ask for it as an
IService1
it gets decorated because you registered a decorator forIService1
, but when you ask for it as anIService2
it’s not decorated because you didn’t register a decorator for that.However, let’s say
Component
is a singleton.Now you want to do the decoration again.
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
T
either has to beComponent
orDecorator
since it’s the same object instance… however,a
was resolved asIService1
andb
isIService2
, so whateverT
is, it also needs to be bothIService1
andIService2
.This same logic holds for instance-per-lifetime-scope (instance-per-request),
ExternallyOwned
, and other situations where an instance of a component may need to be shared across multiple resolve calls.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
ResolveContext
idea is solid, but I don’t think it’ll help in this case. We’d still have the issue where a component with a shared lifetime can’t be both decorated and not decorated at the same time.Sort of… Schrodinger’s Decorator, if you will.
Yeah, that sounds like the same thing - the decorator is trying to decorate the whole registration rather than just the appropriate service.
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:
But that doesn’t solve the problem, it just works around it. We’ll have to see what we can do about this.