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..07eedd0 100644 --- a/TryAtSoftware.Extensions.DependencyInjection.Standard.Tests/ServiceRegistrarTests.cs +++ b/TryAtSoftware.Extensions.DependencyInjection.Standard.Tests/ServiceRegistrarTests.cs @@ -77,26 +77,50 @@ public void ServicesShouldBeSuccessfullyRegistered(Type implementationType, Serv foreach (var i in interfaces) Assert.Contains(i, registeredServiceTypes); } - [Fact] - public void GenericServicesShouldBeSuccessfullyRegistered() + /// + /// 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) })] + [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 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() @@ -148,23 +172,28 @@ private static HashSet AssertSuccessfulRegistration(IServiceCollection ser return registeredServiceTypes; } - private interface IBaseInterface { } - 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; + [AttributeUsage(AttributeTargets.GenericParameter)] private class ValueTypeAttribute : Attribute; + [ServiceConfiguration] private class GenericService<[KeyType] TKey> : IGenericInterface; + [ServiceConfiguration] private class GenericService<[KeyType] TKey, [ValueType] TValue> : IGenericInterface; - [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 {} + [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 diff --git a/TryAtSoftware.Extensions.DependencyInjection.Standard/Attributes/ServiceConfigurationAttribute.cs b/TryAtSoftware.Extensions.DependencyInjection.Standard/Attributes/ServiceConfigurationAttribute.cs index 551a728..3c860ad 100644 --- a/TryAtSoftware.Extensions.DependencyInjection.Standard/Attributes/ServiceConfigurationAttribute.cs +++ b/TryAtSoftware.Extensions.DependencyInjection.Standard/Attributes/ServiceConfigurationAttribute.cs @@ -10,11 +10,12 @@ public class ServiceConfigurationAttribute : Attribute { private ServiceLifetime _lifetime; + private bool _openGeneric; /// - /// 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. @@ -28,6 +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 => 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 fe268d2..f8981c4 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)).Select(x => x.GetGenericTypeDefinition())); + } + + 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--) + { + if (configurationAttributes[i].OpenGenericIsSet) + return configurationAttributes[i].OpenGeneric; + } + + return false; + } + #if NET8_0_OR_GREATER private static string? ExtractKey(ServiceConfigurationAttribute[] configurationAttributes) { 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