From b70853b22abe549b6dd7b3dc778a2b19adb0b705 Mon Sep 17 00:00:00 2001 From: Alexandru Macocian Date: Mon, 25 Oct 2021 14:26:05 +0200 Subject: [PATCH] Introduced ReadOnly for scoped managers. Ability to override IsReadOnly when creating scope. Introduced method to deal with singletons when scoped manager implements them instead of master manager. --- Slim.Tests/ServiceManagerTests.cs | 50 ++++++++++++++++++++++- Slim/IServiceManager.cs | 16 ++++++-- Slim/ServiceManager.cs | 68 +++++++++++++++++++++++++++++-- Slim/Slim.csproj | 6 +-- 4 files changed, 128 insertions(+), 12 deletions(-) diff --git a/Slim.Tests/ServiceManagerTests.cs b/Slim.Tests/ServiceManagerTests.cs index a438a20..d46a7a4 100644 --- a/Slim.Tests/ServiceManagerTests.cs +++ b/Slim.Tests/ServiceManagerTests.cs @@ -534,7 +534,7 @@ namespace Slim.Tests [TestMethod] public void ScopedManagerShouldHaveItsOwnLists() { - var di = new ServiceManager(); + var di = new ServiceManager() { AllowScopedManagerModifications = true }; di.RegisterSingleton(); var scopedDi = di.CreateScope(); @@ -668,5 +668,53 @@ namespace Slim.Tests isRegistered1.Should().BeFalse(); isRegistered2.Should().BeFalse(); } + + [TestMethod] + public void ScopedServiceManagerWithDisallowedModificationsIsReadOnly() + { + var di = new ServiceManager(); + di.RegisterSingleton(); + var scopedDi1 = di.CreateScope() as ServiceManager; + + scopedDi1.IsReadOnly.Should().BeTrue(); + } + + [TestMethod] + public void ScopedServiceManagerWithAllowedModificationsIsNotReadOnly() + { + var di = new ServiceManager() { AllowScopedManagerModifications = true }; + di.RegisterSingleton(); + var scopedDi1 = di.CreateScope() as ServiceManager; + + scopedDi1.IsReadOnly.Should().BeFalse(); + } + + [TestMethod] + public void ModifyingScopedManagerThrowsInvalidOperationException() + { + var di = new ServiceManager(); + di.RegisterSingleton(); + var scopedDi1 = di.CreateScope() as ServiceManager; + + var action = new Action(() => scopedDi1.RegisterSingleton()); + + action.Should().Throw(); + } + + [TestMethod] + public void ModifyingScopedManagerStillResolvesSingleton() + { + var di = new ServiceManager() { AllowScopedManagerModifications = true }; + di.RegisterSingleton(); + var scopedDi1 = di.CreateScope() as ServiceManager; + var scopedDi2 = di.CreateScope() as ServiceManager; + scopedDi1.RegisterSingleton(); + + var dependentService1 = scopedDi1.GetService(); + var dependentService2 = scopedDi2.GetService(); + + dependentService1.Should().Be(dependentService2); + dependentService1.IndependentService.Should().Be(dependentService2.IndependentService); + } } } diff --git a/Slim/IServiceManager.cs b/Slim/IServiceManager.cs index 9264f02..74fdf52 100644 --- a/Slim/IServiceManager.cs +++ b/Slim/IServiceManager.cs @@ -9,28 +9,36 @@ namespace Slim /// public interface IServiceManager : IServiceProvider, IServiceProducer { + /// + /// Allow modifications to scoped created from this . + /// + bool AllowScopedManagerModifications { get; set; } + /// + /// Returns true if is readonly. + /// + bool IsReadOnly { get; } /// /// Returns true if there exists a registration for . /// /// Type of registered service. /// True if service is registered. Otherwise returns false. - public bool IsRegistered(Type tInterface); + bool IsRegistered(Type tInterface); /// /// Returns true if there exists a registration for . /// /// Type of registered service. /// True if service is registered. Otherwise returns false. - public bool IsRegistered(); + bool IsRegistered(); /// /// Returns all services that can be cast to provided type. /// /// Type of the returned services. - public IEnumerable GetServicesOfType(); + IEnumerable GetServicesOfType(); /// /// Returns all services that can be cast to provided type. /// /// Type of the returned services. - public IEnumerable GetServicesOfType(Type type); + IEnumerable GetServicesOfType(Type type); /// /// Register the current service manager as a valid dependency. /// diff --git a/Slim/ServiceManager.cs b/Slim/ServiceManager.cs index 61d2438..134f489 100644 --- a/Slim/ServiceManager.cs +++ b/Slim/ServiceManager.cs @@ -21,6 +21,15 @@ namespace Slim private Dictionary ExceptionHandlers { get; } = new Dictionary(); private List Resolvers { get; } = new List(); + /// + /// Allow modifications to scoped created from this . + /// + public bool AllowScopedManagerModifications { get; set; } + /// + /// Returns true if is readonly. + /// + public bool IsReadOnly { get; } + /// /// Creates an instance of . /// @@ -33,7 +42,8 @@ namespace Slim Dictionary singletons, Dictionary factories, Dictionary exceptionHandlers, - List resolvers) + List resolvers, + bool isReadOnly) { this.InterfaceMapping = interfaceMapping; this.Instances = singletons; @@ -41,6 +51,7 @@ namespace Slim this.ExceptionHandlers = exceptionHandlers; this.Resolvers = resolvers; this.scoped = true; + this.IsReadOnly = isReadOnly; } /// @@ -471,6 +482,11 @@ namespace Slim /// public void Clear() { + if (this.IsReadOnly) + { + throw new InvalidOperationException($"Cannot clear {nameof(ServiceManager)}. {nameof(ServiceManager)} is readonly!"); + } + this.InterfaceMapping.Clear(); foreach(var singleton in this.Instances.Values.Where(s => s is IDisposable).Select(s => (IDisposable)s)) { @@ -499,6 +515,11 @@ namespace Slim /// that manually creates a dependency. public void RegisterResolver(IDependencyResolver dependencyResolver) { + if (this.IsReadOnly) + { + throw new InvalidOperationException($"Cannot register resolver. {nameof(ServiceManager)} is readonly!"); + } + this.Resolvers.Add(dependencyResolver); } /// @@ -522,6 +543,11 @@ namespace Slim } private void RegisterService(Type tClass, Lifetime lifetime, Type tInterface = null, Delegate serviceFactory = null, bool registerAllInterfaces = false) { + if (this.IsReadOnly) + { + throw new InvalidOperationException($"Cannot register service. {nameof(ServiceManager)} is readonly!"); + } + if (tInterface is not null) { this.Map(tInterface, tClass, lifetime); @@ -615,7 +641,7 @@ namespace Slim obj = this.TryImplementService(tInterface); } - if (obj is object) + if (obj is not null) { if (lifetime == Lifetime.Singleton || lifetime == Lifetime.Scoped) { @@ -647,6 +673,10 @@ namespace Slim return factory.DynamicInvoke(this); } + return this.FindAndCallConstructors(implementType); + } + private object FindAndCallConstructors(Type implementType) + { var constructors = implementType.GetConstructors(); foreach (var constructor in constructors) { @@ -740,11 +770,41 @@ namespace Slim { if (kvp.Value.Item2 is Lifetime.Singleton) { - factories[kvp.Key] = new Func((_) => this.PrepareAndGetService(kvp.Key)); + factories[kvp.Key] = new Func((scopedManager) => this.ResolveSingletonWithScopedManager(scopedManager as ServiceManager, kvp.Key, kvp.Value.Item1)); } } - return new ServiceManager(interfaceMapping, new Dictionary(), factories, exceptionHandlers, resolvers); + return new ServiceManager(interfaceMapping, new Dictionary(), factories, exceptionHandlers, resolvers, this.AllowScopedManagerModifications is false); + } + private object ResolveSingletonWithScopedManager(ServiceManager scopedManager, Type registerType, Type implementedType) + { + try + { + return this.PrepareAndGetService(registerType); + } + catch (DependencyInjectionException) + { + if (scopedManager.IsReadOnly) + { + throw; + } + + /* + * Most probably scoped manager has been modified and contains different definitions + * than the parent manager. + * As a last ditch effort, let the scoped provider try to implement the singleton. + * If successful, update the parent provider as well. + */ + var obj = scopedManager.FindAndCallConstructors(implementedType); + if (obj is not null) + { + scopedManager.Instances[implementedType] = obj; + this.Instances[implementedType] = obj; + return obj; + } + + throw; + } } private void TryAction(Action action) { diff --git a/Slim/Slim.csproj b/Slim/Slim.csproj index 7132fb1..f8a296d 100644 --- a/Slim/Slim.csproj +++ b/Slim/Slim.csproj @@ -8,10 +8,10 @@ Service lifetime management and dependency injection framework. - 1.5.5 - 1.5.5 + 1.6 + 1.6 https://github.com/AlexMacocian/Slim - 1.5.5 + 1.6 true true LICENSE