1
0
Форкнуть 0

Add support for "Scoped" lifetime (#31)

This commit is contained in:
Bartek U 2023-10-04 14:04:29 +02:00 коммит произвёл GitHub
Родитель d41299ea39
Коммит 03c1aa801f
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 4AEE18F83AFDEB23
14 изменённых файлов: 445 добавлений и 32 удалений

4
.editorconfig Normal file
Просмотреть файл

@ -0,0 +1,4 @@
[*.cs]
# Default severity for all analyzer diagnostics
dotnet_analyzer_diagnostic.severity = silent

Просмотреть файл

@ -27,6 +27,8 @@ A Dependency Injection (DI) Container provides functionality and automates many
This API mirrors as close as possible the official .NET
[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.
The .NET nanoFramework [Generic Host](https://github.com/nanoframework/nanoFramework.Hosting) provides convenience methods for creating dependency injection (DI) application containers with preconfigured defaults.
## Usage
### Service Collection
@ -91,6 +93,20 @@ var service = (RootObject)serviceProvider.GetService(typeof(RootObject));
service.ServiceObject.Three = "3";
```
Create a scoped Service Provider providing convient access to crate and distroy scoped object.
```csharp
var serviceProvider = new ServiceCollection()
.AddScoped(typeof(typeof(ServiceObject))
.BuildServiceProvider();
using (var scope = serviceProvider.CreateScope())
{
var service = scope.ServiceProvider.GetServices(typeof(ServiceObject));
service.ServiceObject.Three = "3";
}
```
## Activator Utilities
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()
.BuildServiceProvider(new ServiceProviderOptions() { ValidateOnBuild = true });
```
### Validate Scopes
A check verifying that scoped services never gets resolved from root provider. Validate on build is configured false by default.
```csharp
var serviceProvider = new ServiceCollection()
.AddSingleton(typeof(IServiceObject), typeof(ServiceObject))
.BuildServiceProvider(new ServiceProviderOptions() { ValidateScopes = true });
```
## Example Application Container
```csharp

Просмотреть файл

@ -0,0 +1,20 @@
//
// Copyright (c) .NET Foundation and Contributors
// See LICENSE file in the project root for full license information.
//
using System;
namespace nanoFramework.DependencyInjection
{
/// <summary>
/// Defines scope for <see cref="IServiceProvider"/>.
/// </summary>
public interface IServiceScope : IDisposable
{
/// <summary>
/// The <see cref="IServiceProvider"/> used to resolve dependencies from the scope.
/// </summary>
IServiceProvider ServiceProvider { get; }
}
}

Просмотреть файл

@ -9,7 +9,7 @@ using System.Collections;
namespace nanoFramework.DependencyInjection
{
/// <summary>
/// Extensions for <see cref="ServiceCollection"/>.
/// Extensions for <see cref="IServiceCollection"/>.
/// </summary>
public static class ServiceCollectionServiceExtensions
{
@ -123,6 +123,49 @@ namespace nanoFramework.DependencyInjection
return services.AddTransient(serviceType, serviceType);
}
/// <summary>
/// Adds a scoped service of the type specified in <paramref name="serviceType"/> with an
/// implementation of the type specified in <paramref name="implementationType"/> to the
/// specified <see cref="IServiceCollection"/>.
/// </summary>
/// <param name="services">The <see cref="IServiceCollection"/> to add the service to.</param>
/// <param name="serviceType">The type of the service to register.</param>
/// <param name="implementationType">The implementation type of the service.</param>
/// <returns>A reference to this instance after the operation has completed.</returns>
/// <seealso cref="ServiceLifetime.Scoped"/>
/// <exception cref="ArgumentNullException"><paramref name="services"/> can't be <see langword="null"/>.</exception>
public static IServiceCollection AddScoped(this IServiceCollection services, Type serviceType, Type implementationType)
{
if (services == null)
{
throw new ArgumentNullException();
}
var descriptor = new ServiceDescriptor(serviceType, implementationType, ServiceLifetime.Scoped);
services.Add(descriptor);
return services;
}
/// <summary>
/// Adds a scoped service of the type specified in <paramref name="serviceType"/> to the
/// specified <see cref="IServiceCollection"/>.
/// </summary>
/// <param name="services">The <see cref="IServiceCollection"/> to add the service to.</param>
/// <param name="serviceType">The type of the service to register and the implementation to use.</param>
/// <returns>A reference to this instance after the operation has completed.</returns>
/// <seealso cref="ServiceLifetime.Scoped"/>
/// <exception cref="ArgumentNullException"><paramref name="services"/> can't be <see langword="null"/>.</exception>
public static IServiceCollection AddScoped(this IServiceCollection services, Type serviceType)
{
if (services == null)
{
throw new ArgumentNullException();
}
return services.AddScoped(serviceType, serviceType);
}
/// <summary>
/// Adds the specified <paramref name="descriptor"/> to the <paramref name="collection"/> if the
/// service type hasn't already been registered.

Просмотреть файл

@ -18,6 +18,11 @@ namespace nanoFramework.DependencyInjection
/// <summary>
/// Specifies that a new instance of the service will be created every time it is requested.
/// </summary>
Transient
Transient,
/// <summary>
/// Specifies that a single instance of the service will be created within a scope.
/// </summary>
Scoped
}
}

Просмотреть файл

@ -15,7 +15,8 @@ namespace nanoFramework.DependencyInjection
public sealed class ServiceProvider : IServiceProvider, IDisposable
{
private bool _disposed;
internal ServiceProviderEngine _engine;
internal ServiceProviderEngine Engine { get; }
internal ServiceProvider(IServiceCollection services, ServiceProviderOptions options)
{
@ -29,9 +30,10 @@ namespace nanoFramework.DependencyInjection
throw new ArgumentNullException();
}
_engine = GetEngine();
_engine.Services = services;
_engine.Services.Add(new ServiceDescriptor(typeof(IServiceProvider), this));
Engine = GetEngine();
Engine.Services = services;
Engine.Services.Add(new ServiceDescriptor(typeof(IServiceProvider), this));
Engine.Options = options;
if (options.ValidateOnBuild)
{
@ -41,7 +43,7 @@ namespace nanoFramework.DependencyInjection
{
try
{
_engine.ValidateService(descriptor);
Engine.ValidateService(descriptor);
}
catch (Exception ex)
{
@ -65,7 +67,7 @@ namespace nanoFramework.DependencyInjection
throw new ObjectDisposedException();
}
return _engine.GetService(serviceType);
return Engine.GetService(serviceType);
}
/// <inheritdoc/>
@ -76,7 +78,13 @@ namespace nanoFramework.DependencyInjection
throw new ObjectDisposedException();
}
return _engine.GetService(serviceType);
return Engine.GetService(serviceType);
}
/// <inheritdoc />
public IServiceScope CreateScope()
{
return new ServiceProviderEngineScope(this);
}
/// <inheritdoc/>
@ -88,7 +96,7 @@ namespace nanoFramework.DependencyInjection
}
_disposed = true;
_engine.DisposeServices();
Engine.DisposeServices();
}
private ServiceProviderEngine GetEngine()

Просмотреть файл

@ -23,6 +23,11 @@ namespace nanoFramework.DependencyInjection
/// </summary>
internal IServiceCollection Services { get; set; }
/// <summary>
/// ServiceProvider Options instance
/// </summary>
internal ServiceProviderOptions Options { get; set; }
/// <summary>
/// Validate service by attempting to activate all dependent services.
/// </summary>
@ -49,9 +54,10 @@ namespace nanoFramework.DependencyInjection
/// Gets the last added service object of the specified type.
/// </summary>
/// <param name="serviceType">An object that specifies the type of service object to get.</param>
internal object GetService(Type serviceType)
/// <param name="scopeServices">Services collection from current scope.</param>
internal object GetService(Type serviceType, IServiceCollection scopeServices = null)
{
var services = GetServiceObjects(serviceType);
var services = GetServiceObjects(serviceType, scopeServices);
if (services.Length == 0)
{
@ -66,9 +72,10 @@ namespace nanoFramework.DependencyInjection
/// Gets the service objects of the specified type.
/// </summary>
/// <param name="serviceType">An object that specifies the type of service object to get.</param>
/// <param name="scopeServices">Services collection from current scope.</param>
/// <exception cref="ArgumentNullException"><paramref name="serviceType"/> can't be <see langword="null"/>.</exception>
/// <exception cref="ArgumentException"><paramref name="serviceType"/> can't be empty.</exception>
internal object[] GetService(Type[] serviceType)
internal object[] GetService(Type[] serviceType, IServiceCollection scopeServices = null)
{
if (serviceType == null)
{
@ -83,7 +90,7 @@ namespace nanoFramework.DependencyInjection
// optimized for single item service type
if (serviceType.Length == 1)
{
var services = GetServiceObjects(serviceType[0]);
var services = GetServiceObjects(serviceType[0], scopeServices);
if (services.Length > 0)
{
@ -98,7 +105,7 @@ namespace nanoFramework.DependencyInjection
foreach (Type type in serviceType)
{
var services = GetServiceObjects(type);
var services = GetServiceObjects(type, scopeServices);
if (services.Length > 0)
{
@ -118,29 +125,41 @@ namespace nanoFramework.DependencyInjection
/// Gets the service objects of the specified type.
/// </summary>
/// <param name="serviceType">An object that specifies the type of service object to get.</param>
private object[] GetServiceObjects(Type serviceType)
/// <param name="scopeServices">Services collection from current scope.</param>
private object[] GetServiceObjects(Type serviceType, IServiceCollection scopeServices)
{
ArrayList services = new ArrayList();
if (scopeServices != null)
{
foreach (ServiceDescriptor descriptor in scopeServices)
{
if (descriptor.ServiceType != serviceType) continue;
descriptor.ImplementationInstance ??= Resolve(descriptor.ImplementationType);
services.Add(descriptor.ImplementationInstance);
}
}
foreach (ServiceDescriptor descriptor in Services)
{
if (descriptor.ServiceType == serviceType)
if (descriptor.ServiceType != serviceType) continue;
switch (descriptor.Lifetime)
{
if (descriptor.Lifetime == ServiceLifetime.Singleton
&& descriptor.ImplementationInstance != null)
{
case ServiceLifetime.Singleton:
descriptor.ImplementationInstance ??= Resolve(descriptor.ImplementationType);
services.Add(descriptor.ImplementationInstance);
}
else
{
var instance = Resolve(descriptor.ImplementationType);
if (descriptor.Lifetime != ServiceLifetime.Transient)
{
descriptor.ImplementationInstance = instance;
}
break;
services.Add(instance);
}
case ServiceLifetime.Transient:
services.Add(Resolve(descriptor.ImplementationType));
break;
case ServiceLifetime.Scoped:
if (scopeServices == null && Options.ValidateScopes)
throw new InvalidOperationException();
break;
}
}

Просмотреть файл

@ -0,0 +1,107 @@
//
// Copyright (c) .NET Foundation and Contributors
// See LICENSE file in the project root for full license information.
//
using System;
namespace nanoFramework.DependencyInjection
{
/// <summary>
/// The <see cref="System.IDisposable.Dispose"/> method ends the scope lifetime. Once Dispose
/// is called, any scoped services that have been resolved from
/// <see cref="IServiceProvider"/> will be disposed.
/// </summary>
internal sealed class ServiceProviderEngineScope : IServiceScope, IServiceProvider
{
private bool _disposed;
private readonly IServiceCollection _scopeServices = new ServiceCollection();
/// <summary>
/// The root service provider used to resolve dependencies from the scope.
/// </summary>
internal ServiceProvider RootProvider { get; }
/// <summary>
/// Creates instance of <see cref="ServiceProviderEngineScope"/>.
/// </summary>
/// <param name="provider">The root service provider used to resolve dependencies from the scope.</param>
internal ServiceProviderEngineScope(ServiceProvider provider)
{
RootProvider = provider;
CloneScopeServices();
}
/// <summary>
/// The <see cref="IServiceProvider"/> resolved from the scope.
/// </summary>
public IServiceProvider ServiceProvider => this;
/// <inheritdoc/>
public object GetService(Type serviceType)
{
if (_disposed)
{
throw new ObjectDisposedException();
}
return RootProvider.Engine.GetService(serviceType, _scopeServices);
}
/// <inheritdoc/>
public object[] GetService(Type[] serviceType)
{
if (_disposed)
{
throw new ObjectDisposedException();
}
return RootProvider.Engine.GetService(serviceType, _scopeServices);
}
/// <inheritdoc />
public IServiceScope CreateScope()
{
return new ServiceProviderEngineScope(RootProvider);
}
/// <inheritdoc/>
public void Dispose()
{
if (_disposed)
{
return;
}
_disposed = true;
DisposeServices();
}
private void CloneScopeServices()
{
foreach (ServiceDescriptor descriptor in RootProvider.Engine.Services)
{
if (descriptor.Lifetime == ServiceLifetime.Scoped)
{
_scopeServices.Add(new ServiceDescriptor(
descriptor.ServiceType, descriptor.ImplementationType, ServiceLifetime.Scoped));
}
}
}
private void DisposeServices()
{
for (int index = _scopeServices.Count - 1; index >= 0; index--)
{
if (_scopeServices[index].ImplementationInstance is IDisposable disposable)
{
#pragma warning disable S3966 //services must be disposed explicitly, otherwise ServiceRegisteredWithScopeIsDisposedWhenScopeIsDisposed test fails
disposable.Dispose();
#pragma warning restore S3966
}
}
}
}
}

Просмотреть файл

@ -15,9 +15,13 @@ namespace nanoFramework.DependencyInjection
// Avoid allocating objects in the default case
internal static readonly ServiceProviderOptions Default = new ServiceProviderOptions();
/// <summary>
/// <see langword="true"/> to perform check verifying that scoped services never gets resolved from root provider; otherwise <see langword="false"/>. Defaults to <see langword="false"/>.
/// </summary>
public bool ValidateScopes { get; set; }
/// <summary>
/// <see langword="true"/> to perform check verifying that all services can be created during BuildServiceProvider call; otherwise <see langword="false"/>. Defaults to <see langword="false"/>.
/// NOTE: this check doesn't verify open generics services.
/// </summary>
public bool ValidateOnBuild { get; set; }
}

Просмотреть файл

@ -20,6 +20,11 @@ namespace nanoFramework.DependencyInjection
/// <returns>An array of services of type <paramref name="serviceType"/>.</returns>
public static object[] GetServices(this IServiceProvider provider, Type serviceType)
{
if (provider == null)
{
throw new ArgumentNullException();
}
return provider.GetService(new Type[] { serviceType });
}
@ -74,5 +79,23 @@ namespace nanoFramework.DependencyInjection
return service;
}
/// <summary>
/// Creates a new <see cref="ServiceScope"/> that can be used to resolve scoped services.
/// </summary>
/// <param name="provider">The <see cref="IServiceProvider"/> to create the scope from.</param>
/// <returns>An <see cref="ServiceScope"/> that can be used to resolve scoped services.</returns>
/// <exception cref="ArgumentNullException"><paramref name="provider"/>can't be <see langword="null"/>.</exception>
public static IServiceScope CreateScope(this IServiceProvider provider)
{
if (provider == null)
{
throw new ArgumentNullException();
}
var service = (ServiceProvider)provider.GetRequiredService(typeof(IServiceProvider));
return new ServiceScope(service.CreateScope());
}
}
}

Просмотреть файл

@ -0,0 +1,43 @@
//
// Copyright (c) .NET Foundation and Contributors
// See LICENSE file in the project root for full license information.
//
using System;
using System.Diagnostics;
namespace nanoFramework.DependencyInjection
{
/// <summary>
/// An <see cref="IServiceScope" /> implementation that implements <see cref="IDisposable" />.
/// </summary>
[DebuggerDisplay("{ServiceProvider,nq}")]
public readonly struct ServiceScope : IServiceScope
{
private readonly IServiceScope _serviceScope;
/// <summary>
/// Initializes a new instance of the <see cref="ServiceScope"/> struct.
/// Wraps an instance of <see cref="IServiceScope" />.
/// </summary>
/// <param name="serviceScope">The <see cref="IServiceScope"/> instance to wrap.</param>
public ServiceScope(IServiceScope serviceScope)
{
if (serviceScope == null)
{
throw new ArgumentNullException();
}
_serviceScope = serviceScope;
}
/// <inheritdoc />
public IServiceProvider ServiceProvider => _serviceScope.ServiceProvider;
/// <inheritdoc />
public void Dispose()
{
_serviceScope.Dispose();
}
}
}

Просмотреть файл

@ -28,4 +28,4 @@ namespace System
/// </returns>
object[] GetService(Type[] serviceType);
}
}
}

Просмотреть файл

@ -32,7 +32,10 @@
<ItemGroup>
<Compile Include="DependencyInjection\ActivatorUtilities.cs" />
<Compile Include="DependencyInjection\IServiceCollection.cs" />
<Compile Include="DependencyInjection\ServiceScope.cs" />
<Compile Include="DependencyInjection\IServiceScope.cs" />
<Compile Include="DependencyInjection\ServiceProviderEngine.cs" />
<Compile Include="DependencyInjection\ServiceProviderEngineScope.cs" />
<Compile Include="DependencyInjection\TypeExtensions.cs" />
<Compile Include="Properties\AssemblyInfo.cs" />
<Compile Include="DependencyInjection\ServiceCollection.cs" />
@ -48,6 +51,7 @@
<Compile Include="System\IServiceProvider.cs" />
</ItemGroup>
<ItemGroup>
<None Include="..\.editorconfig" Link=".editorconfig" />
<None Include="packages.config" />
</ItemGroup>
<ItemGroup>

Просмотреть файл

@ -290,5 +290,110 @@ namespace nanoFramework.DependencyInjection.UnitTests
Assert.IsNull(service);
}
[TestMethod]
public void ServiceRegisteredWithScopedReturnsSameInstanceWithinScope()
{
var serviceProvider = new ServiceCollection()
.AddScoped(typeof(IFakeService), typeof(FakeService))
.BuildServiceProvider();
using var scope = serviceProvider.CreateScope();
var service1 = scope.ServiceProvider.GetService(typeof(IFakeService));
var service2 = scope.ServiceProvider.GetService(typeof(IFakeService));
Assert.AreSame(service1, service2);
}
[TestMethod]
public void ServiceRegisteredWithScopedReturnsDifferentInstanceOutsideScope()
{
var serviceProvider = new ServiceCollection()
.AddScoped(typeof(IFakeService), typeof(FakeService))
.BuildServiceProvider();
using var scope1 = serviceProvider.CreateScope();
using var scope2 = serviceProvider.CreateScope();
var service1 = scope1.ServiceProvider.GetService(typeof(IFakeService));
var service2 = scope2.ServiceProvider.GetService(typeof(IFakeService));
Assert.AreNotSame(service1, service2);
}
[TestMethod]
public void ServiceRegisteredWithScopedIsDisposedWhenScopeIsDisposed()
{
var serviceProvider = new ServiceCollection()
.AddScoped(typeof(IFakeService), typeof(FakeService))
.BuildServiceProvider();
FakeService service1, service2;
using (var scope1 = serviceProvider.CreateScope())
{
using (var scope2 = serviceProvider.CreateScope())
{
service1 = (FakeService)scope1.ServiceProvider.GetService(typeof(IFakeService));
service2 = (FakeService)scope2.ServiceProvider.GetService(typeof(IFakeService));
}
Assert.IsTrue(service2.Disposed);
Assert.IsFalse(service1.Disposed);
}
}
[TestMethod]
public void ServiceRegisteredWithScopedReturnsNullWhenNoScope()
{
var serviceProvider = new ServiceCollection()
.AddScoped(typeof(IFakeService), typeof(FakeService))
.BuildServiceProvider();
var service = serviceProvider.GetService(typeof(IFakeService));
Assert.IsNull(service);
}
[TestMethod]
public void ServiceRegisteredWithScopedThrowsExceptionWhenValidateScopesEnabledAndNoScope()
{
var serviceProvider = new ServiceCollection()
.AddScoped(typeof(IFakeService), typeof(FakeService))
.BuildServiceProvider(new ServiceProviderOptions{ValidateScopes = true});
Assert.ThrowsException(typeof(InvalidOperationException),
() => serviceProvider.GetService(typeof(IFakeService)));
}
[TestMethod]
public void NoDuplicateServiceRegisteredWithScoped()
{
var serviceProvider = new ServiceCollection()
.AddScoped(typeof(IFakeService), typeof(FakeService))
.BuildServiceProvider();
using var scope = serviceProvider.CreateScope();
var services = scope.ServiceProvider.GetServices(typeof(IFakeService));
Assert.AreEqual(1, services.Length);
}
[TestMethod]
public void IServiceProviderCreateScopeReturnsNewScope()
{
IServiceProvider serviceProvider = new ServiceCollection()
.BuildServiceProvider();
using var scope = serviceProvider.CreateScope();
Assert.IsNotNull(scope);
}
}
}