From 1a11ae26e3bcb832c389ea9f49276948276034d0 Mon Sep 17 00:00:00 2001 From: Tim Maes Date: Thu, 21 Dec 2023 11:30:15 +0100 Subject: [PATCH 1/4] Implement KeyedScoped service and test it --- src/Bindicate.Tests/Bindicate.Tests.csproj | 13 ++++---- .../Scoped/AddScopedAttributeTests.cs | 30 +++++++++++++++++++ .../Scoped/AddKeyedScopeAttribute.cs | 17 +++++++++++ src/Bindicate/Bindicate.csproj | 6 ++-- .../Configuration/AutowiringBuilder.cs | 20 +++++++++++++ 5 files changed, 78 insertions(+), 8 deletions(-) create mode 100644 src/Bindicate/Attributes/Scoped/AddKeyedScopeAttribute.cs diff --git a/src/Bindicate.Tests/Bindicate.Tests.csproj b/src/Bindicate.Tests/Bindicate.Tests.csproj index 4522d53..76928b8 100644 --- a/src/Bindicate.Tests/Bindicate.Tests.csproj +++ b/src/Bindicate.Tests/Bindicate.Tests.csproj @@ -1,4 +1,4 @@ - + net8.0 @@ -8,10 +8,13 @@ - - - - + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + diff --git a/src/Bindicate.Tests/Scoped/AddScopedAttributeTests.cs b/src/Bindicate.Tests/Scoped/AddScopedAttributeTests.cs index df7d2ab..6e64b07 100644 --- a/src/Bindicate.Tests/Scoped/AddScopedAttributeTests.cs +++ b/src/Bindicate.Tests/Scoped/AddScopedAttributeTests.cs @@ -1,4 +1,5 @@ using Bindicate.Attributes; +using Bindicate.Attributes.Scoped; using Microsoft.Extensions.DependencyInjection; namespace Bindicate.Tests.ScopedTests; @@ -42,6 +43,27 @@ public void AddScoped_RegistersCorrectly() service.Should().NotBeNull().And.BeOfType(); serviceDescriptor.Lifetime.Should().Be(ServiceLifetime.Scoped); } + + + [Fact] + public void AddKeyedScoped_WithMultipleKeys_RegistersCorrectly() + { + // Arrange + var services = new ServiceCollection(); + services.AddAutowiringForAssembly(_testAssembly).ForKeyedServices(); + var serviceProvider = services.BuildServiceProvider(); + + // Act and Assert for the first key + using var scope1 = serviceProvider.CreateScope(); + var service1 = scope1.ServiceProvider.GetKeyedService("myKey"); + service1.Should().NotBeNull().And.BeOfType(); + + // Act and Assert for the second key + using var scope2 = serviceProvider.CreateScope(); + var service2 = scope2.ServiceProvider.GetKeyedService("mySecondKey"); + service2.Should().NotBeNull().And.BeOfType(); + } + } [AddScoped] @@ -51,3 +73,11 @@ public interface IScopedInterface { } [AddScoped(typeof(IScopedInterface))] public class ScopedWithInterface : IScopedInterface { } + +[AddKeyedScoped("myKey", typeof(IKeyedService))] +public class KeyedService : IKeyedService { } + +[AddKeyedScoped("mySecondKey", typeof(IKeyedService))] +public class SecondKeyedService : IKeyedService { } + +public interface IKeyedService { } diff --git a/src/Bindicate/Attributes/Scoped/AddKeyedScopeAttribute.cs b/src/Bindicate/Attributes/Scoped/AddKeyedScopeAttribute.cs new file mode 100644 index 0000000..98ec0d2 --- /dev/null +++ b/src/Bindicate/Attributes/Scoped/AddKeyedScopeAttribute.cs @@ -0,0 +1,17 @@ +namespace Bindicate.Attributes.Scoped; + +/// +/// Specifies that a service should be registered with the dependency injection container with a specified key. +/// +public class AddKeyedScopedAttribute : BaseServiceAttribute +{ + public object Key { get; private set; } + + public AddKeyedScopedAttribute(object key, Type serviceType = null) + : base(serviceType) + { + Key = key; + } + + public override Lifetime.Lifetime Lifetime => Bindicate.Lifetime.Lifetime.Scoped; +} \ No newline at end of file diff --git a/src/Bindicate/Bindicate.csproj b/src/Bindicate/Bindicate.csproj index 97f18e9..9f385ad 100644 --- a/src/Bindicate/Bindicate.csproj +++ b/src/Bindicate/Bindicate.csproj @@ -28,9 +28,9 @@ - - - + + + diff --git a/src/Bindicate/Configuration/AutowiringBuilder.cs b/src/Bindicate/Configuration/AutowiringBuilder.cs index c90c8cc..7fa2fee 100644 --- a/src/Bindicate/Configuration/AutowiringBuilder.cs +++ b/src/Bindicate/Configuration/AutowiringBuilder.cs @@ -1,5 +1,6 @@ using Bindicate.Attributes; using Bindicate.Attributes.Options; +using Bindicate.Attributes.Scoped; using Bindicate.Lifetime; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; @@ -107,6 +108,25 @@ public AutowiringBuilder WithOptions(IConfiguration configuration) return this; } + public AutowiringBuilder ForKeyedServices() + { + foreach (var type in _targetAssembly.GetTypes().Where(t => t.IsClass && !t.IsAbstract)) + { + var keyedAttributes = type.GetCustomAttributes(typeof(AddKeyedScopedAttribute), false) + .Cast(); + + foreach (var attr in keyedAttributes) + { + var serviceType = attr.ServiceType ?? type; + var key = attr.Key; + + _services.AddKeyedScoped(serviceType, key, type); + } + } + + return this; + } + /// /// Registers all configured services and options into the IServiceCollection. /// From 08559d5f8d3e06916745a6889ef322a7a947bbeb Mon Sep 17 00:00:00 2001 From: Tim Maes Date: Thu, 21 Dec 2023 11:36:45 +0100 Subject: [PATCH 2/4] Readme update --- README.md | 51 ++++++++++++++++++++++++++++++++++++++++++--------- 1 file changed, 42 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index 7707f74..8b30c09 100644 --- a/README.md +++ b/README.md @@ -15,15 +15,15 @@ ### Supported types
-| Type | Supported| -|----------------|----------| -|AddTransient |✔️ | -|TryAddTransient |✔️ | -|AddScoped |✔️ | -|TryAddScoped |✔️ | -|AddSingleton |✔️ | -|TryAddSingleton |✔️ | -|TryAddEnumerable|❌ | +| **Type** | **Available** | **Keyed (.NET 8)** | +|--------------------|----------|------------------------------| +|AddTransient |✔️ |❌ | +|TryAddTransient |✔️ |❌ | +|AddScoped |✔️ |✔️ | +|TryAddScoped |✔️ |❌ | +|AddSingleton |✔️ |❌ | +|TryAddSingleton |✔️ |❌ | +|TryAddEnumerable |❌ |❌ |
## Installation 📦 @@ -137,6 +137,39 @@ public interface IMyTaskRunner } ``` +** When using keyed services:** + +Decorate your class with the attribute and provide the key + +```csharp +[AddScoped("myKey")] +public class KeyedService +{ + public void Run() + { + // ... + } +} + +[AddScoped("key", typeof(IKeyedService))] +public class KeyedService : IKeyedService +{ + public void Run() + { + // ... + } +} + +[AddScoped("anotherKey")] +public class AnotherKeyedService : IKeyedService +{ + public void Run() + { + // ... + } +} +``` + ### Options Registration Decorate your class containing the options with `[RegisterOptions]` and specify the corresponding section in `appsettings.json`. From 010108e50f6fdd6c00e8da1e3fb7cb7e5fbdd2b2 Mon Sep 17 00:00:00 2001 From: Tim Maes Date: Thu, 21 Dec 2023 11:39:53 +0100 Subject: [PATCH 3/4] Fix readme typo --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 8b30c09..838c22e 100644 --- a/README.md +++ b/README.md @@ -137,7 +137,7 @@ public interface IMyTaskRunner } ``` -** When using keyed services:** +**When using keyed services:** Decorate your class with the attribute and provide the key From 2c25cf93dd71885172d0913ef7f2f802754e11a1 Mon Sep 17 00:00:00 2001 From: Tim Maes Date: Tue, 2 Jan 2024 10:31:03 +0100 Subject: [PATCH 4/4] Fully implement keyed --- README.md | 4 +-- .../Scoped/AddScopedAttributeTests.cs | 25 ++++++++++++++++--- .../Singleton/AddSingletonAttributeTests.cs | 9 +++---- .../Transient/AddTransientAttributeTests.cs | 9 +++---- .../Attributes/BaseKeyedServiceAttribute.cs | 23 +++++++++++++++++ .../Scoped/AddKeyedScopeAttribute.cs | 10 +++----- .../Singleton/AddKeyedSingletonAttribute.cs | 14 +++++++++++ .../Transient/AddKeyedTransientAttribute.cs | 14 +++++++++++ src/Bindicate/Bindicate.csproj | 2 +- .../Configuration/AutowiringBuilder.cs | 23 ++++++++++++++--- 10 files changed, 107 insertions(+), 26 deletions(-) create mode 100644 src/Bindicate/Attributes/BaseKeyedServiceAttribute.cs create mode 100644 src/Bindicate/Attributes/Singleton/AddKeyedSingletonAttribute.cs create mode 100644 src/Bindicate/Attributes/Transient/AddKeyedTransientAttribute.cs diff --git a/README.md b/README.md index 838c22e..bfcd05f 100644 --- a/README.md +++ b/README.md @@ -17,11 +17,11 @@ | **Type** | **Available** | **Keyed (.NET 8)** | |--------------------|----------|------------------------------| -|AddTransient |✔️ |❌ | +|AddTransient |✔️ |✔️ | |TryAddTransient |✔️ |❌ | |AddScoped |✔️ |✔️ | |TryAddScoped |✔️ |❌ | -|AddSingleton |✔️ |❌ | +|AddSingleton |✔️ |✔️ | |TryAddSingleton |✔️ |❌ | |TryAddEnumerable |❌ |❌ | diff --git a/src/Bindicate.Tests/Scoped/AddScopedAttributeTests.cs b/src/Bindicate.Tests/Scoped/AddScopedAttributeTests.cs index 6e64b07..83f0ce8 100644 --- a/src/Bindicate.Tests/Scoped/AddScopedAttributeTests.cs +++ b/src/Bindicate.Tests/Scoped/AddScopedAttributeTests.cs @@ -8,12 +8,28 @@ public class AddScopedAttributeTests { private readonly Assembly _testAssembly = typeof(AddScopedAttributeTests).Assembly; + [Fact] + public void AddScoped_WithoutAttribute_NotRegistered() + { + // Arrange + var services = new ServiceCollection(); + services.AddAutowiringForAssembly(_testAssembly).Register(); + var serviceProvider = services.BuildServiceProvider(); + + // Act + using var scope = serviceProvider.CreateScope(); + var service = scope.ServiceProvider.GetService(); + + // Assert + service.Should().BeNull(); + } + [Fact] public void AddScoped_WithInterface_RegistersCorrectly() { //Arrange var services = new ServiceCollection(); - services.AddAutowiringForAssembly(_testAssembly); + services.AddAutowiringForAssembly(_testAssembly).Register(); var serviceProvider = services.BuildServiceProvider(); //Act @@ -31,7 +47,7 @@ public void AddScoped_RegistersCorrectly() { // Arrange var services = new ServiceCollection(); - services.AddAutowiringForAssembly(_testAssembly); + services.AddAutowiringForAssembly(_testAssembly).Register(); var serviceProvider = services.BuildServiceProvider(); // Act @@ -50,7 +66,7 @@ public void AddKeyedScoped_WithMultipleKeys_RegistersCorrectly() { // Arrange var services = new ServiceCollection(); - services.AddAutowiringForAssembly(_testAssembly).ForKeyedServices(); + services.AddAutowiringForAssembly(_testAssembly).ForKeyedServices().Register(); var serviceProvider = services.BuildServiceProvider(); // Act and Assert for the first key @@ -63,9 +79,10 @@ public void AddKeyedScoped_WithMultipleKeys_RegistersCorrectly() var service2 = scope2.ServiceProvider.GetKeyedService("mySecondKey"); service2.Should().NotBeNull().And.BeOfType(); } - } +public class NonRegisteredClass { } + [AddScoped] public class SimpleScopedClass { } diff --git a/src/Bindicate.Tests/Singleton/AddSingletonAttributeTests.cs b/src/Bindicate.Tests/Singleton/AddSingletonAttributeTests.cs index 9efb2fe..afa9960 100644 --- a/src/Bindicate.Tests/Singleton/AddSingletonAttributeTests.cs +++ b/src/Bindicate.Tests/Singleton/AddSingletonAttributeTests.cs @@ -1,18 +1,17 @@ using Bindicate.Attributes; -using Bindicate.Tests.ScopedTests; using Microsoft.Extensions.DependencyInjection; namespace Bindicate.Tests.Singleton; public class AddSingletonAttributeTests { - private readonly Assembly _testAssembly = typeof(AddScopedAttributeTests).Assembly; + private readonly Assembly _testAssembly = typeof(AddSingletonAttributeTests).Assembly; [Fact] public void AddSingleton_AlwaysReturnsSame() { var services = new ServiceCollection(); - services.AddAutowiringForAssembly(_testAssembly); + services.AddAutowiringForAssembly(_testAssembly).Register(); var serviceProvider = services.BuildServiceProvider(); var instance1 = serviceProvider.GetService(); @@ -26,7 +25,7 @@ public void AddSingleton_WithInterface_RegistersCorrectly() { // Arrange var services = new ServiceCollection(); - services.AddAutowiringForAssembly(_testAssembly); + services.AddAutowiringForAssembly(_testAssembly).Register(); var serviceProvider = services.BuildServiceProvider(); // Act @@ -44,7 +43,7 @@ public void AddSingleton_RegistersCorrectly() { // Arrange var services = new ServiceCollection(); - services.AddAutowiringForAssembly(_testAssembly); + services.AddAutowiringForAssembly(_testAssembly).Register(); var serviceProvider = services.BuildServiceProvider(); // Act diff --git a/src/Bindicate.Tests/Transient/AddTransientAttributeTests.cs b/src/Bindicate.Tests/Transient/AddTransientAttributeTests.cs index b024f0b..17d57fb 100644 --- a/src/Bindicate.Tests/Transient/AddTransientAttributeTests.cs +++ b/src/Bindicate.Tests/Transient/AddTransientAttributeTests.cs @@ -1,18 +1,17 @@ using Bindicate.Attributes; -using Bindicate.Tests.ScopedTests; using Microsoft.Extensions.DependencyInjection; namespace Bindicate.Tests.Transient; public class AddTransientAttributeTests { - private readonly Assembly _testAssembly = typeof(AddScopedAttributeTests).Assembly; + private readonly Assembly _testAssembly = typeof(AddTransientAttributeTests).Assembly; [Fact] public void AddTransient_AlwaysReturnsNewInstance() { var services = new ServiceCollection(); - services.AddAutowiringForAssembly(_testAssembly); + services.AddAutowiringForAssembly(_testAssembly).Register(); var serviceProvider = services.BuildServiceProvider(); var instance1 = serviceProvider.GetService(); @@ -26,7 +25,7 @@ public void AddTransient_WithInterface_RegistersCorrectly() { // Arrange var services = new ServiceCollection(); - services.AddAutowiringForAssembly(_testAssembly); + services.AddAutowiringForAssembly(_testAssembly).Register(); var serviceProvider = services.BuildServiceProvider(); // Act @@ -44,7 +43,7 @@ public void AddSingleton_RegistersCorrectly() { // Arrange var services = new ServiceCollection(); - services.AddAutowiringForAssembly(_testAssembly); + services.AddAutowiringForAssembly(_testAssembly).Register(); var serviceProvider = services.BuildServiceProvider(); // Act diff --git a/src/Bindicate/Attributes/BaseKeyedServiceAttribute.cs b/src/Bindicate/Attributes/BaseKeyedServiceAttribute.cs new file mode 100644 index 0000000..c36bd9b --- /dev/null +++ b/src/Bindicate/Attributes/BaseKeyedServiceAttribute.cs @@ -0,0 +1,23 @@ +namespace Bindicate.Attributes; + +[AttributeUsage(AttributeTargets.Class, AllowMultiple = false, Inherited = true)] +public abstract class BaseKeyedServiceAttribute : Attribute +{ + public Type ServiceType { get; protected set; } + public object Key { get; protected set; } // Added key property + + public abstract Lifetime.Lifetime Lifetime { get; } + + // Constructor for class-only registration without a key + protected BaseKeyedServiceAttribute() + { + ServiceType = null; + } + + // Constructor for explicit interface registration with a key + protected BaseKeyedServiceAttribute(object key, Type serviceType = null) + { + Key = key; + ServiceType = serviceType; + } +} \ No newline at end of file diff --git a/src/Bindicate/Attributes/Scoped/AddKeyedScopeAttribute.cs b/src/Bindicate/Attributes/Scoped/AddKeyedScopeAttribute.cs index 98ec0d2..81987c8 100644 --- a/src/Bindicate/Attributes/Scoped/AddKeyedScopeAttribute.cs +++ b/src/Bindicate/Attributes/Scoped/AddKeyedScopeAttribute.cs @@ -3,15 +3,13 @@ /// /// Specifies that a service should be registered with the dependency injection container with a specified key. /// -public class AddKeyedScopedAttribute : BaseServiceAttribute +public class AddKeyedScopedAttribute : BaseKeyedServiceAttribute { - public object Key { get; private set; } - - public AddKeyedScopedAttribute(object key, Type serviceType = null) - : base(serviceType) + public AddKeyedScopedAttribute(object key, Type serviceType) + : base(key, serviceType) { - Key = key; } public override Lifetime.Lifetime Lifetime => Bindicate.Lifetime.Lifetime.Scoped; + } \ No newline at end of file diff --git a/src/Bindicate/Attributes/Singleton/AddKeyedSingletonAttribute.cs b/src/Bindicate/Attributes/Singleton/AddKeyedSingletonAttribute.cs new file mode 100644 index 0000000..21796f1 --- /dev/null +++ b/src/Bindicate/Attributes/Singleton/AddKeyedSingletonAttribute.cs @@ -0,0 +1,14 @@ +namespace Bindicate.Attributes.Singleton; + +/// +/// Specifies that a service should be registered with the dependency injection container with a specified key. +/// +public class AddKeyedSingletonAttribute : BaseKeyedServiceAttribute +{ + public AddKeyedSingletonAttribute(object key, Type serviceType) + : base(key, serviceType) + { + } + + public override Lifetime.Lifetime Lifetime => Bindicate.Lifetime.Lifetime.Singleton; +} \ No newline at end of file diff --git a/src/Bindicate/Attributes/Transient/AddKeyedTransientAttribute.cs b/src/Bindicate/Attributes/Transient/AddKeyedTransientAttribute.cs new file mode 100644 index 0000000..fb73a2f --- /dev/null +++ b/src/Bindicate/Attributes/Transient/AddKeyedTransientAttribute.cs @@ -0,0 +1,14 @@ +namespace Bindicate.Attributes.Transient; + +/// +/// Specifies that a service should be registered with the dependency injection container with a specified key. +/// +public class AddKeyedTransientAttribute : BaseKeyedServiceAttribute +{ + public AddKeyedTransientAttribute(object key, Type serviceType) + : base(key, serviceType) + { + } + + public override Lifetime.Lifetime Lifetime => Bindicate.Lifetime.Lifetime.Transient; +} \ No newline at end of file diff --git a/src/Bindicate/Bindicate.csproj b/src/Bindicate/Bindicate.csproj index 9f385ad..1463f87 100644 --- a/src/Bindicate/Bindicate.csproj +++ b/src/Bindicate/Bindicate.csproj @@ -13,7 +13,7 @@ di, ioc, service, collection, extensions, attribute Add support for IOptions LICENSE.txt - 1.3 + 1.5 diff --git a/src/Bindicate/Configuration/AutowiringBuilder.cs b/src/Bindicate/Configuration/AutowiringBuilder.cs index 7fa2fee..02db74e 100644 --- a/src/Bindicate/Configuration/AutowiringBuilder.cs +++ b/src/Bindicate/Configuration/AutowiringBuilder.cs @@ -1,6 +1,8 @@ using Bindicate.Attributes; using Bindicate.Attributes.Options; using Bindicate.Attributes.Scoped; +using Bindicate.Attributes.Singleton; +using Bindicate.Attributes.Transient; using Bindicate.Lifetime; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; @@ -108,19 +110,25 @@ public AutowiringBuilder WithOptions(IConfiguration configuration) return this; } + /// + /// Scans the assembly to automatically wire up keyed services based on the attributes. + /// + /// A reference to this instance after the operation has completed. public AutowiringBuilder ForKeyedServices() { foreach (var type in _targetAssembly.GetTypes().Where(t => t.IsClass && !t.IsAbstract)) { - var keyedAttributes = type.GetCustomAttributes(typeof(AddKeyedScopedAttribute), false) - .Cast(); + var keyedAttributes = type.GetCustomAttributes(typeof(BaseKeyedServiceAttribute), false) + .Cast(); foreach (var attr in keyedAttributes) { var serviceType = attr.ServiceType ?? type; var key = attr.Key; - _services.AddKeyedScoped(serviceType, key, type); + var registrationMethod = GetKeyedRegistrationMethod(_services, attr); + + registrationMethod(serviceType, key, type); } } @@ -135,6 +143,15 @@ public IServiceCollection Register() { return _services; } + private static Action GetKeyedRegistrationMethod(IServiceCollection services, BaseKeyedServiceAttribute attr) + => attr switch + { + AddKeyedScopedAttribute _ => (s, k, t) => services.AddKeyedScoped(s, k, t), + AddKeyedSingletonAttribute _ => (s, k, t) => services.AddKeyedSingleton(s, k, t), + AddKeyedTransientAttribute _ => (s, k, t) => services.AddKeyedTransient(s, k, t), + _ => throw new ArgumentOutOfRangeException(nameof(attr), "Unsupported attribute type.") + }; + private static Action GetRegistrationMethod(IServiceCollection services, Lifetime.Lifetime lifetime) => lifetime switch