Skip to content

Commit

Permalink
feat: Adding the ability to register types via attribute
Browse files Browse the repository at this point in the history
* The most significant changes involve the addition of a new attribute-based registration feature. This feature allows classes and interfaces to be registered with Canister using the `RegisterAttribute` and `RegisterAllAttribute` attributes.

*  The `dotnet-publish.yml` file was also updated to publish coverage reports to coveralls.io.
  • Loading branch information
JaCraig committed Feb 19, 2024
1 parent 98f319c commit 3376b6a
Show file tree
Hide file tree
Showing 8 changed files with 318 additions and 1 deletion.
8 changes: 7 additions & 1 deletion .github/workflows/dotnet-publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -48,14 +48,20 @@ jobs:
run: dotnet build --no-restore --configuration $BUILD_CONFIG

- name: Test
run: dotnet test /p:Configuration=$BUILD_CONFIG --no-build --verbosity normal --logger trx --results-directory "TestResults-${{ matrix.dotnet-version }}"
run: dotnet test /p:CollectCoverage=true /p:CoverletOutput=TestResults-${{ matrix.dotnet-version }}/ /p:CoverletOutputFormat=lcov /p:Configuration=$BUILD_CONFIG --no-build --verbosity normal --logger trx --results-directory "TestResults-${{ matrix.dotnet-version }}"

- name: Upload test results
uses: actions/upload-artifact@v4
with:
name: dotnet-results-${{ matrix.dotnet-version }}
path: TestResults-${{ matrix.dotnet-version }}
if: ${{ always() }}

- name: Publish coverage report to coveralls.io
uses: coverallsapp/github-action@master
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
path-to-lcov: ./test/Canister.Tests/TestResults-${{ matrix.dotnet-version }}/coverage.net8.0.info

- name: Upload NuGet package
uses: actions/upload-artifact@v4
Expand Down
28 changes: 28 additions & 0 deletions Canister.IoC.Example/Program.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using Canister.Interfaces;
using Canister.IoC.Attributes;
using Microsoft.Extensions.DependencyInjection;

namespace Canister.IoC.Example
Expand All @@ -15,6 +16,14 @@ internal interface IMyService
string Name { get; }
}

/// <summary>
/// Interface that has all classes that implement it registered as singletons
/// </summary>
[RegisterAll(ServiceLifetime.Singleton)]
internal interface IRegisteredInterface
{
}

/// <summary>
/// Example of how to use Canister
/// </summary>
Expand All @@ -35,6 +44,11 @@ private static void Main(string[] args)
// Build the service provider
?.BuildServiceProvider();

// Get all the services that implement IIRegisteredInterface
IEnumerable<IRegisteredInterface> RegisteredClasses = ServiceProvider?.GetServices<IRegisteredInterface>() ?? Array.Empty<IRegisteredInterface>();
// Write out the number of services found (should be 2)
Console.WriteLine("Number of registered classes found: {0}", RegisteredClasses.Count());

// Get all the services that implement IMyService
IEnumerable<IMyService> ServiceClasses = ServiceProvider?.GetServices<IMyService>() ?? Array.Empty<IMyService>();
// Write out the number of services found (should be 2)
Expand Down Expand Up @@ -94,6 +108,20 @@ internal class MyModule : IModule
public void Load(IServiceCollection serviceDescriptors) => serviceDescriptors.AddTransient<SimpleExampleClass>();
}

/// <summary>
/// Class that implements IRegisteredInterface
/// </summary>
internal class RegisteredClass1 : IRegisteredInterface
{
}

/// <summary>
/// Class that implements IRegisteredInterface
/// </summary>
internal class RegisteredClass2 : IRegisteredInterface
{
}

/// <summary>
/// This is a simple example class
/// </summary>
Expand Down
22 changes: 22 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,28 @@ The module above is loaded automatically by the system and will have the Load fu

The AddAllxxxx functions will find everything that implements a class or interface in the Assemblies that you tell it to look in and will register them with the service collection.

## Attributes

Canister also allows for attributes to be used to control registration. There are two attributes that the system uses:

* RegisterAttribute - This attribute is used to control how a class is registered. It will register the class as all interfaces that it implements as well as the class itself. The attribute takes the life time of the registration as a parameter. If no parameter is given, the registration will be transient. It also can take a service key as well.

```csharp
[Register(LifeTime.Singleton)]
public class MyType : IMyInterface
{
}
```

