Skip to content
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

Explicit test for injecting scoped IServiceProvider into scoped and transient services (#63225) #63226

Conversation

lord-executor
Copy link
Contributor

The new test is analogous to the existing SingletonServiceCanBeResolvedFromScope but covers the more general (at least from my point of view) case of non-singleton injection.

I have also updated the tests of the 4 failing external DI containers (Grace, Autofac, LightInject, StashBox) to skip this new test in order to both, keep the tests green and serve as an indicator to the other maintainers for where they are not 100% compliant.

@ghost ghost added the community-contribution Indicates that the PR has been added by a community member label Dec 30, 2021
@ghost
Copy link

ghost commented Dec 30, 2021

Tagging subscribers to this area: @dotnet/area-extensions-dependencyinjection
See info in area-owners.md if you want to be subscribed.

Issue Details

The new test is analogous to the existing SingletonServiceCanBeResolvedFromScope but covers the more general (at least from my point of view) case of non-singleton injection.

I have also updated the tests of the 4 failing external DI containers (Grace, Autofac, LightInject, StashBox) to skip this new test in order to both, keep the tests green and serve as an indicator to the other maintainers for where they are not 100% compliant.

Author: lord-executor
Assignees: -
Labels:

area-Extensions-DependencyInjection

Milestone: -

@lord-executor lord-executor force-pushed the service-provider-scoped-injection-test branch from c6b3a1f to b375e42 Compare December 30, 2021 20:25
@lord-executor lord-executor changed the title Explicit test for injecting scoped IServiceProvider into scoped and transient services Explicit test for injecting scoped IServiceProvider into scoped and transient services (#63225) Dec 31, 2021
Copy link
Member

@eerhardt eerhardt left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How are the 4 external DI containers failing? Are they all failing for the same reason?

cc @davidfowl - in case you have thoughts on the new test. I think it looks OK.

@lord-executor
Copy link
Contributor Author

Good question. And I should have thought of that before. Currently, the errors look like this:

Autofac

Failed Microsoft.Extensions.DependencyInjection.Specification.AutofacDependencyInjectionSpecificationTests.NonSingletonService_WithInjectedProvider_ResolvesScopeProvider(lifetime: Transient) [4 ms]
  Error Message:
   Assert.Same() Failure
Expected: AutofacServiceProvider { LifetimeScope = LifetimeScope { ComponentRegistry = ComponentRegistry { ... }, Disposer = Disposer { ... }, ParentLifetimeScope = null, RootLifetimeScope = LifetimeScope { ... }, Tag = Object { ... } } }
Actual:   AutofacServiceProvider { LifetimeScope = LifetimeScope { ComponentRegistry = ComponentRegistry { ... }, Disposer = Disposer { ... }, ParentLifetimeScope = null, RootLifetimeScope = LifetimeScope { ... }, Tag = Object { ... } } }
  Stack Trace:
     at Microsoft.Extensions.DependencyInjection.Specification.DependencyInjectionSpecificationTests.NonSingletonService_WithInjectedProvider_ResolvesScopeProvider(ServiceLifetime lifetime) in /workspaces/runtime/src/libraries/Microsoft.Extensions.DependencyInjection.Specification.Tests/src/DependencyInjectionSpecificationTests.cs:line 155

Grace

  Failed Microsoft.Extensions.DependencyInjection.Specification.GraceDependencyInjectionSpecificationTests.NonSingletonService_WithInjectedProvider_ResolvesScopeProvider(lifetime: Scoped) [7 ms]
  Error Message:
   Assert.Same() Failure
Expected: GraceServiceProvider { }
Actual:   LifetimeScope { Keys = [], KeyValuePairs = [], Parent = [], ScopeId = 79534da3-11c5-4f88-9389-adaa3a8b5aaa, ScopeName = "", ... }
  Stack Trace:
     at Microsoft.Extensions.DependencyInjection.Specification.DependencyInjectionSpecificationTests.NonSingletonService_WithInjectedProvider_ResolvesScopeProvider(ServiceLifetime lifetime) in /workspaces/runtime/src/libraries/Microsoft.Extensions.DependencyInjection.Specification.Tests/src/DependencyInjectionSpecificationTests.cs:line 155

LightInject

Failed Microsoft.Extensions.DependencyInjection.Specification.LightInjectDependencyInjectionSpecificationTests.NonSingletonService_WithInjectedProvider_ResolvesScopeProvider(lifetime: Scoped) [8 ms]
  Error Message:
   Assert.Same() Failure
Expected: LightInjectServiceProvider { }
Actual:   LightInjectServiceProvider { }
  Stack Trace:
     at Microsoft.Extensions.DependencyInjection.Specification.DependencyInjectionSpecificationTests.NonSingletonService_WithInjectedProvider_ResolvesScopeProvider(ServiceLifetime lifetime) in /workspaces/runtime/src/libraries/Microsoft.Extensions.DependencyInjection.Specification.Tests/src/DependencyInjectionSpecificationTests.cs:line 155

StashBox

Failed Microsoft.Extensions.DependencyInjection.Specification.StashBoxDependencyInjectionSpecificationTests.NonSingletonService_WithInjectedProvider_ResolvesScopeProvider(lifetime: Scoped) [1 ms]
  Error Message:
   Assert.Same() Failure
Expected: ResolutionScope { Name = null, ParentScope = ResolutionScope { Name = null, ParentScope = null } }
Actual:   StashboxServiceProvider { }
  Stack Trace:
     at Microsoft.Extensions.DependencyInjection.Specification.DependencyInjectionSpecificationTests.NonSingletonService_WithInjectedProvider_ResolvesScopeProvider(ServiceLifetime lifetime) in /workspaces/runtime/src/libraries/Microsoft.Extensions.DependencyInjection.Specification.Tests/src/DependencyInjectionSpecificationTests.cs:line 155

And I did some more "variations" on this test case on my machine which raises an additional question. The existing SingletonServiceCanBeResolvedFromScope tests for the correct service provider by reference equality because there isn't much else to go on, but from my tests it looks like that might not be the best idea. It's also yet another assumption.

Comparing the service provider that is injected into the ClassWithServiceProvider with the service provider of the scope of course only works if they are actually the same instance. If I change the test to

            using (var scope1 = provider.CreateScope())
            {
                scopedSp1 = scope1.ServiceProvider;
                instance1 = scope1.ServiceProvider.GetRequiredService<ClassWithServiceProvider>();
                instance1Inner = instance1.ServiceProvider.GetRequiredService<ClassWithServiceProvider>();
            }

            // ...
            Assert.Same(instance1.ServiceProvider, instance1Inner.ServiceProvider);
            Assert.Same(instance2.ServiceProvider, instance2Inner.ServiceProvider);
            Assert.NotSame(instance1.ServiceProvider, instance2.ServiceProvider);

then

  • StashBox passes which tells me that stashbox injects a different instance than the scope.ServiceProvider instance that is used to do the injection, but while the instance is different, it seems to preserve the right context
  • LightInject fails on the last assertion which would indicate that it always injects the SAME service provider which presumably is the root service provider
  • Autofac and Grace both fail at the first assertion and only in the case where ClassWithServiceProvider is registered as a transient service. So, they both seem to inject new and different instances of the service provider as if the service provider itself was working as a transient registration in that case

Assumptions are hard 😂.

Generally speaking, checking service providers with reference equality seems a bad idea since that does introduce the additional assumption that new instances of the service provider are only created when a new scope is created and that does not apply to all containers.

In the existing SingletonServiceCanBeResolvedFromScope the latter two assertions are probably misleading since in some containers the scope.ServiceProvider is not the same instance that is injected into services anyway while the test implies that they are not the same because of the difference in scope.

Assert.Same(instance1.ServiceProvider, instance2.ServiceProvider);
Assert.NotSame(instance1.ServiceProvider, scopedSp1);
Assert.NotSame(instance2.ServiceProvider, scopedSp2);

If I change the test to the sample code above AND if I limit the test to scoped binding of ClassWithServiceProvider then all containers except for LightInject pass and LightInject fails on the last assertion because it does in fact inject the same service provider instance regardless of scope. That means that LightInject should fail because that is definitely the problem that the test is trying to uncover. In that case, the behavior for transient bindings is not covered since every container deals with that differently.

I'll go and experiment some more with some more elaborate test setups so that the test does not have to rely on service provider reference equality at all but instead only tests the actual service instantiation behavior when creating service instances through a "captured" service provider like in ClassWithServiceProvider

@lord-executor lord-executor force-pushed the service-provider-scoped-injection-test branch from b375e42 to dced533 Compare January 3, 2022 18:58
@lord-executor
Copy link
Contributor Author

OK, now we have version 2.0 of the new test. I replaced the old commit with the new one.

@eerhardt & @davidfowl
I think this version is an overall improvement since it doesn't rely on reference equality for service provider instances. I also updated the existing SingletonServiceCanBeResolvedFromScope to no longer do that and be more consistent with the new test.

The "trick" for testing if the service provider is tied to the correct scope is simply to use it to instantiate a scoped service (FakeService). These service instances, we can actually reliably test with reference equality.

With this new implementation, only LightInject fails with

  Failed Microsoft.Extensions.DependencyInjection.Specification.LightInjectDependencyInjectionSpecificationTests.NonSingletonService_WithInjectedProvider_ResolvesScopeProvider(lifetime: Scoped) [9 ms]
  Error Message:
   Assert.NotSame() Failure
  Stack Trace:
     at Microsoft.Extensions.DependencyInjection.Specification.DependencyInjectionSpecificationTests.NonSingletonService_WithInjectedProvider_ResolvesScopeProvider(ServiceLifetime lifetime) in /workspaces/runtime/src/libraries/Microsoft.Extensions.DependencyInjection.Specification.Tests/src/DependencyInjectionSpecificationTests.cs:line 163

which means that LightInject is always using the root scope service provider since that is the only way that the scoped FakeService instances can be the same.

// Assert
Assert.Same(instance1.ServiceProvider, instance2.ServiceProvider);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this is breaking the original intention of this test. See dotnet/extensions#2236 and dotnet/extensions#1301 for more information on why this test was written this way.

Copy link
Contributor Author

@lord-executor lord-executor Jan 3, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm... I don't think so. The fact that the instances are the same is not strictly necessary for #1301. All that is really needed is that the two providers are bound to the same (root) scope which the new version is validating with

Assert.Same(fakeServiceFromScope1, fakeServiceFromScope2);

In fact, if you add the very simple test

        [Fact]
        public void RootProviderEquality()
        {
            // Arrange
            var collection = new TestServiceCollection();
            collection.AddScoped<IFakeService, FakeService>();
            collection.Add(new ServiceDescriptor(typeof(ClassWithServiceProvider), typeof(ClassWithServiceProvider), ServiceLifetime.Scoped));
            var provider = CreateServiceProvider(collection);
            var sameProvider = provider.GetRequiredService<IServiceProvider>();
            var sameProvider2 = provider.GetRequiredService<IServiceProvider>();

            // Assert
            Assert.Same(sameProvider, sameProvider2);
            Assert.Same(provider, sameProvider);
        }

Then that fails for Grace, LightInject, Autofac and StashBox because they do not actually ensure that the service provider is the same instance. They do however ensure that the instances are tied to the same scope which is what these changes here are testing. Ensuring reference equality of the provider instance is sufficient, but not necessary. As long as the scope is the right one, the problem in #1301 should be solved.

And I did a similar test for scopes which generates similarly entertaining failures with the same set of containers and additionally DryIoc and Lamar also fail that one.

        [Fact]
        public void ScopedProviderEquality()
        {
            // Arrange
            var collection = new TestServiceCollection();
            collection.AddScoped<IFakeService, FakeService>();
            collection.Add(new ServiceDescriptor(typeof(ClassWithServiceProvider), typeof(ClassWithServiceProvider), ServiceLifetime.Scoped));
            var provider = CreateServiceProvider(collection);

            IServiceProvider scopeProvider = null;
            IServiceProvider sameProvider = null;
            IServiceProvider sameProvider2 = null;

            using (var scope = provider.CreateScope())
            {
                scopeProvider = scope.ServiceProvider;
                sameProvider = provider.GetRequiredService<IServiceProvider>();
                sameProvider2 = provider.GetRequiredService<IServiceProvider>();
            }

            // Assert
            Assert.Same(scopeProvider, sameProvider);
            Assert.Same(sameProvider, sameProvider2);
        }

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If I actually enable that singleton test for Unity (where it is currently still disabled), then it does still fail that test, but in an "interesting" way:

Failed Microsoft.Extensions.DependencyInjection.Specification.UnityDependencyInjectionSpecificationTests.SingletonServiceCanBeResolvedFromScope [23 ms]
  Error Message:
   System.InvalidOperationException : No service for type 'Microsoft.Extensions.DependencyInjection.Specification.Fakes.IFakeService' has been registered.
  Stack Trace:
     at Microsoft.Extensions.DependencyInjection.ServiceProviderServiceExtensions.GetRequiredService(IServiceProvider provider, Type serviceType) in /workspaces/runtime/src/libraries/Microsoft.Extensions.DependencyInjection.Abstractions/src/ServiceProviderServiceExtensions.cs:line 58
   at Microsoft.Extensions.DependencyInjection.ServiceProviderServiceExtensions.GetRequiredService[T](IServiceProvider provider) in /workspaces/runtime/src/libraries/Microsoft.Extensions.DependencyInjection.Abstractions/src/ServiceProviderServiceExtensions.cs:line 78
   at Microsoft.Extensions.DependencyInjection.Specification.DependencyInjectionSpecificationTests.SingletonServiceCanBeResolvedFromScope() in /workspaces/runtime/src/libraries/Microsoft.Extensions.DependencyInjection.Specification.Tests/src/DependencyInjectionSpecificationTests.cs:line 196

Meaning that when it tries to resolve the fake service in the second scope, it can't find a registration for IFakeService. This is admittedly not what I was expecting.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just to be clear. I'll of course revert the changes to this test if you prefer, but I do think that the reference equality testing on the service provider is unnecessary and misleading as demonstrated by my two simple demo tests above.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

as demonstrated by my two simple demo tests above.

This test is for Singleton services, and both those demo tests use Scoped services.

For Singleton services, I believe the intention is that the ServiceProvider they get is the root provider, and thus they should be the same.

@davidfowl - you originally wrote the test. Do you have any thoughts here?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, this test is for Singleton services. And the ClassWithServiceProvider is still registered as a singleton and thus should be injected with the root provider. The scoped IFakeService is just my "canary in the coal mine" for figuring out if the singleton was actually injected with the root scope. It still tests the same scenario, just in a different way.

  • scopedFakeServiceFromScope1 is resolved from the scope's service provider => that should be a fresh instance bound to scope1
  • fakeServiceFromScope1 is resolved from the service provider that was injected into the singleton which should be the root scope => that should give me a different instance to scopedFakeServiceFromScope1
  • then we play the same game with scope2 and compare results based on where we expect the same or different instances

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Btw. the reason for the weird failure mode of Unity in the Singleton scenario has actually already been explained by @davidfowl in #extensions/1301 I just didn't read his explanation carefully enough.

Even though Foo is a singleton, the service provider injected is from the first scope it's resolved from. That's a recipe for disaster.

Unity fails for the same reason it failed before, because it injects the scope1 service provider into the singleton. That scope is then disposed and in the code for scope2 it is trying to resolve a service through this already disposed scope1 service provider which fails because it can no longer find the IFakeService registration.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let me take a deeper look at this today. On second glance I can see why my test would make other containers fail. The service provider instance isn't as important as the lifetime of the objects that come out of it.

Copy link
Member

@eerhardt eerhardt left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the new test looks fine. But I think the changes to the existing (singleton) test should get reviewed by @davidfowl, who is out until next week.

@lord-executor
Copy link
Contributor Author

No worries. It's not like this is an urgent issue, so I'll just wait and be patient.

@eerhardt
Copy link
Member

@davidfowl thoughts here?

I still think the existing test should remain as-is. It is a simpler way of testing the scenario, instead of the indirection being added here.

@lord-executor
Copy link
Contributor Author

@eerhardt : I wasn't quite sure about the protocol and I didn't want to cause too much confusion by just completely removing the code that the previous discussion is all about, so I just created a seperate PR #64558 which only contains the new test and leaves the existing one alone. If you like that one better, then I'll just close this one.

- Refactored existing singleton test to avoid testing service provider reference equality
@eerhardt eerhardt force-pushed the service-provider-scoped-injection-test branch from dced533 to d3f9d86 Compare February 7, 2022 16:37
@eerhardt
Copy link
Member

eerhardt commented Feb 7, 2022

I've merged #64558, so the new test is now added. All that remains here is the refactoring of the existing test. @davidfowl - thoughts?

@eerhardt
Copy link
Member

eerhardt commented Mar 7, 2022

Thanks for the work here, @lord-executor.

I'm going to close out this PR since I don't believe this test refactoring is adding value and is making it harder to understand what is being tested by adding the extra indirection. If the current test proves to be a problem with DI implementers, we can re-open it.

@eerhardt eerhardt closed this Mar 7, 2022
@ghost ghost locked as resolved and limited conversation to collaborators Apr 6, 2022
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
area-Extensions-DependencyInjection community-contribution Indicates that the PR has been added by a community member
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants