diff --git a/Neleus.DependencyInjection.Extensions.Tests/Neleus.DependencyInjection.Extensions.Tests.csproj b/Neleus.DependencyInjection.Extensions.Tests/Neleus.DependencyInjection.Extensions.Tests.csproj index 285560c..27f70e5 100644 --- a/Neleus.DependencyInjection.Extensions.Tests/Neleus.DependencyInjection.Extensions.Tests.csproj +++ b/Neleus.DependencyInjection.Extensions.Tests/Neleus.DependencyInjection.Extensions.Tests.csproj @@ -1,7 +1,7 @@ - net5.0 + net7.0 false diff --git a/Neleus.DependencyInjection.Extensions.Tests/ServiceByNameFactoryLifetimeTests.cs b/Neleus.DependencyInjection.Extensions.Tests/ServiceByNameFactoryLifetimeTests.cs new file mode 100644 index 0000000..9b3d2e6 --- /dev/null +++ b/Neleus.DependencyInjection.Extensions.Tests/ServiceByNameFactoryLifetimeTests.cs @@ -0,0 +1,81 @@ +using System; +using System.Collections.Generic; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace Neleus.DependencyInjection.Extensions.Tests +{ + [TestClass] + public class ServiceByNameFactoryLifetimeTests + { + private ServiceCollection _container; + + [TestInitialize] + public void Init() + { + _container = new Microsoft.Extensions.DependencyInjection.ServiceCollection(); + } + + [TestMethod] + public void ServiceByNameFactorySingleton_GetByName_ResolvesSingleton() + { + _container.AddTransient>(); + + _container.AddByName>() + .AddSingleton>("1") + .AddSingleton>("2") + .BuildSingleton(); + + var serviceProvider = _container.BuildServiceProvider(); + + // direct resolution by calling GetByName + var listA1 = serviceProvider.GetService>>() + .GetByName("1"); + var listA2 = serviceProvider.GetService>>() + .GetByName("2"); + listA1.Add(1); + listA1.Add(2); + listA2.Add(3); + listA2.Add(4); + + var listB1 = serviceProvider.GetService>>() + .GetByName("1"); + var listB2 = serviceProvider.GetService>>() + .GetByName("2"); + + Assert.AreSame(listA1, listB1); + Assert.AreSame(listA2, listB2); + } + + [TestMethod] + public void ServiceByNameFactorySingleton_GetByName_ResolvesTransient() + { + _container.AddTransient>(); + + _container.AddByName>() + .Add>("1") + .Add>("2") + .Build(); + + var serviceProvider = _container.BuildServiceProvider(); + + // direct resolution by calling GetByName + var listA1 = serviceProvider.GetService>>() + .GetByName("1"); + var listA2 = serviceProvider.GetService>>() + .GetByName("2"); + listA1.Add(1); + listA1.Add(2); + listA2.Add(3); + listA2.Add(4); + + var listB1 = serviceProvider.GetService>>() + .GetByName("1"); + var listB2 = serviceProvider.GetService>>() + .GetByName("2"); + + Assert.AreNotEqual(listA1, listB1); + Assert.AreNotEqual(listA2, listB2); + } + } +} diff --git a/Neleus.DependencyInjection.Extensions/Lifetime.cs b/Neleus.DependencyInjection.Extensions/Lifetime.cs new file mode 100644 index 0000000..5b0a76e --- /dev/null +++ b/Neleus.DependencyInjection.Extensions/Lifetime.cs @@ -0,0 +1,13 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace Neleus.DependencyInjection.Extensions +{ + public enum Lifetime + { + Transient, + Scoped, + Singleton + } +} diff --git a/Neleus.DependencyInjection.Extensions/ServiceByNameFactory.cs b/Neleus.DependencyInjection.Extensions/ServiceByNameFactory.cs index 4a11d9c..562091b 100644 --- a/Neleus.DependencyInjection.Extensions/ServiceByNameFactory.cs +++ b/Neleus.DependencyInjection.Extensions/ServiceByNameFactory.cs @@ -6,9 +6,9 @@ namespace Neleus.DependencyInjection.Extensions internal class ServiceByNameFactory : IServiceByNameFactory { private readonly IServiceProvider _serviceProvider; - private readonly IDictionary _registrations; + private readonly IDictionary _registrations; - public ServiceByNameFactory(IServiceProvider serviceProvider, IDictionary registrations) + public ServiceByNameFactory(IServiceProvider serviceProvider, IDictionary registrations) { _serviceProvider = serviceProvider; _registrations = registrations; @@ -18,14 +18,36 @@ public TService GetByName(string name) { if (!_registrations.TryGetValue(name, out var implementationType)) throw new ArgumentException($"Service name '{name}' is not registered"); - return (TService)_serviceProvider.GetService(implementationType); + switch (implementationType.Lifetime) + { + case Lifetime.Scoped: + case Lifetime.Singleton: + if (implementationType.Instance == null) + { + implementationType.Instance = (TService)_serviceProvider.GetService(implementationType.Type); + } + return (TService)implementationType.Instance; + default: + return (TService)_serviceProvider.GetService(implementationType.Type); + } } public TService GetRequiredByName(string name) { if (!_registrations.TryGetValue(name, out var implementationType)) throw new ArgumentException($"Service name '{name}' is not registered"); - return (TService)_serviceProvider.GetRequiredService(implementationType); + switch (implementationType.Lifetime) + { + case Lifetime.Scoped: + case Lifetime.Singleton: + if (implementationType.Instance == null) + { + implementationType.Instance = (TService)_serviceProvider.GetRequiredService(implementationType.Type); + } + return (TService)implementationType.Instance; + default: + return (TService)_serviceProvider.GetRequiredService(implementationType.Type); + } } public ICollection GetNames() diff --git a/Neleus.DependencyInjection.Extensions/ServicesByNameBuilder.cs b/Neleus.DependencyInjection.Extensions/ServicesByNameBuilder.cs index edf878d..b85a77e 100644 --- a/Neleus.DependencyInjection.Extensions/ServicesByNameBuilder.cs +++ b/Neleus.DependencyInjection.Extensions/ServicesByNameBuilder.cs @@ -12,14 +12,14 @@ public class ServicesByNameBuilder { private readonly IServiceCollection _services; - private readonly IDictionary _registrations; + private readonly IDictionary _registrations; internal ServicesByNameBuilder(IServiceCollection services, NameBuilderSettings settings) { _services = services; _registrations = settings.CaseInsensitiveNames - ? new Dictionary(StringComparer.OrdinalIgnoreCase) - : new Dictionary(); + ? new Dictionary(StringComparer.OrdinalIgnoreCase) + : new Dictionary(); } /// @@ -29,7 +29,50 @@ internal ServicesByNameBuilder(IServiceCollection services, NameBuilderSettings /// public ServicesByNameBuilder Add(string name, Type implemtnationType) { - _registrations.Add(name, implemtnationType); + var typeLifetime = new TypeLifetime() { Type = implemtnationType, Lifetime = Lifetime.Transient }; + _registrations.Add(name, typeLifetime); + return this; + } + + /// + /// Generic version of + /// + public ServicesByNameBuilder AddScoped(string name) + where TImplementation : TService + { + return AddScoped(name, typeof(TImplementation)); + } + + /// + /// Maps name to corresponding implementation. + /// Note that this implementation has to be also registered in IoC container so + /// that is be able to resolve it. + /// + public ServicesByNameBuilder AddScoped(string name, Type implemtnationType) + { + var typeLifetime = new TypeLifetime() { Type = implemtnationType, Lifetime = Lifetime.Scoped }; + _registrations.Add(name, typeLifetime); + return this; + } + + /// + /// Generic version of + /// + public ServicesByNameBuilder AddSingleton(string name) + where TImplementation : TService + { + return AddSingleton(name, typeof(TImplementation)); + } + + /// + /// Maps name to corresponding implementation. + /// Note that this implementation has to be also registered in IoC container so + /// that is be able to resolve it. + /// + public ServicesByNameBuilder AddSingleton(string name, Type implemtnationType) + { + var typeLifetime = new TypeLifetime() { Type = implemtnationType, Lifetime = Lifetime.Singleton }; + _registrations.Add(name, typeLifetime); return this; } @@ -53,5 +96,17 @@ public void Build() //Registrations are shared across all instances _services.AddTransient>(s => new ServiceByNameFactory(s, registrations)); } + public void BuildScoped() + { + var registrations = _registrations; + //Registrations are shared across all instances + _services.AddScoped>(s => new ServiceByNameFactory(s, registrations)); + } + public void BuildSingleton() + { + var registrations = _registrations; + //Registrations are shared across all instances + _services.AddSingleton>(s => new ServiceByNameFactory(s, registrations)); + } } } \ No newline at end of file diff --git a/Neleus.DependencyInjection.Extensions/TypeLifetime.cs b/Neleus.DependencyInjection.Extensions/TypeLifetime.cs new file mode 100644 index 0000000..75549bb --- /dev/null +++ b/Neleus.DependencyInjection.Extensions/TypeLifetime.cs @@ -0,0 +1,13 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace Neleus.DependencyInjection.Extensions +{ + public class TypeLifetime + { + public Lifetime Lifetime { get; set; } = Lifetime.Transient; + public Type Type { get; set; } = typeof(Type); + public object Instance { get; set; } + } +} diff --git a/README.md b/README.md index 7152eef..e935bf8 100644 --- a/README.md +++ b/README.md @@ -34,4 +34,72 @@ In this case your client code is clean and has only dependency to `IService` and } In this case your client code depends on both IServiceByNameFactory and IService which can be heplful in case when client has its own logic of name resolution. +## Life Time Options + +In order to register instances using Singleton or Scope you can use the following examples: + + services.AddTransient(); + services.AddTransient(); + services.AddTransient(); + services.AddByName() + .AddSingleton("key1") + .AddSingleton("key2") + .AddSingleton("key3") + .BuildSingleton(); + +## Loop through keys + +There are times where you want to use the registered names as a way to handle do work against each key. + + _container.AddTransient>(); + _container.AddTransient>(); + + _container.AddByName>(new NameBuilderSettings() + { + CaseInsensitiveNames = true + }) + .Add>("list") + .Add>("hashSet") + .Build(); + + var serviceProvider = _container.BuildServiceProvider(); + + // allow to get each type + var group = serviceProvider.GetService>>(); + foreach (var name in group.GetNames()) + { + var instance = group.GetByName(name); + switch (name) { + case "list": + Assert.AreEqual(typeof(List), instance.GetType()); + break; + case "hashSet": + Assert.AreEqual(typeof(HashSet), instance.GetType()); + break; + } + } + +## GetRequiredByName + +Many times when sharing code between projects you want to throw an exception when a service isn't registered. To do this here is an example: + + _container.AddTransient>(); + + _container.AddByName>(new NameBuilderSettings() + { + CaseInsensitiveNames = true + }) + .Add("list", typeof(List)) + .Add("hashSet", typeof(HashSet)) + .Build(); + + + var serviceProvider = _container.BuildServiceProvider(); + + var serviceByNameFactory = serviceProvider.GetService>>(); + + Assert.ThrowsException(() => + { + var commandInstance = serviceByNameFactory.GetRequiredByName("list"); + });