-
-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #16 from Tim-Maes/feature/options
Add support for IOptions<T>
- Loading branch information
Showing
5 changed files
with
213 additions
and
109 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
12 changes: 12 additions & 0 deletions
12
src/Bindicate/Attributes/Options/RegisterOptionsAttribute.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,12 @@ | ||
namespace Bindicate.Attributes.Options; | ||
|
||
[AttributeUsage(AttributeTargets.Class, AllowMultiple = false)] | ||
public class RegisterOptionsAttribute : Attribute | ||
{ | ||
public string ConfigurationSection { get; } | ||
|
||
public RegisterOptionsAttribute(string configurationSection) | ||
{ | ||
ConfigurationSection = configurationSection; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,134 @@ | ||
using Bindicate.Attributes; | ||
using Bindicate.Attributes.Options; | ||
using Bindicate.Lifetime; | ||
using Microsoft.Extensions.Configuration; | ||
using Microsoft.Extensions.DependencyInjection; | ||
using Microsoft.Extensions.DependencyInjection.Extensions; | ||
using System.Reflection; | ||
|
||
namespace Bindicate.Configuration; | ||
|
||
public class AutowiringBuilder | ||
{ | ||
public IServiceCollection _services { get; } | ||
|
||
public Assembly _targetAssembly { get; } | ||
|
||
public AutowiringBuilder(IServiceCollection services, Assembly targetAssembly) | ||
{ | ||
_services = services; | ||
_targetAssembly = targetAssembly; | ||
AddAutowiringForAssembly(); | ||
} | ||
|
||
/// <summary> | ||
/// Scans the assembly to automatically wire up services based on the attributes. | ||
/// </summary> | ||
/// <returns>A reference to this instance after the operation has completed.</returns> | ||
public AutowiringBuilder AddAutowiringForAssembly() | ||
{ | ||
foreach (var type in _targetAssembly.GetTypes().Where(t => t.IsClass && !t.IsAbstract)) | ||
{ | ||
var registerAttributes = type.GetCustomAttributes(typeof(BaseServiceAttribute), false) | ||
.Cast<BaseServiceAttribute>(); | ||
|
||
foreach (var attr in registerAttributes) | ||
{ | ||
var serviceType = attr.ServiceType ?? type; | ||
var registrationMethod = GetRegistrationMethod(_services, attr.Lifetime); | ||
|
||
if (serviceType.IsDefined(typeof(RegisterGenericInterfaceAttribute), false)) | ||
{ | ||
if (serviceType.IsGenericType) | ||
{ | ||
_services.Add(ServiceDescriptor.Describe( | ||
serviceType.GetGenericTypeDefinition(), | ||
type.GetGenericTypeDefinition(), | ||
attr.Lifetime.ConvertToServiceLifetime()) | ||
); | ||
} | ||
else | ||
{ | ||
// Handle non-generic services with generic interfaces | ||
foreach (var iface in type.GetInterfaces()) | ||
{ | ||
if (iface.IsGenericType && iface.GetGenericTypeDefinition().IsDefined(typeof(RegisterGenericInterfaceAttribute), false)) | ||
{ | ||
var genericInterface = iface.GetGenericTypeDefinition(); | ||
_services.Add(ServiceDescriptor.Describe(genericInterface, type, attr.Lifetime.ConvertToServiceLifetime())); | ||
} | ||
} | ||
} | ||
} | ||
else if (type.GetInterfaces().Contains(serviceType) || type == serviceType) | ||
{ | ||
RegisterService(serviceType, type, registrationMethod); | ||
} | ||
else | ||
{ | ||
throw new InvalidOperationException($"Type {type.FullName} does not implement {serviceType.FullName}"); | ||
} | ||
} | ||
} | ||
|
||
return this; | ||
} | ||
|
||
/// <summary> | ||
/// Scans assemblies to find classes annotated with RegisterOptionsAttribute, | ||
/// and configures them as options from the provided IConfiguration object. | ||
/// </summary> | ||
/// <param name="configuration">The IConfiguration object to read the settings from.</param> | ||
/// <returns>A reference to this instance after the operation has completed.</returns> | ||
public AutowiringBuilder WithOptions(IConfiguration configuration) | ||
{ | ||
foreach (var type in _targetAssembly.GetTypes().Where(t => t.IsClass && !t.IsAbstract)) | ||
{ | ||
var optionAttributes = type.GetCustomAttributes(typeof(RegisterOptionsAttribute), false) | ||
.Cast<RegisterOptionsAttribute>(); | ||
|
||
foreach (var attr in optionAttributes) | ||
{ | ||
var configSection = configuration.GetSection(attr.ConfigurationSection); | ||
|
||
if (!configSection.Exists()) | ||
throw new InvalidOperationException($"Missing configuration section: {attr.ConfigurationSection}"); | ||
|
||
var genericOptionsConfigureMethod = typeof(OptionsConfigurationServiceCollectionExtensions) | ||
.GetMethods() | ||
.FirstOrDefault(m => m.Name == "Configure" && m.GetParameters().Length == 2); | ||
|
||
var specializedMethod = genericOptionsConfigureMethod.MakeGenericMethod(type); | ||
Check warning on line 101 in src/Bindicate/Configuration/AutowiringBuilder.cs GitHub Actions / run_test
|
||
specializedMethod.Invoke(null, new object[] { _services, configSection }); | ||
} | ||
} | ||
|
||
return this; | ||
} | ||
|
||
/// <summary> | ||
/// Registers all configured services and options into the IServiceCollection. | ||
/// </summary> | ||
/// <returns>The IServiceCollection that services and options were registered into.</returns> | ||
public IServiceCollection Register() | ||
{ | ||
return _services; | ||
} | ||
|
||
private static Action<Type, Type> GetRegistrationMethod(IServiceCollection services, Lifetime.Lifetime lifetime) | ||
=> lifetime switch | ||
{ | ||
Lifetime.Lifetime.Scoped => (s, t) => services.AddScoped(s, t), | ||
Lifetime.Lifetime.Singleton => (s, t) => services.AddSingleton(s, t), | ||
Lifetime.Lifetime.Transient => (s, t) => services.AddTransient(s, t), | ||
Lifetime.Lifetime.TryAddScoped => (s, t) => services.TryAddScoped(s, t), | ||
Lifetime.Lifetime.TryAddSingleton => (s, t) => services.TryAddSingleton(s, t), | ||
Lifetime.Lifetime.TryAddTransient => (s, t) => services.TryAddTransient(s, t), | ||
_ => throw new ArgumentOutOfRangeException(nameof(lifetime), "Unsupported lifetime.") | ||
}; | ||
|
||
private static void RegisterService(Type serviceType, Type implementationType, Action<Type, Type> registrationMethod) | ||
{ | ||
registrationMethod(serviceType, implementationType); | ||
} | ||
} |
98 changes: 7 additions & 91 deletions
98
src/Bindicate/Configuration/ServiceCollectionExtensions.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,101 +1,17 @@ | ||
using Bindicate.Attributes; | ||
using Bindicate.Lifetime; | ||
using Microsoft.Extensions.DependencyInjection; | ||
using Microsoft.Extensions.DependencyInjection.Extensions; | ||
using Microsoft.Extensions.DependencyInjection; | ||
using System.Reflection; | ||
|
||
namespace Bindicate.Configuration; | ||
|
||
public static class ServiceCollectionExtensions | ||
{ | ||
/// <summary> | ||
/// Registers services using autowiring for all loaded assemblies. | ||
/// This will scan through all types in each loaded assembly and | ||
/// register them according to their attributes. | ||
/// Initializes a new instance of the <see cref="AutowiringBuilder"/> class for automatically | ||
/// registering services from the specified assembly. | ||
/// </summary> | ||
/// <param name="services">The <see cref="IServiceCollection"/> to add the services to.</param> | ||
/// <returns>The same service collection so that multiple calls can be chained.</returns> | ||
public static IServiceCollection AddAutowiring(this IServiceCollection services) | ||
{ | ||
// Iterate over all loaded assemblies and call AddAutowiringForAssembly for each one | ||
foreach (var assembly in AppDomain.CurrentDomain.GetAssemblies()) | ||
{ | ||
services.AddAutowiringForAssembly(assembly); | ||
} | ||
return services; | ||
} | ||
|
||
/// <summary> | ||
/// Registers services using autowiring for a specific assembly. | ||
/// This will scan through all types in the given assembly and | ||
/// register them according to their attributes. | ||
/// </summary> | ||
/// <param name="services">The <see cref="IServiceCollection"/> to add the services to.</param> | ||
/// <param name="assembly">The assembly to scan for types.</param> | ||
/// <returns>The same service collection so that multiple calls can be chained.</returns> | ||
public static IServiceCollection AddAutowiringForAssembly(this IServiceCollection services, Assembly assembly) | ||
{ | ||
foreach (var type in assembly.GetTypes().Where(t => t.IsClass && !t.IsAbstract)) | ||
{ | ||
var registerAttributes = type.GetCustomAttributes(typeof(BaseServiceAttribute), false) | ||
.Cast<BaseServiceAttribute>(); | ||
|
||
foreach (var attr in registerAttributes) | ||
{ | ||
var serviceType = attr.ServiceType ?? type; | ||
var registrationMethod = GetRegistrationMethod(services, attr.Lifetime); | ||
|
||
if (serviceType.IsDefined(typeof(RegisterGenericInterfaceAttribute), false)) | ||
{ | ||
if (serviceType.IsGenericType) | ||
{ | ||
services.Add(ServiceDescriptor.Describe( | ||
serviceType.GetGenericTypeDefinition(), | ||
type.GetGenericTypeDefinition(), | ||
attr.Lifetime.ConvertToServiceLifetime()) | ||
); | ||
} | ||
else | ||
{ | ||
// Handle non-generic services with generic interfaces | ||
foreach (var iface in type.GetInterfaces()) | ||
{ | ||
if (iface.IsGenericType && iface.GetGenericTypeDefinition().IsDefined(typeof(RegisterGenericInterfaceAttribute), false)) | ||
{ | ||
var genericInterface = iface.GetGenericTypeDefinition(); | ||
services.Add(ServiceDescriptor.Describe(genericInterface, type, attr.Lifetime.ConvertToServiceLifetime())); | ||
} | ||
} | ||
} | ||
} | ||
else if (type.GetInterfaces().Contains(serviceType) || type == serviceType) | ||
{ | ||
RegisterService(serviceType, type, registrationMethod); | ||
} | ||
else | ||
{ | ||
throw new InvalidOperationException($"Type {type.FullName} does not implement {serviceType.FullName}"); | ||
} | ||
} | ||
} | ||
|
||
return services; | ||
} | ||
|
||
private static Action<Type, Type> GetRegistrationMethod(IServiceCollection services, Lifetime.Lifetime lifetime) | ||
=> lifetime switch | ||
{ | ||
Lifetime.Lifetime.Scoped => (s, t) => services.AddScoped(s, t), | ||
Lifetime.Lifetime.Singleton => (s, t) => services.AddSingleton(s, t), | ||
Lifetime.Lifetime.Transient => (s, t) => services.AddTransient(s, t), | ||
Lifetime.Lifetime.TryAddScoped => (s, t) => services.TryAddScoped(s, t), | ||
Lifetime.Lifetime.TryAddSingleton => (s, t) => services.TryAddSingleton(s, t), | ||
Lifetime.Lifetime.TryAddTransient => (s, t) => services.TryAddTransient(s, t), | ||
_ => throw new ArgumentOutOfRangeException(nameof(lifetime), "Unsupported lifetime.") | ||
}; | ||
|
||
private static void RegisterService(Type serviceType, Type implementationType, Action<Type, Type> registrationMethod) | ||
{ | ||
registrationMethod(serviceType, implementationType); | ||
} | ||
/// <param name="targetAssembly">The assembly to scan for types to register.</param> | ||
/// <returns>An instance of <see cref="AutowiringBuilder"/> configured with the provided services and assembly.</returns> | ||
public static AutowiringBuilder AddAutowiringForAssembly(this IServiceCollection services, Assembly targetAssembly) | ||
=> new AutowiringBuilder(services, targetAssembly); | ||
} |