Skip to content

Commit

Permalink
Merge pull request #16 from Tim-Maes/feature/options
Browse files Browse the repository at this point in the history
Add support for IOptions<T>
  • Loading branch information
Tim-Maes authored Oct 23, 2023
2 parents fbd2e5a + f8a629d commit b766ba0
Show file tree
Hide file tree
Showing 5 changed files with 213 additions and 109 deletions.
72 changes: 56 additions & 16 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
## Features 🌟

- Automatic registration of services using custom attributes.
- Automatic registration and configuration of options via `IOptions<T>`.
- No need for explicit interface specification for class-only registrations.
- Provides clear visibility and reduces boilerplate code.
- Simple integration with the built-in .NET IoC container.
Expand Down Expand Up @@ -41,34 +42,52 @@ dotnet add package Bindicate

### Autowire dependencies

**Register Services per Assembly**
**Register Services**

Add this line in a project to register all decorated services. You can repeat this line and pass any assembly.
To also configure options, use `.WithOptions()`.
You can also use the `ServiceCollectionExtension` pattern and use `IConfiguration` as a parameters for your extension method if they have options to register.

**Example in host project**
```csharp
// Register all types in current project
services.AddAutowiringForAssembly(Assembly.GetExecutingAssembly());

// Register types from referenced project
services.AddAutowiringForAssembly(Assembly.GetAssembly(typeof(IInterface)));
// Register all decorated services in the current project
builder.Services
.AddAutowiringForAssembly(Assembly.GetExecutingAssembly())
.Register();

// Also register Options as IOptions<T>
builder.Services
.AddAutowiringForAssembly(Assembly.GetExecutingAssembly())
.WithOptions(Configuration) //Pass builder.Configuration here
.Register();

// Register types and options from referenced project
builder.Services
.AddAutowiringForAssembly(Assembly.GetAssembly(typeof(IInterface)))
.WithOptions(Configuration)
.Register();
```

**Register Services Across Multiple Assemblies**

If you want to scan and register services across all loaded assemblies, you can do so by adding the following line in your hosting project:

***Note** that this might not work if not all assemblies are loaded at this point in startup configuration*!
**Example with ServiceCollectionExtensions**

```csharp
// Trigger loading of unloaded assemblies to be able to use AddAutowiring:
var triggerAssembly1 = typeof(ProjectName.SomeType);
var triggerAssembly2 = typeof(OtherProjectName.SomeOtherType);
// Hosting project:
var configuration = builder.Configuration;

services.AddAutowiring();
builder.Services.AddSecondProject(configuration);

// In other project
public static IServiceCollection AddSecondProject(this IServiceCollection services, IConfiguration configuration)
{
services.AddAutowiringForAssembly(Assembly.GetExecutingAssembly())
.WithOptions(configuration)
.Register();

//Or just use AddAutowiringForAssembly method
return services;
}
```


## Decorate your services:

### Basic usage
Expand Down Expand Up @@ -117,6 +136,27 @@ public interface IMyTaskRunner
}
```

### Options Registration

Decorate your class containing the options with `[RegisterOptions]` and specify the corresponding section in `appsettings.json`.

```
[RegisterOptions("testOptions")]
public class TestOptions
{
public string Test { get; set; } = "";
}
//appsettings.json:
{
"testOptions": {
"test": "test"
}
}
```

Now you can use this value when injection IOptions<TestOptions> in your service

### Generics

**Define a generic interface:**
Expand Down
12 changes: 12 additions & 0 deletions src/Bindicate/Attributes/Options/RegisterOptionsAttribute.cs
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;
}
}
6 changes: 4 additions & 2 deletions src/Bindicate/Bindicate.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,9 @@
<PackageReadmeFile>README.md</PackageReadmeFile>
<RepositoryUrl>https://www.github.com/Tim-Maes/Bindicate</RepositoryUrl>
<PackageTags>di, ioc, service, collection, extensions, attribute</PackageTags>
<PackageReleaseNotes>Can scan all assemblies</PackageReleaseNotes>
<PackageReleaseNotes>Add support for IOptions</PackageReleaseNotes>
<PackageLicenseFile>LICENSE.txt</PackageLicenseFile>
<Version>1.1.9</Version>
<Version>1.2.0</Version>
</PropertyGroup>

<ItemGroup>
Expand All @@ -28,7 +28,9 @@
</ItemGroup>

<ItemGroup>
<PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="7.0.0" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="7.0.0" />
<PackageReference Include="Microsoft.Extensions.Options.ConfigurationExtensions" Version="7.0.0" />
</ItemGroup>

</Project>
134 changes: 134 additions & 0 deletions src/Bindicate/Configuration/AutowiringBuilder.cs
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

View workflow job for this annotation

GitHub Actions / run_test

Dereference of a possibly null reference.

Check warning on line 101 in src/Bindicate/Configuration/AutowiringBuilder.cs

View workflow job for this annotation

GitHub Actions / create_nuget

Dereference of a possibly null reference.
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 src/Bindicate/Configuration/ServiceCollectionExtensions.cs
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);
}

0 comments on commit b766ba0

Please sign in to comment.