diff --git a/.github/workflows/dotnet-publish.yml b/.github/workflows/dotnet-publish.yml index ee2a054..8112314 100644 --- a/.github/workflows/dotnet-publish.yml +++ b/.github/workflows/dotnet-publish.yml @@ -48,7 +48,7 @@ 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 @@ -56,6 +56,12 @@ jobs: 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 diff --git a/Canister.IoC.Example/Program.cs b/Canister.IoC.Example/Program.cs index 514be5a..42551f0 100644 --- a/Canister.IoC.Example/Program.cs +++ b/Canister.IoC.Example/Program.cs @@ -1,4 +1,5 @@ using Canister.Interfaces; +using Canister.IoC.Attributes; using Microsoft.Extensions.DependencyInjection; namespace Canister.IoC.Example @@ -15,6 +16,14 @@ internal interface IMyService string Name { get; } } + /// + /// Interface that has all classes that implement it registered as singletons + /// + [RegisterAll(ServiceLifetime.Singleton)] + internal interface IRegisteredInterface + { + } + /// /// Example of how to use Canister /// @@ -35,6 +44,11 @@ private static void Main(string[] args) // Build the service provider ?.BuildServiceProvider(); + // Get all the services that implement IIRegisteredInterface + IEnumerable RegisteredClasses = ServiceProvider?.GetServices() ?? Array.Empty(); + // 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 ServiceClasses = ServiceProvider?.GetServices() ?? Array.Empty(); // Write out the number of services found (should be 2) @@ -94,6 +108,20 @@ internal class MyModule : IModule public void Load(IServiceCollection serviceDescriptors) => serviceDescriptors.AddTransient(); } + /// + /// Class that implements IRegisteredInterface + /// + internal class RegisteredClass1 : IRegisteredInterface + { + } + + /// + /// Class that implements IRegisteredInterface + /// + internal class RegisteredClass2 : IRegisteredInterface + { + } + /// /// This is a simple example class /// diff --git a/README.md b/README.md index 0950361..35f83cb 100644 --- a/README.md +++ b/README.md @@ -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. diff --git a/docfx_project/articles/intro.md b/docfx_project/articles/intro.md index 0b8cd91..9ee318e 100644 --- a/docfx_project/articles/intro.md +++ b/docfx_project/articles/intro.md @@ -4,6 +4,7 @@ # Output ``` +Number of registered classes found: 2 Number of services found: 2 ExampleService1 ExampleService2 diff --git a/src/Canister.IoC/Attributes/RegisterAllAttribute.cs b/src/Canister.IoC/Attributes/RegisterAllAttribute.cs new file mode 100644 index 0000000..6599587 --- /dev/null +++ b/src/Canister.IoC/Attributes/RegisterAllAttribute.cs @@ -0,0 +1,35 @@ +using Microsoft.Extensions.DependencyInjection; +using System; + +namespace Canister.IoC.Attributes +{ + /// + /// This attribute is used to register all items of a specific type with Canister and is used to + /// mark a type for registration. + /// + [AttributeUsage(AttributeTargets.Interface, AllowMultiple = false)] + public class RegisterAllAttribute : Attribute + { + /// + /// Constructor + /// + public RegisterAllAttribute() + { + Lifetime = ServiceLifetime.Transient; + } + + /// + /// Constructor + /// + /// Lifetime of the service + public RegisterAllAttribute(ServiceLifetime lifetime) + { + Lifetime = lifetime; + } + + /// + /// The lifetime of the service. + /// + public ServiceLifetime Lifetime { get; set; } + } +} \ No newline at end of file diff --git a/src/Canister.IoC/Attributes/RegisterAttribute.cs b/src/Canister.IoC/Attributes/RegisterAttribute.cs new file mode 100644 index 0000000..f116cb2 --- /dev/null +++ b/src/Canister.IoC/Attributes/RegisterAttribute.cs @@ -0,0 +1,41 @@ +using Microsoft.Extensions.DependencyInjection; +using System; + +namespace Canister.IoC.Attributes +{ + /// + /// This attribute is used to register a type with Canister and is used to mark a type for registration. + /// + [AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct, AllowMultiple = false)] + public class RegisterAttribute : Attribute + { + /// + /// Constructor + /// + public RegisterAttribute() + { + Lifetime = ServiceLifetime.Transient; + } + + /// + /// Constructor + /// + /// Lifetime of the service + /// The service key to register as (if any) + public RegisterAttribute(ServiceLifetime lifetime, object? serviceKey = null) + { + Lifetime = lifetime; + ServiceKey = serviceKey; + } + + /// + /// The lifetime of the service. + /// + public ServiceLifetime Lifetime { get; set; } + + /// + /// The service key to register as (if any). + /// + public object? ServiceKey { get; set; } + } +} \ No newline at end of file diff --git a/src/Canister.IoC/ExtensionMethods/ServiceCollectionExtensions.cs b/src/Canister.IoC/ExtensionMethods/ServiceCollectionExtensions.cs index c1ba607..33b3ebf 100644 --- a/src/Canister.IoC/ExtensionMethods/ServiceCollectionExtensions.cs +++ b/src/Canister.IoC/ExtensionMethods/ServiceCollectionExtensions.cs @@ -1,4 +1,5 @@ using Canister.Interfaces; +using Canister.IoC.Attributes; using Canister.IoC.Utils; using Fast.Activator; using Microsoft.Extensions.DependencyInjection.Extensions; @@ -35,6 +36,25 @@ private static Assembly[] Assemblies } } + /// + /// The available interfaces + /// + 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; + } + } + } + /// /// Gets the available types. /// @@ -70,6 +90,11 @@ private static Type[] AvailableTypes /// private static Assembly[]? _Assemblies; + /// + /// The available interfaces + /// + private static Type[]? _AvailableInterfaces; + /// /// The available types /// @@ -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(); //Clear info and return @@ -1036,6 +1069,32 @@ private static Assembly[] FindModules() /// The objects of a specific type. private static IEnumerable GetAllOfType() => AvailableTypes.Where(type => typeof(TObject).IsAssignableFrom(type)).Select(type => (TObject)FastActivator.CreateInstance(type)); + /// + /// Gets all attributes that have the RegisterAttribute. + /// + /// The list of types. + private static IEnumerable GetAllRegisteredTypes() => AvailableTypes.Where(type => type.GetCustomAttributes().Any()) + .Concat(AvailableInterfaces.Where(type => type.GetCustomAttributes().Any())); + + /// + /// Gets the available interfaces + /// + /// The interfaces that are available + private static Type[] GetAvailableInterfaces() + { + return Assemblies.SelectMany(x => + { + try + { + return x.GetTypes(); + } + catch (ReflectionTypeLoadException) { return Array.Empty(); } + }) + .Where(x => x.IsInterface + && !x.ContainsGenericParameters) + .ToArray(); + } + /// /// Gets the available types. /// @@ -1061,5 +1120,71 @@ private static Type[] GetAvailableTypes() /// /// The configuration. private static void LoadModules(ICanisterConfiguration configuration) => configuration.AddAssembly(Assemblies); + + /// + /// Registers the classes. + /// + /// Services + /// Registered type + private static void RegisterClasses(IServiceCollection? serviceDescriptors, Type registeredType) + { + if (serviceDescriptors is null) + return; + IEnumerable TypeChain = registeredType.GetInterfaces().Concat(new[] { registeredType }); + foreach (RegisterAttribute RegisterAttribute in registeredType.GetCustomAttributes()) + { + 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; + } + } + } + } + + /// + /// Registers the interfaces. + /// + /// The service descriptors. + /// The registered type. + private static void RegisterInterfaces(IServiceCollection? serviceDescriptors, Type registeredType) + { + if (serviceDescriptors is null) + return; + foreach (RegisterAllAttribute RegisterAllAttribute in registeredType.GetCustomAttributes()) + { + switch (RegisterAllAttribute.Lifetime) + { + case ServiceLifetime.Scoped: + _ = serviceDescriptors.AddAllScoped(registeredType); + break; + + case ServiceLifetime.Singleton: + _ = serviceDescriptors.AddAllSingleton(registeredType); + break; + + case ServiceLifetime.Transient: + _ = serviceDescriptors.AddAllTransient(registeredType); + break; + } + } + } } } \ No newline at end of file diff --git a/test/Canister.Tests/ExtensionMethods/ServiceCollectionAddByAttribute.cs b/test/Canister.Tests/ExtensionMethods/ServiceCollectionAddByAttribute.cs new file mode 100644 index 0000000..51258f3 --- /dev/null +++ b/test/Canister.Tests/ExtensionMethods/ServiceCollectionAddByAttribute.cs @@ -0,0 +1,59 @@ +using Canister.IoC.Attributes; +using Microsoft.Extensions.DependencyInjection; +using System; +using System.Collections.Generic; +using System.Linq; +using Xunit; + +namespace Canister.Tests.ExtensionMethods +{ + [RegisterAll(ServiceLifetime.Singleton)] + public interface IInterfaceToAdd + { + } + + public interface IInterfaceToAdd2 + { + } + + [Register(ServiceLifetime.Singleton)] + public class ClassToAdd : IInterfaceToAdd2 + { + } + + public class ClassToAdd2 : IInterfaceToAdd + { + } + + public class ClassToAdd3 : IInterfaceToAdd + { + } + + public class ClassToNotAdd + { + } + + public class ServiceCollectionAddByAttribute + { + [Fact] + public void ServiceCollectionAddByAttributeTest() + { + var services = new ServiceCollection(); + _ = services.AddCanisterModules(); + ServiceProvider provider = services.BuildServiceProvider(); + + IInterfaceToAdd2? instance = provider.GetService(); + Assert.NotNull(instance); + + IEnumerable Instances = provider.GetServices(); + Assert.NotNull(Instances); + Assert.Equal(2, Instances.Count()); + + ClassToAdd? instance3 = provider.GetService(); + Assert.NotNull(instance3); + + ClassToNotAdd? instance6 = provider.GetService(); + Assert.Null(instance6); + } + } +} \ No newline at end of file