Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ public void ServiceKeyShouldHaveCorrectDefaultValue()
var attribute = new ServiceConfigurationAttribute();
Assert.Null(attribute.Key);
}

[Fact]
public void ServiceKeyShouldBeSetCorrectly()
{
Expand All @@ -49,5 +49,9 @@ public void ServiceKeyShouldBeSetToNullCorrectly()
}
#endif

public static IEnumerable<object[]> GetServiceLifetimeData() => Enum.GetValues<ServiceLifetime>().Select(x => new object[] { x });
public static TheoryData<ServiceLifetime> GetServiceLifetimeData()
{
var serviceLifetimeValues = Enum.GetValues<ServiceLifetime>();
return new TheoryData<ServiceLifetime>(serviceLifetimeValues);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -77,26 +77,50 @@ public void ServicesShouldBeSuccessfullyRegistered(Type implementationType, Serv
foreach (var i in interfaces) Assert.Contains(i, registeredServiceTypes);
}

[Fact]
public void GenericServicesShouldBeSuccessfullyRegistered()
/// <remarks>
/// For the <paramref name="genericTypesSetup"/>,
/// every attribute type is immediately followed by the corresponding generic type argument.
/// </remarks>
[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<Type, Type> { [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()
Expand Down Expand Up @@ -148,23 +172,28 @@ private static HashSet<Type> AssertSuccessfulRegistration(IServiceCollection ser
return registeredServiceTypes;
}

private interface IBaseInterface { }
private interface IGenericInterface<TKey> { }
private interface IImplementedInterface1 : IBaseInterface { }
private interface IImplementedInterface2 : IBaseInterface { }
private abstract class BaseService : IImplementedInterface1, IImplementedInterface2 { }
private interface IBaseInterface;
private interface IGenericInterface<T>;
private interface IGenericInterface<T1, T2>;
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<TKey>;
[ServiceConfiguration] private class GenericService<[KeyType] TKey, [ValueType] TValue> : IGenericInterface<TKey, TValue>;

[AttributeUsage(AttributeTargets.GenericParameter)] private class KeyTypeAttribute : Attribute { }
private class GenericService<[KeyType] TKey> : IGenericInterface<TKey> { }
[ServiceConfiguration(OpenGeneric = true)] private class OpenGenericService<T> : IGenericInterface<T>, IGenericInterface<(T First, T Second)>;
[ServiceConfiguration(OpenGeneric = true)] private class OpenGenericService<T1, T2> : IGenericInterface<T1, T2>, 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
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
<PropertyGroup>
<TargetFrameworks>net7.0;net8.0;net9.0</TargetFrameworks>
<ImplicitUsings>enable</ImplicitUsings>
<LangVersion>latest</LangVersion>
<Nullable>enable</Nullable>

<IsPackable>false</IsPackable>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,12 @@
public class ServiceConfigurationAttribute : Attribute
{
private ServiceLifetime _lifetime;
private bool _openGeneric;

/// <summary>
/// Gets a value indicating whether or not a value is set to the <see cref="Lifetime"/> property.
/// Gets a value indicating whether a value is set to the <see cref="Lifetime"/> property.
/// </summary>
public bool LifetimeIsSet { get; private set; }
internal bool LifetimeIsSet { get; private set; }

/// <summary>
/// Gets or sets the lifetime of the decorated service.
Expand All @@ -28,6 +29,24 @@ public ServiceLifetime Lifetime
this._lifetime = value;
}
}

/// <summary>
/// Gets a value indicating whether a value is set to the <see cref="OpenGeneric"/> property.
/// </summary>
internal bool OpenGenericIsSet { get; private set; }

/// <summary>
/// Gets or sets a value indicating whether the decorated class should be registered as an open generic type within the standard dependency injection container.
/// </summary>
public bool OpenGeneric
{
get => this._openGeneric;
set
{
this.OpenGenericIsSet = true;
this._openGeneric = value;
}
}

#if NET8_0_OR_GREATER
public string? Key { get; set; }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,16 +37,29 @@
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<ServiceConfigurationAttribute>(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<Type> 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<string, Type>? ExtractGenericParametersSetup(Type serviceType, RegisterServiceOptions? options)
{
if (options?.GenericTypesMap is null) return null;
Expand All @@ -69,7 +82,7 @@
if (!string.IsNullOrWhiteSpace(key)) serviceDescriptor = new ServiceDescriptor(interfaceType, key, implementationType, lifetime);
#endif

serviceDescriptor ??= new ServiceDescriptor(interfaceType, implementationType, lifetime);

Check warning on line 85 in TryAtSoftware.Extensions.DependencyInjection.Standard/ServiceRegistrar.cs

View workflow job for this annotation

GitHub Actions / CI

Remove this unnecessary check for null. (https://rules.sonarsource.com/csharp/RSPEC-2589)

Check warning on line 85 in TryAtSoftware.Extensions.DependencyInjection.Standard/ServiceRegistrar.cs

View workflow job for this annotation

GitHub Actions / CI

Remove this unnecessary check for null. (https://rules.sonarsource.com/csharp/RSPEC-2589)
this._services.Add(serviceDescriptor);
}

Expand All @@ -84,6 +97,17 @@
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)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,10 @@
<PackageReadmeFile>TryAtSoftware.Extensions.DependencyInjection.Standard.md</PackageReadmeFile>
</PropertyGroup>

<ItemGroup>
<InternalsVisibleTo Include="TryAtSoftware.Extensions.DependencyInjection.Standard.Tests" />
</ItemGroup>

<ItemGroup>
<PackageReference Include="SonarAnalyzer.CSharp" Version="10.6.0.109712">
<PrivateAssets>all</PrivateAssets>
Expand Down
Loading