Skip to content

Commit 6ed4084

Browse files
initial implementation of optional keyed services
1 parent e802883 commit 6ed4084

File tree

4 files changed

+209
-3
lines changed

4 files changed

+209
-3
lines changed
Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
using AwesomeAssertions;
2+
using Microsoft.Extensions.DependencyInjection;
3+
using Microsoft.Extensions.DependencyInjection.Extensions;
4+
using Ninject.Web.AspNetCore.Test.Fakes;
5+
using System;
6+
using System.Collections.Generic;
7+
using System.Linq;
8+
using Xunit;
9+
10+
namespace Ninject.Web.AspNetCore.Test.Unit
11+
{
12+
13+
#if NET8_0_OR_GREATER
14+
15+
public class ServiceProviderKeyedTest
16+
{
17+
18+
[Fact]
19+
public void OptionalKeyedServiceCollectionExisting_CorrectServiceResolved()
20+
{
21+
var collection = new ServiceCollection();
22+
collection.Add(new ServiceDescriptor(typeof(IWarrior),"Samurai", typeof(Samurai), ServiceLifetime.Transient));
23+
collection.Add(new ServiceDescriptor(typeof(IWarrior), "Ninja", new Ninja("test")));
24+
var kernel = CreateTestKernel(collection);
25+
var provider = CreateServiceProvider(kernel);
26+
27+
provider.GetKeyedService(typeof(IWarrior), "Ninja").Should().NotBeNull().And.BeOfType(typeof(Ninja));
28+
provider.GetKeyedService(typeof(IWarrior), "Samurai").Should().NotBeNull().And.BeOfType(typeof(Samurai));
29+
30+
}
31+
32+
[Fact]
33+
public void OptionalKeyedNinjectDirectBindingExisting_CorrectServiceResolved()
34+
{
35+
var kernel = CreateTestKernel();
36+
kernel.Bind<IWarrior>().To<Samurai>().WithMetadata(nameof(ServiceKey), new ServiceKey("Samurai"));
37+
kernel.Bind<IWarrior>().ToConstant(new Ninja("test")).WithMetadata(nameof(ServiceKey), new ServiceKey("Ninja"));
38+
var provider = CreateServiceProvider(kernel);
39+
40+
provider.GetKeyedService(typeof(IWarrior), "Samurai").Should().NotBeNull().And.BeOfType(typeof(Samurai));
41+
provider.GetKeyedService(typeof(IWarrior), "Ninja").Should().NotBeNull().And.BeOfType(typeof(Ninja));
42+
}
43+
44+
[Fact]
45+
public void OptionalKeyedNonExisting_SingleServiceResolvedToNull()
46+
{
47+
var kernel = CreateTestKernel();
48+
var provider = CreateServiceProvider(kernel);
49+
50+
provider.GetKeyedService(typeof(IWarrior), "Samurai").Should().BeNull();
51+
}
52+
53+
[Fact]
54+
public void OptionalExistingMultipleKeydServices_ResolvedQueriedAsList()
55+
{
56+
var kernel = CreateTestKernel();
57+
kernel.Bind<IWarrior>().To<Samurai>().WithMetadata(nameof(ServiceKey), new ServiceKey("Samurai"));;
58+
kernel.Bind<IWarrior>().ToConstant(new Ninja("test")).WithMetadata(nameof(ServiceKey), new ServiceKey("Ninja"));
59+
var provider = CreateServiceProvider(kernel);
60+
61+
var result = provider.GetService(typeof(IList<IWarrior>)) as IEnumerable<IWarrior>;
62+
63+
result.Should().NotBeNull();
64+
var resultList = result.ToList();
65+
resultList.Should().HaveCount(2);
66+
resultList.Should().Contain(x => x is Samurai);
67+
resultList.Should().Contain(x => x is Ninja);
68+
}
69+
70+
[Fact]
71+
public void ExistingMultipleServices_ResolvesNonKeyedToNull()
72+
{
73+
var kernel = CreateTestKernel();
74+
kernel.Bind<IWarrior>().To<Samurai>().WithMetadata(nameof(ServiceKey), new ServiceKey("Samurai"));;
75+
kernel.Bind<IWarrior>().ToConstant(new Ninja("test")).WithMetadata(nameof(ServiceKey), new ServiceKey("Ninja"));
76+
var provider = CreateServiceProvider(kernel);
77+
78+
provider.GetService(typeof(IWarrior)).Should().BeNull();
79+
}
80+
81+
private IServiceProvider CreateServiceProvider(AspNetCoreKernel kernel)
82+
{
83+
NinjectServiceProviderBuilder builder = CreateServiceProviderBuilder(kernel);
84+
var provider = builder.Build();
85+
return provider;
86+
}
87+
88+
private NinjectServiceProviderBuilder CreateServiceProviderBuilder(AspNetCoreKernel kernel)
89+
{
90+
var collection = new ServiceCollection();
91+
var factory = new NinjectServiceProviderFactory(kernel);
92+
var builder = factory.CreateBuilder(collection);
93+
return builder;
94+
}
95+
96+
private AspNetCoreKernel CreateTestKernel(IServiceCollection collection = null)
97+
{
98+
var kernel = new AspNetCoreKernel(new NinjectSettings() { LoadExtensions = false });
99+
kernel.Load(typeof(AspNetCoreApplicationPlugin).Assembly);
100+
if (collection != null)
101+
{
102+
new ServiceCollectionAdapter().Populate(kernel, collection);
103+
}
104+
105+
return kernel;
106+
}
107+
108+
}
109+
#endif
110+
}

