Skip to content

Commit 5865016

Browse files
Merge pull request #13 from lord-executor/bugfix/remotenavigationmanager-has-not-been-initialized
Using the correct (scoped) IServiceProvider for implementation factories Closes #12
2 parents 230a8c8 + 307e470 commit 5865016

File tree

5 files changed

+96
-5
lines changed

5 files changed

+96
-5
lines changed
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
using System;
2+
using FluentAssertions;
3+
using Microsoft.Extensions.DependencyInjection;
4+
using Microsoft.Extensions.DependencyInjection.Extensions;
5+
using Ninject.Web.AspNetCore.Test.Fakes;
6+
using Xunit;
7+
8+
namespace Ninject.Web.AspNetCore.Test.Unit;
9+
10+
public class ServiceProviderScopeTest : TestKernelContext
11+
{
12+
[Theory]
13+
[MemberData(nameof(ServiceConfigurations))]
14+
public void ScopedServices_ResolvedInScope_ShouldBeDifferent(ServiceCollection serviceCollection)
15+
{
16+
var kernel = CreateKernel(serviceCollection);
17+
var provider = kernel.Get<IServiceProvider>();
18+
19+
var rootKnight = provider.GetRequiredService<Knight>();
20+
21+
using (var scope = provider.CreateScope())
22+
{
23+
// The IWeapon instantiated inside of this scope should also be tied to this scope when created through
24+
// a ServiceDescriptor.ImplementationFactory
25+
var levelOneKnight = scope.ServiceProvider.GetRequiredService<Knight>();
26+
AssertNotSameKnightAndWeapon(levelOneKnight, rootKnight);
27+
28+
var levelOneKnight2 = scope.ServiceProvider.GetRequiredService<Knight>();
29+
AssertSameKnightAndWeapon(levelOneKnight2, levelOneKnight);
30+
}
31+
32+
var rootKnight2 = provider.GetRequiredService<Knight>();
33+
AssertSameKnightAndWeapon(rootKnight2, rootKnight);
34+
}
35+
36+
private void AssertSameKnightAndWeapon(Knight value, Knight expected)
37+
{
38+
value.Should().BeSameAs(expected);
39+
value.Weapon.Should().BeSameAs(expected.Weapon);
40+
}
41+
42+
private void AssertNotSameKnightAndWeapon(Knight value, Knight expected)
43+
{
44+
value.Should().NotBeSameAs(expected);
45+
value.Weapon.Should().NotBeSameAs(expected.Weapon);
46+
}
47+
48+
public static TheoryData<ServiceCollection> ServiceConfigurations => new TheoryData<ServiceCollection>
49+
{
50+
{
51+
new ServiceCollection() {
52+
new ServiceDescriptor(typeof(Knight), typeof(Knight), ServiceLifetime.Scoped),
53+
new ServiceDescriptor(typeof(IWeapon), typeof(Longsword), ServiceLifetime.Scoped)
54+
}
55+
},
56+
{
57+
new ServiceCollection() {
58+
new ServiceDescriptor(typeof(Knight), typeof(Knight), ServiceLifetime.Scoped),
59+
new ServiceDescriptor(typeof(Longsword), typeof(Longsword), ServiceLifetime.Scoped),
60+
// See https://github.com/lord-executor/Ninject.Web.AspNetCore/issues/12
61+
// The difference here is the use of a descriptor with an implementation factory where the implementation
62+
// must make sure that the correct _scoped_ IServiceProvider instance is passed to the factory
63+
new ServiceDescriptor(typeof(IWeapon),serviceProvider => serviceProvider.GetService<Longsword>(), ServiceLifetime.Scoped)
64+
}
65+
},
66+
};
67+
}

