Skip to content

Commit 03c1aa8

Browse files
authored
Add support for "Scoped" lifetime (#31)
1 parent d41299e commit 03c1aa8

14 files changed

+445
-32
lines changed

.editorconfig

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
[*.cs]
2+
3+
# Default severity for all analyzer diagnostics
4+
dotnet_analyzer_diagnostic.severity = silent

README.md

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,8 @@ A Dependency Injection (DI) Container provides functionality and automates many
2727
This API mirrors as close as possible the official .NET
2828
[DependencyInjection](https://docs.microsoft.com/en-us/dotnet/core/extensions/dependency-injection). Exceptions are mainly derived from the lack of generics support in .NET nanoFramework.
2929

30+
The .NET nanoFramework [Generic Host](https://github.com/nanoframework/nanoFramework.Hosting) provides convenience methods for creating dependency injection (DI) application containers with preconfigured defaults.
31+
3032
## Usage
3133

3234
### Service Collection
@@ -91,6 +93,20 @@ var service = (RootObject)serviceProvider.GetService(typeof(RootObject));
9193
service.ServiceObject.Three = "3";
9294
```
9395

96+
Create a scoped Service Provider providing convient access to crate and distroy scoped object.
97+
98+
```csharp
99+
var serviceProvider = new ServiceCollection()
100+
.AddScoped(typeof(typeof(ServiceObject))
101+
.BuildServiceProvider();
102+
103+
using (var scope = serviceProvider.CreateScope())
104+
{
105+
var service = scope.ServiceProvider.GetServices(typeof(ServiceObject));
106+
service.ServiceObject.Three = "3";
107+
}
108+
```
109+
94110
## Activator Utilities
95111

96112
An instance of an object can be created by calling its constructor with any dependencies resolved through the service provider. Automatically instantiate a type with constructor arguments provided from an IServiceProvider without having to register the type with the DI Container.
@@ -116,6 +132,18 @@ var serviceProvider = new ServiceCollection()
116132
.BuildServiceProvider(new ServiceProviderOptions() { ValidateOnBuild = true });
117133
```
118134

135+
136+
### Validate Scopes
137+
138+
A check verifying that scoped services never gets resolved from root provider. Validate on build is configured false by default.
139+
140+
```csharp
141+
var serviceProvider = new ServiceCollection()
142+
.AddSingleton(typeof(IServiceObject), typeof(ServiceObject))
143+
.BuildServiceProvider(new ServiceProviderOptions() { ValidateScopes = true });
144+
```
145+
146+
119147
## Example Application Container
120148

121149
```csharp
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
//
2+
// Copyright (c) .NET Foundation and Contributors
3+
// See LICENSE file in the project root for full license information.
4+
//
5+
6+
using System;
7+
8+
namespace nanoFramework.DependencyInjection
9+
{
10+
/// <summary>
11+
/// Defines scope for <see cref="IServiceProvider"/>.
12+
/// </summary>
13+
public interface IServiceScope : IDisposable
14+
{
15+
/// <summary>
16+
/// The <see cref="IServiceProvider"/> used to resolve dependencies from the scope.
17+
/// </summary>
18+
IServiceProvider ServiceProvider { get; }
19+
}
20+
}

nanoFramework.DependencyInjection/DependencyInjection/ServiceCollectionServiceExtensions.cs

Lines changed: 44 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
namespace nanoFramework.DependencyInjection
1010
{
1111
/// <summary>
12-
/// Extensions for <see cref="ServiceCollection"/>.
12+
/// Extensions for <see cref="IServiceCollection"/>.
1313
/// </summary>
1414
public static class ServiceCollectionServiceExtensions
1515
{
@@ -123,6 +123,49 @@ public static IServiceCollection AddTransient(this IServiceCollection services,
123123
return services.AddTransient(serviceType, serviceType);
124124
}
125125

126+
/// <summary>
127+
/// Adds a scoped service of the type specified in <paramref name="serviceType"/> with an
128+
/// implementation of the type specified in <paramref name="implementationType"/> to the
129+
/// specified <see cref="IServiceCollection"/>.
130+
/// </summary>
131+
/// <param name="services">The <see cref="IServiceCollection"/> to add the service to.</param>
132+
/// <param name="serviceType">The type of the service to register.</param>
133+
/// <param name="implementationType">The implementation type of the service.</param>
134+
/// <returns>A reference to this instance after the operation has completed.</returns>
135+
/// <seealso cref="ServiceLifetime.Scoped"/>
136+
/// <exception cref="ArgumentNullException"><paramref name="services"/> can't be <see langword="null"/>.</exception>
137+
public static IServiceCollection AddScoped(this IServiceCollection services, Type serviceType, Type implementationType)
138+
{
139+
if (services == null)
140+
{
141+
throw new ArgumentNullException();
142+
}
143+
144+
var descriptor = new ServiceDescriptor(serviceType, implementationType, ServiceLifetime.Scoped);
145+
services.Add(descriptor);
146+
147+
return services;
148+
}
149+
150+
/// <summary>
151+
/// Adds a scoped service of the type specified in <paramref name="serviceType"/> to the
152+
/// specified <see cref="IServiceCollection"/>.
153+
/// </summary>
154+
/// <param name="services">The <see cref="IServiceCollection"/> to add the service to.</param>
155+
/// <param name="serviceType">The type of the service to register and the implementation to use.</param>
156+
/// <returns>A reference to this instance after the operation has completed.</returns>
157+
/// <seealso cref="ServiceLifetime.Scoped"/>
158+
/// <exception cref="ArgumentNullException"><paramref name="services"/> can't be <see langword="null"/>.</exception>
159+
public static IServiceCollection AddScoped(this IServiceCollection services, Type serviceType)
160+
{
161+
if (services == null)
162+
{
163+
throw new ArgumentNullException();
164+
}
165+
166+
return services.AddScoped(serviceType, serviceType);
167+
}
168+
126169
/// <summary>
127170
/// Adds the specified <paramref name="descriptor"/> to the <paramref name="collection"/> if the
128171
/// service type hasn't already been registered.

nanoFramework.DependencyInjection/DependencyInjection/ServiceLifetime.cs

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,11 @@ public enum ServiceLifetime
1818
/// <summary>
1919
/// Specifies that a new instance of the service will be created every time it is requested.
2020
/// </summary>
21-
Transient
21+
Transient,
22+
23+
/// <summary>
24+
/// Specifies that a single instance of the service will be created within a scope.
25+
/// </summary>
26+
Scoped
2227
}
2328
}

nanoFramework.DependencyInjection/DependencyInjection/ServiceProvider.cs

Lines changed: 16 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,8 @@ namespace nanoFramework.DependencyInjection
1515
public sealed class ServiceProvider : IServiceProvider, IDisposable
1616
{
1717
private bool _disposed;
18-
internal ServiceProviderEngine _engine;
18+
19+
internal ServiceProviderEngine Engine { get; }
1920

2021
internal ServiceProvider(IServiceCollection services, ServiceProviderOptions options)
2122
{
@@ -29,9 +30,10 @@ internal ServiceProvider(IServiceCollection services, ServiceProviderOptions opt
2930
throw new ArgumentNullException();
3031
}
3132

32-
_engine = GetEngine();
33-
_engine.Services = services;
34-
_engine.Services.Add(new ServiceDescriptor(typeof(IServiceProvider), this));
33+
Engine = GetEngine();
34+
Engine.Services = services;
35+
Engine.Services.Add(new ServiceDescriptor(typeof(IServiceProvider), this));
36+
Engine.Options = options;
3537

3638
if (options.ValidateOnBuild)
3739
{
@@ -41,7 +43,7 @@ internal ServiceProvider(IServiceCollection services, ServiceProviderOptions opt
4143
{
4244
try
4345
{
44-
_engine.ValidateService(descriptor);
46+
Engine.ValidateService(descriptor);
4547
}
4648
catch (Exception ex)
4749
{
@@ -65,7 +67,7 @@ public object GetService(Type serviceType)
6567
throw new ObjectDisposedException();
6668
}
6769

68-
return _engine.GetService(serviceType);
70+
return Engine.GetService(serviceType);
6971
}
7072

7173
/// <inheritdoc/>
@@ -76,7 +78,13 @@ public object[] GetService(Type[] serviceType)
7678
throw new ObjectDisposedException();
7779
}
7880

79-
return _engine.GetService(serviceType);
81+
return Engine.GetService(serviceType);
82+
}
83+
84+
/// <inheritdoc />
85+
public IServiceScope CreateScope()
86+
{
87+
return new ServiceProviderEngineScope(this);
8088
}
8189

8290
/// <inheritdoc/>
@@ -88,7 +96,7 @@ public void Dispose()
8896
}
8997

9098
_disposed = true;
91-
_engine.DisposeServices();
99+
Engine.DisposeServices();
92100
}
93101

94102
private ServiceProviderEngine GetEngine()

nanoFramework.DependencyInjection/DependencyInjection/ServiceProviderEngine.cs

Lines changed: 39 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,11 @@ private ServiceProviderEngine() { }
2323
/// </summary>
2424
internal IServiceCollection Services { get; set; }
2525

26+
/// <summary>
27+
/// ServiceProvider Options instance
28+
/// </summary>
29+
internal ServiceProviderOptions Options { get; set; }
30+
2631
/// <summary>
2732
/// Validate service by attempting to activate all dependent services.
2833
/// </summary>
@@ -49,9 +54,10 @@ internal void DisposeServices()
4954
/// Gets the last added service object of the specified type.
5055
/// </summary>
5156
/// <param name="serviceType">An object that specifies the type of service object to get.</param>
52-
internal object GetService(Type serviceType)
57+
/// <param name="scopeServices">Services collection from current scope.</param>
58+
internal object GetService(Type serviceType, IServiceCollection scopeServices = null)
5359
{
54-
var services = GetServiceObjects(serviceType);
60+
var services = GetServiceObjects(serviceType, scopeServices);
5561

5662
if (services.Length == 0)
5763
{
@@ -66,9 +72,10 @@ internal object GetService(Type serviceType)
6672
/// Gets the service objects of the specified type.
6773
/// </summary>
6874
/// <param name="serviceType">An object that specifies the type of service object to get.</param>
75+
/// <param name="scopeServices">Services collection from current scope.</param>
6976
/// <exception cref="ArgumentNullException"><paramref name="serviceType"/> can't be <see langword="null"/>.</exception>
7077
/// <exception cref="ArgumentException"><paramref name="serviceType"/> can't be empty.</exception>
71-
internal object[] GetService(Type[] serviceType)
78+
internal object[] GetService(Type[] serviceType, IServiceCollection scopeServices = null)
7279
{
7380
if (serviceType == null)
7481
{
@@ -83,7 +90,7 @@ internal object[] GetService(Type[] serviceType)
8390
// optimized for single item service type
8491
if (serviceType.Length == 1)
8592
{
86-
var services = GetServiceObjects(serviceType[0]);
93+
var services = GetServiceObjects(serviceType[0], scopeServices);
8794

8895
if (services.Length > 0)
8996
{
@@ -98,7 +105,7 @@ internal object[] GetService(Type[] serviceType)
98105

99106
foreach (Type type in serviceType)
100107
{
101-
var services = GetServiceObjects(type);
108+
var services = GetServiceObjects(type, scopeServices);
102109

103110
if (services.Length > 0)
104111
{
@@ -118,29 +125,41 @@ internal object[] GetService(Type[] serviceType)
118125
/// Gets the service objects of the specified type.
119126
/// </summary>
120127
/// <param name="serviceType">An object that specifies the type of service object to get.</param>
121-
private object[] GetServiceObjects(Type serviceType)
128+
/// <param name="scopeServices">Services collection from current scope.</param>
129+
private object[] GetServiceObjects(Type serviceType, IServiceCollection scopeServices)
122130
{
123131
ArrayList services = new ArrayList();
124132

133+
if (scopeServices != null)
134+
{
135+
foreach (ServiceDescriptor descriptor in scopeServices)
136+
{
137+
if (descriptor.ServiceType != serviceType) continue;
138+
139+
descriptor.ImplementationInstance ??= Resolve(descriptor.ImplementationType);
140+
services.Add(descriptor.ImplementationInstance);
141+
}
142+
}
143+
125144
foreach (ServiceDescriptor descriptor in Services)
126145
{
127-
if (descriptor.ServiceType == serviceType)
146+
if (descriptor.ServiceType != serviceType) continue;
147+
148+
switch (descriptor.Lifetime)
128149
{
129-
if (descriptor.Lifetime == ServiceLifetime.Singleton
130-
&& descriptor.ImplementationInstance != null)
131-
{
150+
case ServiceLifetime.Singleton:
151+
descriptor.ImplementationInstance ??= Resolve(descriptor.ImplementationType);
132152
services.Add(descriptor.ImplementationInstance);
133-
}
134-
else
135-
{
136-
var instance = Resolve(descriptor.ImplementationType);
137-
if (descriptor.Lifetime != ServiceLifetime.Transient)
138-
{
139-
descriptor.ImplementationInstance = instance;
140-
}
153+
break;
141154

142-
services.Add(instance);
143-
}
155+
case ServiceLifetime.Transient:
156+
services.Add(Resolve(descriptor.ImplementationType));
157+
break;
158+
159+
case ServiceLifetime.Scoped:
160+
if (scopeServices == null && Options.ValidateScopes)
161+
throw new InvalidOperationException();
162+
break;
144163
}
145164
}
146165

0 commit comments

Comments
 (0)