* RegisterAllAttribute - This attribute is used to control how an interface is registered. It will register all classes that implement the interface similar to the AddAllxxxx functions. The attribute takes the life time of the registration as a parameter. If no parameter is given, the registration will be transient.

```csharp
[RegisterAll(LifeTime.Singleton)]
public interface IMyInterface
{
}
```

### Canister Extension Methods

Canister provides a set of extension methods to streamline your IoC (Inversion of Control) container registration code. These methods offer convenient ways to conditionally register services based on certain criteria, enhancing the flexibility of your application's dependency injection setup.
Expand Down
1 change: 1 addition & 0 deletions docfx_project/articles/intro.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
# Output

```
Number of registered classes found: 2
Number of services found: 2
ExampleService1
ExampleService2
Expand Down
35 changes: 35 additions & 0 deletions src/Canister.IoC/Attributes/RegisterAllAttribute.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
using Microsoft.Extensions.DependencyInjection;
using System;

namespace Canister.IoC.Attributes
{
/// <summary>
/// This attribute is used to register all items of a specific type with Canister and is used to
/// mark a type for registration.
/// </summary>
[AttributeUsage(AttributeTargets.Interface, AllowMultiple = false)]
public class RegisterAllAttribute : Attribute
{
/// <summary>
/// Constructor
/// </summary>
public RegisterAllAttribute()
{
Lifetime = ServiceLifetime.Transient;
}

/// <summary>
/// Constructor
/// </summary>
/// <param name="lifetime">Lifetime of the service</param>
public RegisterAllAttribute(ServiceLifetime lifetime)
{
Lifetime = lifetime;
}

/// <summary>
/// The lifetime of the service.
/// </summary>
public ServiceLifetime Lifetime { get; set; }
}
}
41 changes: 41 additions & 0 deletions src/Canister.IoC/Attributes/RegisterAttribute.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
using Microsoft.Extensions.DependencyInjection;
using System;

namespace Canister.IoC.Attributes
{
/// <summary>
/// This attribute is used to register a type with Canister and is used to mark a type for registration.
/// </summary>
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct, AllowMultiple = false)]
public class RegisterAttribute : Attribute
{
/// <summary>
/// Constructor
/// </summary>
public RegisterAttribute()
{
Lifetime = ServiceLifetime.Transient;
}

/// <summary>
/// Constructor
/// </summary>
/// <param name="lifetime">Lifetime of the service</param>
/// <param name="serviceKey">The service key to register as (if any)</param>
public RegisterAttribute(ServiceLifetime lifetime, object? serviceKey = null)
{
Lifetime = lifetime;
ServiceKey = serviceKey;
}

/// <summary>
/// The lifetime of the service.
/// </summary>
public ServiceLifetime Lifetime { get; set; }

/// <summary>
/// The service key to register as (if any).
/// </summary>
public object? ServiceKey { get; set; }
}
}
125 changes: 125 additions & 0 deletions src/Canister.IoC/ExtensionMethods/ServiceCollectionExtensions.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using Canister.Interfaces;
using Canister.IoC.Attributes;
using Canister.IoC.Utils;
using Fast.Activator;
using Microsoft.Extensions.DependencyInjection.Extensions;
Expand Down Expand Up @@ -35,6 +36,25 @@ private static Assembly[] Assemblies
}
}

/// <summary>
/// The available interfaces
/// </summary>
private static Type[] AvailableInterfaces
{
get
{
if (_AvailableInterfaces is not null)
return _AvailableInterfaces;
lock (_TypeLockObject)
{
if (_AvailableInterfaces is not null)
return _AvailableInterfaces;
_AvailableInterfaces = GetAvailableInterfaces();
return _AvailableInterfaces;
}
}
}

/// <summary>
/// Gets the available types.
/// </summary>
Expand Down Expand Up @@ -70,6 +90,11 @@ private static Type[] AvailableTypes
/// </summary>
private static Assembly[]? _Assemblies;

/// <summary>
/// The available interfaces
/// </summary>
private static Type[]? _AvailableInterfaces;

/// <summary>
/// The available types
/// </summary>
Expand Down Expand Up @@ -197,6 +222,14 @@ private static Type[] AvailableTypes
{
ResolvedModule.Load(serviceDescriptors);
}

//Load types with the RegisterAttribute to the service collection
foreach (Type RegisteredType in GetAllRegisteredTypes())
{
RegisterClasses(serviceDescriptors, RegisteredType);
RegisterInterfaces(serviceDescriptors, RegisteredType);
}

serviceDescriptors.TryAddSingleton<CanisterRegisteredFlag>();

//Clear info and return
Expand Down Expand Up @@ -1036,6 +1069,32 @@ private static Assembly[] FindModules()
/// <returns>The objects of a specific type.</returns>
private static IEnumerable<TObject> GetAllOfType<TObject>() => AvailableTypes.Where(type => typeof(TObject).IsAssignableFrom(type)).Select(type => (TObject)FastActivator.CreateInstance(type));

/// <summary>
/// Gets all attributes that have the RegisterAttribute.
/// </summary>
/// <returns>The list of types.</returns>
private static IEnumerable<Type> GetAllRegisteredTypes() => AvailableTypes.Where(type => type.GetCustomAttributes<RegisterAttribute>().Any())
.Concat(AvailableInterfaces.Where(type => type.GetCustomAttributes<RegisterAllAttribute>().Any()));

/// <summary>
/// Gets the available interfaces
/// </summary>
/// <returns>The interfaces that are available</returns>
private static Type[] GetAvailableInterfaces()
{
return Assemblies.SelectMany(x =>
{
try
{
return x.GetTypes();
}
catch (ReflectionTypeLoadException) { return Array.Empty<Type>(); }
})
.Where(x => x.IsInterface
&& !x.ContainsGenericParameters)
.ToArray();
}

/// <summary>
/// Gets the available types.
/// </summary>
Expand All @@ -1061,5 +1120,71 @@ private static Type[] GetAvailableTypes()
/// </summary>
/// <param name="configuration">The configuration.</param>
private static void LoadModules(ICanisterConfiguration configuration) => configuration.AddAssembly(Assemblies);

/// <summary>
/// Registers the classes.
/// </summary>
/// <param name="serviceDescriptors">Services</param>
/// <param name="registeredType">Registered type</param>
private static void RegisterClasses(IServiceCollection? serviceDescriptors, Type registeredType)
{
if (serviceDescriptors is null)
return;
IEnumerable<Type> TypeChain = registeredType.GetInterfaces().Concat(new[] { registeredType });
foreach (RegisterAttribute RegisterAttribute in registeredType.GetCustomAttributes<RegisterAttribute>())
{
foreach (Type? ServiceType in TypeChain)
{
switch (RegisterAttribute.Lifetime)
{
case ServiceLifetime.Scoped:
_ = RegisterAttribute.ServiceKey is null
? serviceDescriptors.AddScoped(ServiceType, registeredType)
: serviceDescriptors.AddKeyedScoped(ServiceType, RegisterAttribute.ServiceKey, registeredType);
break;

case ServiceLifetime.Singleton:
_ = RegisterAttribute.ServiceKey is null
? serviceDescriptors.AddSingleton(ServiceType, registeredType)
: serviceDescriptors.AddKeyedSingleton(ServiceType, RegisterAttribute.ServiceKey, registeredType);
break;

case ServiceLifetime.Transient:
_ = RegisterAttribute.ServiceKey is null
? serviceDescriptors.AddTransient(ServiceType, registeredType)
: serviceDescriptors.AddKeyedTransient(ServiceType, RegisterAttribute.ServiceKey, registeredType);
break;
}
}
}
}

/// <summary>
/// Registers the interfaces.
/// </summary>
/// <param name="serviceDescriptors">The service descriptors.</param>
/// <param name="registeredType">The registered type.</param>
private static void RegisterInterfaces(IServiceCollection? serviceDescriptors, Type registeredType)
{
if (serviceDescriptors is null)
return;
foreach (RegisterAllAttribute RegisterAllAttribute in registeredType.GetCustomAttributes<RegisterAllAttribute>())
{
switch (RegisterAllAttribute.Lifetime)
{
case ServiceLifetime.Scoped:
_ = serviceDescriptors.AddAllScoped(registeredType);
break;

case ServiceLifetime.Singleton:
_ = serviceDescriptors.AddAllSingleton(registeredType);
break;

case ServiceLifetime.Transient:
_ = serviceDescriptors.AddAllTransient(registeredType);
break;
}
}
}
}
}

0 comments on commit 3376b6a

Please sign in to comment.