src/Ninject.Web.AspNetCore/AspNetCoreApplicationPlugin.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ public object GetRequestScope(IContext context)
1919
// when being instantiated through the ServiceProviderScopeResolutionRoot, the parameter for explicit nested scopes
2020
// created through IServiceScopeFactory.CreateScope has precedence in order to preserve the behavior that is expected
2121
// from IServiceProvider with scoped services.
22-
var scope = context.Parameters.OfType<ServiceProviderScopeParameter>().SingleOrDefault()?.GetValue(context, null);
22+
var scope = context.GetServiceProviderScopeParameter()?.GetValue(context, null);
2323
// returns the currently active request scope. Used when binding with scope InRequestScope.
2424
return scope ?? RequestScope.Current ?? throw new ActivationException("Trying to activate a service InRequestScope without a request scope present");
2525
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
using System.Linq;
2+
using Ninject.Activation;
3+
4+
namespace Ninject.Web.AspNetCore
5+
{
6+
/// <summary>
7+
/// Extension methods on <see cref="IContext"/> to simplify dealing with the <see cref="ServiceProviderScopeParameter" />
8+
/// </summary>
9+
public static class ContextExtensions
10+
{
11+
/// <summary>
12+
/// Gets the <see cref="ServiceProviderScopeParameter"/> from the context parameters.
13+
/// </summary>
14+
/// <param name="context">The resolution context from which to retrieve the scope parameter</param>
15+
/// <returns>The scope from which the request originated or <c>null</c> if there is none</returns>
16+
public static ServiceProviderScopeParameter GetServiceProviderScopeParameter(this IContext context)
17+
{
18+
return context.Parameters.OfType<ServiceProviderScopeParameter>().SingleOrDefault();
19+
}
20+
}
21+
}

src/Ninject.Web.AspNetCore/NinjectServiceProviderBuilder.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ public IServiceProvider Build()
2525
// It essentially always injects the instance that is being used for the instantiation
2626
// which kinda makes sense, but is not documented ANYWHERE, not tested ANYWHERE and was really quite difficult to
2727
// figure out.
28-
var scopeProvider = context.Parameters.OfType<ServiceProviderScopeParameter>().SingleOrDefault()?.SourceServiceProvider;
28+
var scopeProvider = context.GetServiceProviderScopeParameter()?.SourceServiceProvider;
2929
if (scopeProvider != null)
3030
{
3131
var descriptor = context.Request.ParentContext?.Binding.Metadata.Get<ServiceDescriptor>(nameof(ServiceDescriptor));

src/Ninject.Web.AspNetCore/ServiceCollectionAdapter.cs

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -66,8 +66,11 @@ private IBindingWithOrOnSyntax<T> ConfigureImplementationAndLifecycle<T>(
6666
result = ConfigureLifecycle(bindingToSyntax.ToMethod(context
6767
=>
6868
{
69-
var provider = context.Kernel.Get<IServiceProvider>();
70-
return descriptor.ImplementationFactory(provider) as T;
69+
// When resolved through the ServiceProviderScopeResolutionRoot which adds this parameter, the
70+
// correct _scoped_ IServiceProvider is used. Fall back to root IServiceProvider when not created
71+
// through a NinjectServiceProvider (some tests do this to prove a point)
72+
var scopeProvider = context.GetServiceProviderScopeParameter()?.SourceServiceProvider ?? context.Kernel.Get<IServiceProvider>();
73+
return descriptor.ImplementationFactory(scopeProvider) as T;
7174
}), descriptor.Lifetime);
7275
}
7376
else
@@ -99,7 +102,7 @@ private IBindingNamedWithOrOnSyntax<T> ConfigureLifecycle<T>(
99102
// Microsoft.Extensions.DependencyInjection expects transient services to be disposed when the IServiceScope
100103
// in which they were created is disposed. See the compliance tests for more details.
101104
return bindingInSyntax.InScope(context => {
102-
var scope = context.Parameters.OfType<ServiceProviderScopeParameter>().SingleOrDefault();
105+
var scope = context.GetServiceProviderScopeParameter();
103106
return scope?.DeriveTransientScope();
104107
});
105108
default:

0 commit comments

Comments
 (0)