From fd87ca8e6426e60f5db459143ab3dc99c1d8df69 Mon Sep 17 00:00:00 2001 From: Tony Troeff Date: Sat, 1 Mar 2025 00:05:29 +0200 Subject: [PATCH 1/6] Added sample logic for registering open generic services within the standard DI container. --- .../ServiceConfigurationAttribute.cs | 5 +++ .../ServiceRegistrar.cs | 32 ++++++++++++++++--- 2 files changed, 33 insertions(+), 4 deletions(-) diff --git a/TryAtSoftware.Extensions.DependencyInjection.Standard/Attributes/ServiceConfigurationAttribute.cs b/TryAtSoftware.Extensions.DependencyInjection.Standard/Attributes/ServiceConfigurationAttribute.cs index 551a728..8c439f0 100644 --- a/TryAtSoftware.Extensions.DependencyInjection.Standard/Attributes/ServiceConfigurationAttribute.cs +++ b/TryAtSoftware.Extensions.DependencyInjection.Standard/Attributes/ServiceConfigurationAttribute.cs @@ -29,6 +29,11 @@ public ServiceLifetime Lifetime } } + /// + /// Gets or sets a value indicating whether the decorated class should be registered as an open generic type within the standard dependency injection container. + /// + public bool? OpenGeneric { get; set; } + #if NET8_0_OR_GREATER public string? Key { get; set; } #endif diff --git a/TryAtSoftware.Extensions.DependencyInjection.Standard/ServiceRegistrar.cs b/TryAtSoftware.Extensions.DependencyInjection.Standard/ServiceRegistrar.cs index fe268d2..ee17f40 100644 --- a/TryAtSoftware.Extensions.DependencyInjection.Standard/ServiceRegistrar.cs +++ b/TryAtSoftware.Extensions.DependencyInjection.Standard/ServiceRegistrar.cs @@ -37,16 +37,29 @@ public void Register(Type type, RegisterServiceOptions? options = null) if (type is null) throw new ArgumentNullException(nameof(type)); if (!type.IsClass || type.IsAbstract) throw new InvalidOperationException("Only non-abstract classes can be registered into a dependency injection container."); - var genericParametersSetup = ExtractGenericParametersSetup(type, options); - var implementationType = ResolveGenericParameters(type, genericParametersSetup); - var implementedInterfaces = implementationType.GetInterfaces().Select(x => ResolveGenericParameters(x, genericParametersSetup)); - var configurationAttributes = this._hierarchyScanner.ScanForAttribute(type).ToArray(); + var (implementationType, implementedInterfaces) = BuildServiceTypes(type, configurationAttributes, options); + this.RegisterService(implementationType, implementationType, configurationAttributes); foreach (var interfaceType in implementedInterfaces) this.RegisterService(interfaceType, implementationType, configurationAttributes); } + private static (Type ImplementationType, IEnumerable ImplementedInterfaces) BuildServiceTypes(Type type, ServiceConfigurationAttribute[] configurationAttributes, RegisterServiceOptions? options) + { + var implementedInterfaces = type.GetInterfaces(); + if (!type.IsGenericType) return (type, implementedInterfaces); + + if (ShouldBeRegisteredAsOpenGeneric(configurationAttributes)) + { + var genericArguments = type.GetGenericArguments(); + return (type, implementedInterfaces.Where(x => genericArguments.SequenceEqual(x.GenericTypeArguments))); + } + + var genericParametersSetup = ExtractGenericParametersSetup(type, options); + return (ResolveGenericParameters(type, genericParametersSetup), implementedInterfaces.Select(x => ResolveGenericParameters(x, genericParametersSetup))); + } + private static IDictionary? ExtractGenericParametersSetup(Type serviceType, RegisterServiceOptions? options) { if (options?.GenericTypesMap is null) return null; @@ -84,6 +97,17 @@ private static ServiceLifetime ExtractLifetime(ServiceConfigurationAttribute[] c return ServiceLifetime.Scoped; } + private static bool ShouldBeRegisteredAsOpenGeneric(ServiceConfigurationAttribute[] configurationAttributes) + { + for (var i = configurationAttributes.Length - 1; i >= 0; i--) + { + var openGeneric = configurationAttributes[i].OpenGeneric; + if (openGeneric.HasValue) return openGeneric.Value; + } + + return false; + } + #if NET8_0_OR_GREATER private static string? ExtractKey(ServiceConfigurationAttribute[] configurationAttributes) { From 405c6250bddc5cc74682df6326275d74f1c6d9cb Mon Sep 17 00:00:00 2001 From: Tony Troeff Date: Sat, 1 Mar 2025 00:13:23 +0200 Subject: [PATCH 2/6] Changed the visibility of the `LifetimeIsSet` property to internal (this is a potential breaking change) --- .../Attributes/ServiceConfigurationAttribute.cs | 4 ++-- ...yAtSoftware.Extensions.DependencyInjection.Standard.csproj | 4 ++++ 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/TryAtSoftware.Extensions.DependencyInjection.Standard/Attributes/ServiceConfigurationAttribute.cs b/TryAtSoftware.Extensions.DependencyInjection.Standard/Attributes/ServiceConfigurationAttribute.cs index 8c439f0..d7a67d5 100644 --- a/TryAtSoftware.Extensions.DependencyInjection.Standard/Attributes/ServiceConfigurationAttribute.cs +++ b/TryAtSoftware.Extensions.DependencyInjection.Standard/Attributes/ServiceConfigurationAttribute.cs @@ -12,9 +12,9 @@ public class ServiceConfigurationAttribute : Attribute private ServiceLifetime _lifetime; /// - /// Gets a value indicating whether or not a value is set to the property. + /// Gets a value indicating whether a value is set to the property. /// - public bool LifetimeIsSet { get; private set; } + internal bool LifetimeIsSet { get; private set; } /// /// Gets or sets the lifetime of the decorated service. diff --git a/TryAtSoftware.Extensions.DependencyInjection.Standard/TryAtSoftware.Extensions.DependencyInjection.Standard.csproj b/TryAtSoftware.Extensions.DependencyInjection.Standard/TryAtSoftware.Extensions.DependencyInjection.Standard.csproj index 5058383..29dacf1 100644 --- a/TryAtSoftware.Extensions.DependencyInjection.Standard/TryAtSoftware.Extensions.DependencyInjection.Standard.csproj +++ b/TryAtSoftware.Extensions.DependencyInjection.Standard/TryAtSoftware.Extensions.DependencyInjection.Standard.csproj @@ -17,6 +17,10 @@ TryAtSoftware.Extensions.DependencyInjection.Standard.md + + + + all From 253b0c97d8f4c79f3a78b836a65fed08ea1ae557 Mon Sep 17 00:00:00 2001 From: Tony Troeff Date: Sat, 1 Mar 2025 00:16:04 +0200 Subject: [PATCH 3/6] Changed the initial approach a little bit as nullable boolean is not a supported type for named attribute parameters. --- .../Attributes/ServiceConfigurationAttribute.cs | 16 +++++++++++++++- .../ServiceRegistrar.cs | 4 ++-- 2 files changed, 17 insertions(+), 3 deletions(-) diff --git a/TryAtSoftware.Extensions.DependencyInjection.Standard/Attributes/ServiceConfigurationAttribute.cs b/TryAtSoftware.Extensions.DependencyInjection.Standard/Attributes/ServiceConfigurationAttribute.cs index d7a67d5..3c860ad 100644 --- a/TryAtSoftware.Extensions.DependencyInjection.Standard/Attributes/ServiceConfigurationAttribute.cs +++ b/TryAtSoftware.Extensions.DependencyInjection.Standard/Attributes/ServiceConfigurationAttribute.cs @@ -10,6 +10,7 @@ public class ServiceConfigurationAttribute : Attribute { private ServiceLifetime _lifetime; + private bool _openGeneric; /// /// Gets a value indicating whether a value is set to the property. @@ -28,11 +29,24 @@ public ServiceLifetime Lifetime this._lifetime = value; } } + + /// + /// Gets a value indicating whether a value is set to the property. + /// + internal bool OpenGenericIsSet { get; private set; } /// /// Gets or sets a value indicating whether the decorated class should be registered as an open generic type within the standard dependency injection container. /// - public bool? OpenGeneric { get; set; } + public bool OpenGeneric + { + get => this._openGeneric; + set + { + this.OpenGenericIsSet = true; + this._openGeneric = value; + } + } #if NET8_0_OR_GREATER public string? Key { get; set; } diff --git a/TryAtSoftware.Extensions.DependencyInjection.Standard/ServiceRegistrar.cs b/TryAtSoftware.Extensions.DependencyInjection.Standard/ServiceRegistrar.cs index ee17f40..4fb20dd 100644 --- a/TryAtSoftware.Extensions.DependencyInjection.Standard/ServiceRegistrar.cs +++ b/TryAtSoftware.Extensions.DependencyInjection.Standard/ServiceRegistrar.cs @@ -101,8 +101,8 @@ private static bool ShouldBeRegisteredAsOpenGeneric(ServiceConfigurationAttribut { for (var i = configurationAttributes.Length - 1; i >= 0; i--) { - var openGeneric = configurationAttributes[i].OpenGeneric; - if (openGeneric.HasValue) return openGeneric.Value; + if (configurationAttributes[i].OpenGenericIsSet) + return configurationAttributes[i].OpenGeneric; } return false; From 756b04318c09e405045d8e50a2c2f5a641e2d537 Mon Sep 17 00:00:00 2001 From: Tony Troeff Date: Fri, 7 Mar 2025 00:36:27 +0200 Subject: [PATCH 4/6] Refactor and enhance generic service registration tests Introduced parameterized tests for generic service registrations to improve coverage and flexibility. Adjusted logic in `ServiceRegistrar` to handle open generic interfaces correctly by using their generic type definitions. Streamlined and simplified test data sources for better readability and maintainability. --- .../ServiceConfigurationAttributeTests.cs | 8 ++- .../ServiceRegistrarTests.cs | 50 ++++++++++++++----- .../ServiceRegistrar.cs | 2 +- 3 files changed, 45 insertions(+), 15 deletions(-) diff --git a/TryAtSoftware.Extensions.DependencyInjection.Standard.Tests/ServiceConfigurationAttributeTests.cs b/TryAtSoftware.Extensions.DependencyInjection.Standard.Tests/ServiceConfigurationAttributeTests.cs index d68538b..511649f 100644 --- a/TryAtSoftware.Extensions.DependencyInjection.Standard.Tests/ServiceConfigurationAttributeTests.cs +++ b/TryAtSoftware.Extensions.DependencyInjection.Standard.Tests/ServiceConfigurationAttributeTests.cs @@ -31,7 +31,7 @@ public void ServiceKeyShouldHaveCorrectDefaultValue() var attribute = new ServiceConfigurationAttribute(); Assert.Null(attribute.Key); } - + [Fact] public void ServiceKeyShouldBeSetCorrectly() { @@ -49,5 +49,9 @@ public void ServiceKeyShouldBeSetToNullCorrectly() } #endif - public static IEnumerable GetServiceLifetimeData() => Enum.GetValues().Select(x => new object[] { x }); + public static TheoryData GetServiceLifetimeData() + { + var serviceLifetimeValues = Enum.GetValues(); + return new TheoryData(serviceLifetimeValues); + } } \ No newline at end of file diff --git a/TryAtSoftware.Extensions.DependencyInjection.Standard.Tests/ServiceRegistrarTests.cs b/TryAtSoftware.Extensions.DependencyInjection.Standard.Tests/ServiceRegistrarTests.cs index ab68301..ba5526d 100644 --- a/TryAtSoftware.Extensions.DependencyInjection.Standard.Tests/ServiceRegistrarTests.cs +++ b/TryAtSoftware.Extensions.DependencyInjection.Standard.Tests/ServiceRegistrarTests.cs @@ -77,26 +77,48 @@ public void ServicesShouldBeSuccessfullyRegistered(Type implementationType, Serv foreach (var i in interfaces) Assert.Contains(i, registeredServiceTypes); } - [Fact] - public void GenericServicesShouldBeSuccessfullyRegistered() + /// + /// For the parameter, attribute types are followed by the actual generic parameter type. + /// + [Theory] + [InlineData(typeof(GenericService<>), new[] { typeof(IGenericInterface<>) }, new[] { typeof(KeyTypeAttribute), typeof(int) })] + public void GenericServicesShouldBeSuccessfullyRegistered(Type implementationType, Type[] serviceTypes, Type[] genericTypesSetup) { - var implementationType = typeof(GenericService<>); - - var keyType = typeof(int); - var registrationOptions = new RegisterServiceOptions { GenericTypesMap = new Dictionary { [typeof(KeyTypeAttribute)] = keyType } }; + var genericTypesMatrix = genericTypesSetup.Chunk(2).ToArray(); + var registrationOptions = new RegisterServiceOptions { GenericTypesMap = genericTypesMatrix.ToDictionary(x => x[0], x => x[1]) }; var (serviceCollection, _, registrar) = InstantiateRegistrar(); registrar.Register(implementationType, registrationOptions); - var interfaces = implementationType.GetInterfaces(); - Assert.Equal(interfaces.Length + 1, serviceCollection.Count); + Assert.Equal(serviceTypes.Length + 1, serviceCollection.Count); - var genericImplementationType = implementationType.MakeGenericType(keyType); - var genericInterfaceType = typeof(IGenericInterface<>).MakeGenericType(keyType); + var genericTypeArguments = genericTypesMatrix.Select(x => x[1]).ToArray(); + var genericImplementationType = implementationType.MakeGenericType(genericTypeArguments); var registeredServiceTypes = AssertSuccessfulRegistration(serviceCollection, genericImplementationType, ServiceLifetime.Scoped); Assert.Contains(genericImplementationType, registeredServiceTypes); - Assert.Contains(genericInterfaceType, registeredServiceTypes); + + foreach (var serviceType in serviceTypes) + { + var genericServiceType = serviceType.MakeGenericType(genericTypeArguments); + Assert.Contains(genericServiceType, registeredServiceTypes); + } + } + + [Theory] + [InlineData(typeof(OpenGenericService<>), new[] { typeof(IGenericInterface<>) })] + [InlineData(typeof(OpenGenericService<,>), new[] { typeof(IGenericInterface<,>) })] + public void OpenGenericServicesShouldBeSuccessfullyRegistered(Type implementationType, Type[] serviceTypes) + { + var (serviceCollection, _, registrar) = InstantiateRegistrar(); + registrar.Register(implementationType); + + Assert.Equal(serviceTypes.Length + 1, serviceCollection.Count); + + var registeredServiceTypes = AssertSuccessfulRegistration(serviceCollection, implementationType, ServiceLifetime.Scoped); + + Assert.Contains(implementationType, registeredServiceTypes); + foreach (var interfaceType in serviceTypes) Assert.Contains(interfaceType, registeredServiceTypes); } private static (IServiceCollection Services, IHierarchyScanner HierarchyScanner, IServiceRegistrar Registrar) InstantiateRegistrar() @@ -149,7 +171,8 @@ private static HashSet AssertSuccessfulRegistration(IServiceCollection ser } private interface IBaseInterface { } - private interface IGenericInterface { } + private interface IGenericInterface { } + private interface IGenericInterface { } private interface IImplementedInterface1 : IBaseInterface { } private interface IImplementedInterface2 : IBaseInterface { } private abstract class BaseService : IImplementedInterface1, IImplementedInterface2 { } @@ -163,6 +186,9 @@ private class Service : BaseService { } [AttributeUsage(AttributeTargets.GenericParameter)] private class KeyTypeAttribute : Attribute { } private class GenericService<[KeyType] TKey> : IGenericInterface { } + [ServiceConfiguration(OpenGeneric = true)] private class OpenGenericService : IGenericInterface, IGenericInterface<(T First, T Second)> { } + [ServiceConfiguration(OpenGeneric = true)] private class OpenGenericService : IGenericInterface, IGenericInterface<(T1 First, T2 Second)> { } + #if NET8_0_OR_GREATER [ServiceConfiguration(Key = "service_1")] private class KeyedService : BaseService {} diff --git a/TryAtSoftware.Extensions.DependencyInjection.Standard/ServiceRegistrar.cs b/TryAtSoftware.Extensions.DependencyInjection.Standard/ServiceRegistrar.cs index 4fb20dd..f8981c4 100644 --- a/TryAtSoftware.Extensions.DependencyInjection.Standard/ServiceRegistrar.cs +++ b/TryAtSoftware.Extensions.DependencyInjection.Standard/ServiceRegistrar.cs @@ -53,7 +53,7 @@ private static (Type ImplementationType, IEnumerable ImplementedInterfaces if (ShouldBeRegisteredAsOpenGeneric(configurationAttributes)) { var genericArguments = type.GetGenericArguments(); - return (type, implementedInterfaces.Where(x => genericArguments.SequenceEqual(x.GenericTypeArguments))); + return (type, implementedInterfaces.Where(x => genericArguments.SequenceEqual(x.GenericTypeArguments)).Select(x => x.GetGenericTypeDefinition())); } var genericParametersSetup = ExtractGenericParametersSetup(type, options); From ed0d14a6e38efa7418cdbe85bd539e115f1bed1b Mon Sep 17 00:00:00 2001 From: Tony Troeff Date: Fri, 7 Mar 2025 01:01:42 +0200 Subject: [PATCH 5/6] Add support for multi-generic services and latest C# syntax Extended tests to include multi-generic services and adjusted attribute usage for them. Simplified interfaces and class definitions to use modern C# syntax. Updated project file to enable the latest language version. --- .../ServiceRegistrarTests.cs | 39 ++++++++++--------- ....DependencyInjection.Standard.Tests.csproj | 1 + 2 files changed, 22 insertions(+), 18 deletions(-) diff --git a/TryAtSoftware.Extensions.DependencyInjection.Standard.Tests/ServiceRegistrarTests.cs b/TryAtSoftware.Extensions.DependencyInjection.Standard.Tests/ServiceRegistrarTests.cs index ba5526d..e080430 100644 --- a/TryAtSoftware.Extensions.DependencyInjection.Standard.Tests/ServiceRegistrarTests.cs +++ b/TryAtSoftware.Extensions.DependencyInjection.Standard.Tests/ServiceRegistrarTests.cs @@ -78,10 +78,12 @@ public void ServicesShouldBeSuccessfullyRegistered(Type implementationType, Serv } /// - /// For the parameter, attribute types are followed by the actual generic parameter type. + /// For the parameter, + /// attribute types are followed by the actual generic parameter type. /// [Theory] [InlineData(typeof(GenericService<>), new[] { typeof(IGenericInterface<>) }, new[] { typeof(KeyTypeAttribute), typeof(int) })] + [InlineData(typeof(GenericService<,>), new[] { typeof(IGenericInterface<,>) }, new[] { typeof(KeyTypeAttribute), typeof(int), typeof(ValueTypeAttribute), typeof(string) })] public void GenericServicesShouldBeSuccessfullyRegistered(Type implementationType, Type[] serviceTypes, Type[] genericTypesSetup) { var genericTypesMatrix = genericTypesSetup.Chunk(2).ToArray(); @@ -170,27 +172,28 @@ private static HashSet AssertSuccessfulRegistration(IServiceCollection ser return registeredServiceTypes; } - private interface IBaseInterface { } - private interface IGenericInterface { } - private interface IGenericInterface { } - private interface IImplementedInterface1 : IBaseInterface { } - private interface IImplementedInterface2 : IBaseInterface { } - private abstract class BaseService : IImplementedInterface1, IImplementedInterface2 { } + private interface IBaseInterface; + private interface IGenericInterface; + private interface IGenericInterface; + private interface IImplementedInterface1 : IBaseInterface; + private interface IImplementedInterface2 : IBaseInterface; + private abstract class BaseService : IImplementedInterface1, IImplementedInterface2; - private class Service : BaseService { } - [ServiceConfiguration(Lifetime = ServiceLifetime.Transient)] private class TransientService : BaseService { } - [ServiceConfiguration(Lifetime = ServiceLifetime.Scoped)] private class ExplicitlyScopedService : BaseService { } - [ServiceConfiguration] private class ImplicitlyScopedService : BaseService { } - [ServiceConfiguration(Lifetime = ServiceLifetime.Singleton)] private class SingletonService : BaseService { } + private class Service : BaseService; + [ServiceConfiguration(Lifetime = ServiceLifetime.Transient)] private class TransientService : BaseService; + [ServiceConfiguration(Lifetime = ServiceLifetime.Scoped)] private class ExplicitlyScopedService : BaseService; + [ServiceConfiguration] private class ImplicitlyScopedService : BaseService; + [ServiceConfiguration(Lifetime = ServiceLifetime.Singleton)] private class SingletonService : BaseService; - [AttributeUsage(AttributeTargets.GenericParameter)] private class KeyTypeAttribute : Attribute { } - private class GenericService<[KeyType] TKey> : IGenericInterface { } + [AttributeUsage(AttributeTargets.GenericParameter)] private class KeyTypeAttribute : Attribute; + [AttributeUsage(AttributeTargets.GenericParameter)] private class ValueTypeAttribute : Attribute; + [ServiceConfiguration] private class GenericService<[KeyType] TKey> : IGenericInterface; + [ServiceConfiguration] private class GenericService<[KeyType] TKey, [ValueType] TValue> : IGenericInterface; - [ServiceConfiguration(OpenGeneric = true)] private class OpenGenericService : IGenericInterface, IGenericInterface<(T First, T Second)> { } - [ServiceConfiguration(OpenGeneric = true)] private class OpenGenericService : IGenericInterface, IGenericInterface<(T1 First, T2 Second)> { } + [ServiceConfiguration(OpenGeneric = true)] private class OpenGenericService : IGenericInterface, IGenericInterface<(T First, T Second)>; + [ServiceConfiguration(OpenGeneric = true)] private class OpenGenericService : IGenericInterface, IGenericInterface<(T1 First, T2 Second)>; #if NET8_0_OR_GREATER - [ServiceConfiguration(Key = "service_1")] - private class KeyedService : BaseService {} + [ServiceConfiguration(Key = "service_1")] private class KeyedService : BaseService; #endif } \ No newline at end of file diff --git a/TryAtSoftware.Extensions.DependencyInjection.Standard.Tests/TryAtSoftware.Extensions.DependencyInjection.Standard.Tests.csproj b/TryAtSoftware.Extensions.DependencyInjection.Standard.Tests/TryAtSoftware.Extensions.DependencyInjection.Standard.Tests.csproj index b24243e..51ccd64 100644 --- a/TryAtSoftware.Extensions.DependencyInjection.Standard.Tests/TryAtSoftware.Extensions.DependencyInjection.Standard.Tests.csproj +++ b/TryAtSoftware.Extensions.DependencyInjection.Standard.Tests/TryAtSoftware.Extensions.DependencyInjection.Standard.Tests.csproj @@ -3,6 +3,7 @@ net7.0;net8.0;net9.0 enable + latest enable false From c9f6d86294b3ae522b9f464342257350b99db5d9 Mon Sep 17 00:00:00 2001 From: Tony Troeff Date: Fri, 7 Mar 2025 23:56:45 +0200 Subject: [PATCH 6/6] Improving code quality. --- .../ServiceRegistrarTests.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/TryAtSoftware.Extensions.DependencyInjection.Standard.Tests/ServiceRegistrarTests.cs b/TryAtSoftware.Extensions.DependencyInjection.Standard.Tests/ServiceRegistrarTests.cs index e080430..07eedd0 100644 --- a/TryAtSoftware.Extensions.DependencyInjection.Standard.Tests/ServiceRegistrarTests.cs +++ b/TryAtSoftware.Extensions.DependencyInjection.Standard.Tests/ServiceRegistrarTests.cs @@ -78,8 +78,8 @@ public void ServicesShouldBeSuccessfullyRegistered(Type implementationType, Serv } /// - /// For the parameter, - /// attribute types are followed by the actual generic parameter type. + /// For the , + /// every attribute type is immediately followed by the corresponding generic type argument. /// [Theory] [InlineData(typeof(GenericService<>), new[] { typeof(IGenericInterface<>) }, new[] { typeof(KeyTypeAttribute), typeof(int) })]