Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

WIP: Add support for IOptions<T> #16

Merged
merged 12 commits into from
Oct 23, 2023
Merged
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
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);
}
Loading