Multiple registrations of a service with and without tags
I've registered a service twice. Once with InstancePerMatchingLifetimeScope and once with InstancePerLifetimeScope.
I was certain that this would result in the latter service being used when no tag's were used and the former being used when a tagged lifetimescope was used.
This does not work however, not with PreserveExistingDefaults either.
Would it break the design of the framework if one registration has precedence over another?
This feature would be useful in those cases where I do not control scope initialization but have the ability to specify a tag (e.g QuartzAutofacFactoryModule).
In my case I wish the scope to use a specific service where other scopes use the un-tagged one.
Can you outline what you would expect to happen in various cases?
Off the top of my head, here are a few things folks might do; I'm curious what you would expect to happen in these.
[Fact]
public void ResolveFromContainer()
{
var b = new Builder();
b.RegisterType<C1>().As<I1>().InstancePerLifetimeScope();
b.RegisterType<C2>().As<I1>().InstancePerMatchingLifetimeScope("s");
var c = builder.Build();
var obj = c.Resolve<I1>();
// This is current - still expected?
Assert.IsType<C1>(obj);
}
[Fact]
public void ResolveFromUntaggedScope()
{
var b = new Builder();
b.RegisterType<C1>().As<I1>().InstancePerLifetimeScope();
b.RegisterType<C2>().As<I1>().InstancePerMatchingLifetimeScope("s");
var c = builder.Build();
using(var scope = c.BeginLifetimeScope())
{
var obj = scope.Resolve<I1>();
// This is current - still expected?
Assert.IsType<C1>(obj);
}
}
[Fact]
public void ResolveFromTaggedScope()
{
var b = new Builder();
b.RegisterType<C1>().As<I1>().InstancePerLifetimeScope();
b.RegisterType<C2>().As<I1>().InstancePerMatchingLifetimeScope("s");
var c = builder.Build();
using(var scope = c.BeginLifetimeScope("s"))
{
var obj = scope.Resolve<I1>();
// I'm guessing this is what you want to happen? What happens now?
Assert.IsType<C2>(obj);
}
}
[Fact]
public void ResolveFromUntaggedScopeInsideTaggedScope()
{
var b = new Builder();
b.RegisterType<C1>().As<I1>().InstancePerLifetimeScope();
b.RegisterType<C2>().As<I1>().InstancePerMatchingLifetimeScope("s");
var c = builder.Build();
using(var tagged = c.BeginLifetimeScope("s"))
{
using(var untagged = tagged.BeginLifetimeScope())
{
var obj = scope.Resolve<I1>();
// What's expected to happen here? It's in the tagged lifetime
// scope but ALSO untagged.
Assert.IsType<XXXX>(obj);
}
}
}
[Fact]
public void ResolveFromTaggedScopeInsideUntaggedScope()
{
var b = new Builder();
b.RegisterType<C1>().As<I1>().InstancePerLifetimeScope();
b.RegisterType<C2>().As<I1>().InstancePerMatchingLifetimeScope("s");
var c = builder.Build();
using(var untagged = c.BeginLifetimeScope())
{
using(var tagged = untagged.BeginLifetimeScope("s"))
{
var obj = scope.Resolve<I1>();
// What's expected to happen here? It's in the untagged lifetime
// scope but ALSO tagged.
Assert.IsType<XXXX>(obj);
}
}
}
[Fact]
public void ResolveWhenOverridesExist()
{
var b = new Builder();
b.RegisterType<C1>().As<I1>().InstancePerLifetimeScope();
b.RegisterType<C2>().As<I1>().InstancePerMatchingLifetimeScope("s");
var c = builder.Build();
using(var scope = c.BeginLifetimeScope("s", builder => builder.RegisterType<C3>().As<I1>()))
{
var obj = scope.Resolve<I1>();
// What's expected to happen here? There's a tagged scope, but also an override
// in the scope creation. Currently this would be C3.
Assert.IsType<XXXX>(obj);
}
}
That's not an exhaustive list of cases, but it should get you thinking. What do you expect to happen with this feature in these cases? Does it change what currently happens? How? Can you show that in a unit test style format so we can see what you're thinking?
Other cases I can think of are permutations on these, and they'd all need to be considered. What would you expect to happen if a singleton (whose dependencies come from the root scope) consumes an I1 but the singleton is resolved during a tagged lifetime scope? What about if you have two InstancePerMatchingLifetimeScope(tag) calls where you have one matching lifetime inside the other matching lifetime? Untagged lifetime inside that?
If you can lay out how you think this should work, we can look at how something like that might be implemented. Ideally the addition of the feature won't change current behavior in a way that would break folks, but it could be included in a major version if we can understand the details of the feature.
Thank you for your reply.
I can see that you have quite a lot of knowledge about this, you mention several cases I have not thought about.
I'll go through your test-cases, I've rewritten them to use mstest if you do not mind, but I guess not as you most likely wrote them directly here as it does not compile ;)
[TestMethod]
public void ResolveFromContainer()
{
var b = new ContainerBuilder();
b.RegisterType<C1>().As<I1>().InstancePerLifetimeScope();
b.RegisterType<C2>().As<I1>().InstancePerMatchingLifetimeScope("s");
var c = b.Build();
var obj = c.Resolve<I1>();
// This is current - still expected?
Assert.IsInstanceOfType(obj, typeof(C1));
}
This one does not work as it is, because C2 is registered after C1 thus replacing it. But in either case in this scenario I would expect C1 to be resolved since there is no match on InstancePerMatchingLifetimeScope. This test throws an exception; No scope with a tag matching 's' is visible from the scope in which the instance was requested.
[TestMethod]
public void ResolveFromUntaggedScope()
{
var b = new ContainerBuilder();
b.RegisterType<C1>().As<I1>().InstancePerLifetimeScope();
b.RegisterType<C2>().As<I1>().InstancePerMatchingLifetimeScope("s");
var c = b.Build();
using (var scope = c.BeginLifetimeScope())
{
var obj = scope.Resolve<I1>();
// This is current - still expected?
Assert.IsInstanceOfType(obj, typeof(C1));
}
}
This one fails with the same exception as the one above, and also in this scenario I would expect C1 since no tag is specified.
[TestMethod]
public void ResolveFromTaggedScope()
{
var b = new ContainerBuilder();
b.RegisterType<C1>().As<I1>().InstancePerLifetimeScope();
b.RegisterType<C2>().As<I1>().InstancePerMatchingLifetimeScope("s");
var c = b.Build();
using (var scope = c.BeginLifetimeScope("s"))
{
var obj = scope.Resolve<I1>();
// I'm guessing this is what you want to happen? What happens now?
Assert.IsInstanceOfType(obj, typeof(C2));
}
}
This test succeeds, but only because C2 is registered last. And also, resolving without a tag fails (as above).
[TestMethod]
public void ResolveFromUntaggedScopeInsideTaggedScope()
{
var b = new ContainerBuilder();
b.RegisterType<C1>().As<I1>().InstancePerLifetimeScope();
b.RegisterType<C2>().As<I1>().InstancePerMatchingLifetimeScope("s");
var c = b.Build();
using (var tagged = c.BeginLifetimeScope("s"))
{
using (var untagged = tagged.BeginLifetimeScope())
{
var obj = untagged.Resolve<I1>();
// What's expected to happen here? It's in the tagged lifetime
// scope but ALSO untagged.
Assert.IsInstanceOfType(obj, typeof(C2));
}
}
}
This one success because, but most likely because of the registration ordering like above. I'm not certain what I would expect in this scenario, but I believe C2 should be resolved since the untagged scope exists within a tagged scope.
[TestMethod]
public void ResolveFromTaggedScopeInsideUntaggedScope()
{
var b = new ContainerBuilder();
b.RegisterType<C1>().As<I1>().InstancePerLifetimeScope();
b.RegisterType<C2>().As<I1>().InstancePerMatchingLifetimeScope("s");
var c = b.Build();
using (var untagged = c.BeginLifetimeScope())
{
using (var tagged = untagged.BeginLifetimeScope("s"))
{
var obj = tagged.Resolve<I1>();
// What's expected to happen here? It's in the untagged lifetime
// scope but ALSO tagged.
Assert.IsInstanceOfType(obj, typeof(C2));
}
}
}
I think C2 should be resolved.
[TestMethod]
public void ResolveWhenOverridesExist()
{
var b = new ContainerBuilder();
b.RegisterType<C1>().As<I1>().InstancePerLifetimeScope();
b.RegisterType<C2>().As<I1>().InstancePerMatchingLifetimeScope("s");
var c = b.Build();
using (var scope = c.BeginLifetimeScope("s", builder => builder.RegisterType<C3>().As<I1>()))
{
var obj = scope.Resolve<I1>();
// What's expected to happen here? There's a tagged scope, but also an override
// in the scope creation. Currently this would be C3.
Assert.IsInstanceOfType(obj, typeof(C3));
}
}
Here I think C3 should be resolved.
This last example illustrates the behaviour I'm looking for, except I do not have control over the scope creation. I've considered creating a new scope within first thing, but really do not wish to inject ILifetimeScope because of best-practices.
To provide a very specific use-case for this, and possibly receive alternative approaches to the problem, I can say that I currently inject the currently logged in user into several services on a InstancePerLifetimeScope basis.
builder.Register(
c =>
c.Resolve<ApplicationUserManager>()
.FindById(HttpContext.Current?.User?.Identity?.GetUserId()) ?? new AnonymousUser())
.As<ApplicationUser>().InstancePerLifetimeScope();
I also have some services that run as system-services and update some entities. Since I have auditing enabled I need to specify a user for these services (also because they do not have a HttpContext).
builder.Register(c => c.Resolve<IUserRepository>().Query(x => x.DisplayName == "System").SingleOrDefault())
.As<ApplicationUser>()
.InstancePerMatchingLifetimeScope("system");
So, this is my motiviation behind this request.
If I were to say it with one sentence only, I'd say that the InstancePerMatchingLifetimeScope has a higher priority that the InstancePerLifetimeScope since it's more specific.
This works: https://stackoverflow.com/questions/55323684/resolve-component-in-both-tagged-scope-and-untagged-scope
This StackOverflow question has happened upon a similar issue, which I think overlaps enough that I'm filing it in here. The issue revolves around trying to resolve IEnumerable<T> when there is no matching lifetime scope.
Here's the repro. This fails under Autofac 6.0.0.
using System;
using System.Collections.Generic;
using Autofac;
using Xunit;
namespace AutofacDemo
{
public class LifetimeScopeTests
{
public interface IMyInterface
{
}
public class A : IMyInterface
{
}
public class B : IMyInterface
{
}
[Fact]
public void UntaggedScope()
{
var cb = new ContainerBuilder();
cb.RegisterType<B>().As<IMyInterface>().InstancePerMatchingLifetimeScope("B");
cb.RegisterType<A>().As<IMyInterface>().InstancePerMatchingLifetimeScope("A");
var container = cb.Build();
// Resolve throws trying to resolve B (but would also fail on A if
// it got that far).
var items = container.Resolve<IEnumerable<IMyInterface>>();
Assert.Empty(items);
}
[Fact]
public void TaggedScope()
{
var cb = new ContainerBuilder();
cb.RegisterType<B>().As<IMyInterface>().InstancePerMatchingLifetimeScope("B");
cb.RegisterType<A>().As<IMyInterface>().InstancePerMatchingLifetimeScope("A");
var container = cb.Build();
using var scope = container.BeginLifetimeScope("A");
// Resolve throws trying to resolve B.
var items = scope.Resolve<IEnumerable<IMyInterface>>();
Assert.Single(items);
}
}
}
When resolution fails it gets the usual No scope with a tag matching 'XXXXX' is visible from the scope in which the instance was requested exception.
/cc @alistairjevans (in case you hadn't seen this one)
I'm going to switch this from an enhancement to a bug since this seems like it should work... right?
@tillig, did you mean to link to the Stack Overflow question?
Anyway, this is a tricky one. The matching lifetime scope tag is specifically for the scope selection behaviour when resolving a shared instance after we have already retrieved the registration; it never gets considered as part of registration selection. So, you could argue this is working as intended.
For this to really work, the lifetime scope tag would need to be stored directly against the registration; then, when the CollectionSource resolves the collection of items, it must walk the entire lifetime scope tree at that point in time, looking for all (distinct?) matching scope names. It must then filter its set of registrations by those that have a matching scope in the list gathered from all the parents. It won't be able to cache these either, because they could change every time the collection activator is called. It's a significant overhead to the collection source, and a fairly chunky change.
Whoops, yup, here's the original StackOverflow question.
I think the challenge as I see it is that this also doesn't work:
[Fact]
public void Mixture()
{
var cb = new ContainerBuilder();
cb.RegisterType<B>().As<IMyInterface>();
cb.RegisterType<A>().As<IMyInterface>().InstancePerMatchingLifetimeScope("A");
var container = cb.Build();
// Resolve throws trying to resolve A even though B is a
// viable candidate for the collection.
var items = container.Resolve<IEnumerable<IMyInterface>>();
Assert.Single(items);
}
Like, once you've registered any of the items as InstancePerMatchingLifetimeScope you've "poisoned" the collection - it's not resolvable anymore unless you can resolve all of them.
I agree it's not an easy fix and I don't have ideas off the top of my head, but I can also see this is sort of a foot-gun. It's admittedly taken me this long to really wrap my head around the sort of challenge this imposes (which is not to say no one else has figured it out, just that it's only now I'm 💡 really getting it... or maybe I got it before then lost it and got it again?).
It may be that this just ends up a "known issue" that we document and call it a day, possibly heading toward deprecation of InstancePerMatchingLifetimeScope or something.
Yeah, I can see how that's pretty broken. I don't think we can just leave this as a permanent known issue (or deprecate matching scopes), but I'm hoping we can find a way to hot-path it; like, if you've got no matching lifetime scope registrations in the container, we don't do the extra checks in the collection source.
Hi: Has there been any progress on this issue? I see that the following problem is still not solved https://stackoverflow.com/questions/64189196/autofac-resolving-matchinglifetimescope-collection
I have found some workarounds, but it would be nice if this could be solved in Autofac if that is possible.
If there's no comment here, there's no update or progress. We're open to PRs if you have a solution.
Alright, but do you know of any workarounds to register different interface implementations for different scopes, as in the StackOverflow question?
All we have/all we know is in the issue here. If it doesn't answer the question or provide a workaround, I'm sorry. We do what we can with the time and abilities we have.
Ouch. I just encountered this issue. Unfortunately for me, I spent several hours refactoring code with the assumption this would behave the way I thought it would (based on my interpretation of the documentation. [Which I'm highly thankful exists])
I don't have anything positive to contribute to resolve this issue, but perhaps in the short term a quick caveat can be added to the documentation/example usage.
This issue has been open for quite some time without any traction. Much as we understand the challenges presented here, we also get very few substantial contributions and we don't have time to really dig into non-trivial issues like this. As such, we're closing this issue. If someone feels like they really want this, we would love to see a PR where this gets addressed.