src/Ninject.Web.AspNetCore/NinjectServiceProvider.cs

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,9 @@ namespace Ninject.Web.AspNetCore
1919
/// we pass an <see cref="IServiceScope"/> constructor argument when creating the root service provider.
2020
/// </summary>
2121
public class NinjectServiceProvider : IServiceProvider, ISupportRequiredService, IDisposable
22+
#if NET8_0_OR_GREATER
23+
, IKeyedServiceProvider
24+
#endif
2225
{
2326
private readonly IResolutionRoot _resolutionRoot;
2427
private readonly IServiceScope _scope;
@@ -45,5 +48,17 @@ public void Dispose()
4548
{
4649
_scope?.Dispose();
4750
}
51+
52+
#if NET8_0_OR_GREATER
53+
public object GetKeyedService(Type serviceType, object serviceKey)
54+
{
55+
return _resolutionRoot.TryGet(serviceType, metadata => metadata.Get<ServiceKey>(nameof(ServiceKey))?.Key == serviceKey);
56+
}
57+
58+
public object GetRequiredKeyedService(Type serviceType, object serviceKey)
59+
{
60+
throw new NotImplementedException();
61+
}
62+
#endif
4863
}
4964
}

src/Ninject.Web.AspNetCore/ServiceCollectionAdapter.cs

Lines changed: 65 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,38 @@ private IBindingWithOrOnSyntax<T> ConfigureImplementationAndLifecycle<T>(
5656
BindingIndex bindingIndex) where T : class
5757
{
5858
IBindingNamedWithOrOnSyntax<T> result;
59+
#if NET8_0_OR_GREATER
60+
if (descriptor.IsKeyedService)
61+
{
62+
result = ConfigureImplementationAndLifecycleKeyed(bindingToSyntax, descriptor);
63+
}
64+
#endif
65+
#if NET8_0_OR_GREATER
66+
else
67+
{
68+
#endif
69+
result = ConfigureImplementationAndLifecycleNonKeyed(bindingToSyntax, descriptor);
70+
#if NET8_0_OR_GREATER
71+
}
72+
#endif
73+
74+
var resultWithMetadata = result
75+
.WithMetadata(nameof(ServiceDescriptor), descriptor)
76+
.WithMetadata(nameof(BindingIndex), bindingIndex.Next(descriptor.ServiceType));
77+
78+
#if NET8_0_OR_GREATER
79+
if (descriptor.IsKeyedService)
80+
{
81+
resultWithMetadata = resultWithMetadata.WithMetadata(nameof(ServiceKey), new ServiceKey(descriptor.ServiceKey));
82+
}
83+
#endif
84+
return resultWithMetadata;
85+
}
86+
87+
private IBindingNamedWithOrOnSyntax<T> ConfigureImplementationAndLifecycleNonKeyed<T>(IBindingToSyntax<T> bindingToSyntax,
88+
ServiceDescriptor descriptor) where T : class
89+
{
90+
IBindingNamedWithOrOnSyntax<T> result;
5991
if (descriptor.ImplementationType != null)
6092
{
6193
result = ConfigureLifecycle(bindingToSyntax.To(descriptor.ImplementationType), descriptor.Lifetime);
@@ -79,10 +111,40 @@ private IBindingWithOrOnSyntax<T> ConfigureImplementationAndLifecycle<T>(
79111
result = bindingToSyntax.ToMethod(context => descriptor.ImplementationInstance as T).InSingletonScope();
80112
}
81113

82-
return result
83-
.WithMetadata(nameof(ServiceDescriptor), descriptor)
84-
.WithMetadata(nameof(BindingIndex), bindingIndex.Next(descriptor.ServiceType));
114+
return result;
115+
}
116+
117+
#if NET8_0_OR_GREATER
118+
private IBindingNamedWithOrOnSyntax<T> ConfigureImplementationAndLifecycleKeyed<T>(IBindingToSyntax<T> bindingToSyntax,
119+
ServiceDescriptor descriptor) where T : class
120+
{
121+
IBindingNamedWithOrOnSyntax<T> result;
122+
if (descriptor.KeyedImplementationType != null)
123+
{
124+
result = ConfigureLifecycle(bindingToSyntax.To(descriptor.KeyedImplementationType), descriptor.Lifetime);
125+
}
126+
else if (descriptor.KeyedImplementationFactory != null)
127+
{
128+
129+
result = ConfigureLifecycle(bindingToSyntax.ToMethod(context
130+
=>
131+
{
132+
// When resolved through the ServiceProviderScopeResolutionRoot which adds this parameter, the
133+
// correct _scoped_ IServiceProvider is used. Fall back to root IServiceProvider when not created
134+
// through a NinjectServiceProvider (some tests do this to prove a point)
135+
var scopeProvider = context.GetServiceProviderScopeParameter()?.SourceServiceProvider ?? context.Kernel.Get<IServiceProvider>();
136+
return descriptor.KeyedImplementationFactory(scopeProvider, descriptor.ServiceKey) as T;
137+
}), descriptor.Lifetime);
138+
}
139+
else
140+
{
141+
// use ToMethod here as ToConstant has the wrong return type.
142+
result = bindingToSyntax.ToMethod(context => descriptor.KeyedImplementationInstance as T).InSingletonScope();
143+
}
144+
145+
return result;
85146
}
147+
#endif
86148

87149
private IBindingNamedWithOrOnSyntax<T> ConfigureLifecycle<T>(
88150
IBindingInSyntax<T> bindingInSyntax,
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
using System;
2+
3+
namespace Ninject.Web.AspNetCore
4+
{
5+
6+
#if NET8_0_OR_GREATER
7+
8+
public class ServiceKey
9+
{
10+
public object Key { get; }
11+
12+
public ServiceKey(object key)
13+
{
14+
Key = key;
15+
}
16+
}
17+
#endif
18+
19+
}

0 commit comments

Comments
 (0)