From e19bcaede5d50724ad3139b08e21a22722e8f771 Mon Sep 17 00:00:00 2001 From: Tim Maes Date: Fri, 11 Oct 2024 14:06:17 +0200 Subject: [PATCH 1/2] Add support for decorators --- .../RegisterDecoratorAttributeTests.cs | 98 ++++++++++++++++ .../Decorator/RegisterDecoratorAttribute.cs | 31 +++++ .../Configuration/AutowiringBuilder.cs | 110 +++++++++++++++++- src/Bindicate/Configuration/TypeMetadata.cs | 4 +- 4 files changed, 238 insertions(+), 5 deletions(-) create mode 100644 src/Bindicate.Tests/Decorator/RegisterDecoratorAttributeTests.cs create mode 100644 src/Bindicate/Attributes/Decorator/RegisterDecoratorAttribute.cs diff --git a/src/Bindicate.Tests/Decorator/RegisterDecoratorAttributeTests.cs b/src/Bindicate.Tests/Decorator/RegisterDecoratorAttributeTests.cs new file mode 100644 index 0000000..bed9518 --- /dev/null +++ b/src/Bindicate.Tests/Decorator/RegisterDecoratorAttributeTests.cs @@ -0,0 +1,98 @@ +using Bindicate.Attributes; +using Microsoft.Extensions.DependencyInjection; + +namespace Bindicate.Tests.Decorator; + +public class RegisterDecoratorAttributeTests +{ + public interface IOperation + { + int Perform(int a, int b); + } + + [AddSingleton(typeof(IOperation))] + public class Operation : IOperation + { + public int Perform(int a, int b) + { + return a + b; + } + } + + [RegisterDecorator(typeof(IOperation))] + public class LoggingOperationDecorator : IOperation + { + private readonly IOperation _innerOperation; + + public LoggingOperationDecorator(IOperation innerOperation) + { + _innerOperation = innerOperation; + } + + public int Perform(int a, int b) + { + Console.WriteLine($"Logging before operation: {a}, {b}"); + var result = _innerOperation.Perform(a, b); + Console.WriteLine($"Logging after operation: result {result}"); + return result; + } + } + + // Test class to verify the decorator implementation using xUnit and FluentAssertions + public class DecoratorTests + { + private readonly IServiceProvider _serviceProvider; + + public DecoratorTests() + { + var services = new ServiceCollection(); + + // Use the AutowiringBuilder to register services and decorators + services.AddAutowiringForAssembly(typeof(IOperation).Assembly) + .Register(); + + _serviceProvider = services.BuildServiceProvider(); + } + + [Fact] + public void Operation_ShouldBeDecoratedWithLogging() + { + var operation = _serviceProvider.GetRequiredService(); + + using var consoleOutput = new ConsoleOutput(); + + var result = operation.Perform(5, 7); + + result.Should().Be(12); + + var expectedOutput = $"Logging before operation: 5, 7{Environment.NewLine}Logging after operation: result 12{Environment.NewLine}"; + consoleOutput.GetOutput().Should().Be(expectedOutput); + + operation.Should().BeOfType(); + } + } + + public class ConsoleOutput : IDisposable + { + private readonly StringWriter _stringWriter; + private readonly TextWriter _originalOutput; + + public ConsoleOutput() + { + _stringWriter = new StringWriter(); + _originalOutput = Console.Out; + Console.SetOut(_stringWriter); + } + + public string GetOutput() + { + return _stringWriter.ToString(); + } + + public void Dispose() + { + Console.SetOut(_originalOutput); + _stringWriter.Dispose(); + } + } +} \ No newline at end of file diff --git a/src/Bindicate/Attributes/Decorator/RegisterDecoratorAttribute.cs b/src/Bindicate/Attributes/Decorator/RegisterDecoratorAttribute.cs new file mode 100644 index 0000000..dff36ad --- /dev/null +++ b/src/Bindicate/Attributes/Decorator/RegisterDecoratorAttribute.cs @@ -0,0 +1,31 @@ +namespace Bindicate.Attributes; + +/// +/// Specifies that a class is a decorator for a specified service type. +/// Decorators are applied in the order specified by the property. +/// +[AttributeUsage(AttributeTargets.Class, AllowMultiple = true)] +public class RegisterDecoratorAttribute : Attribute +{ + /// + /// Gets the type of the service to be decorated. + /// + public Type ServiceType { get; } + + /// + /// Gets the order in which the decorator should be applied. + /// Lower values are applied first. + /// + public int Order { get; } + + /// + /// Initializes a new instance of the class. + /// + /// The type of the service to be decorated. + /// The order in which the decorator should be applied. Lower values are applied first. + public RegisterDecoratorAttribute(Type serviceType, int order = 0) + { + ServiceType = serviceType; + Order = order; + } +} diff --git a/src/Bindicate/Configuration/AutowiringBuilder.cs b/src/Bindicate/Configuration/AutowiringBuilder.cs index bf5cf7a..62fe978 100644 --- a/src/Bindicate/Configuration/AutowiringBuilder.cs +++ b/src/Bindicate/Configuration/AutowiringBuilder.cs @@ -25,23 +25,26 @@ public AutowiringBuilder(IServiceCollection services, Assembly targetAssembly) AddAutowiringForAssembly(); } + private List ScanAssembly(Assembly assembly) { var typeMetadatas = new List(); foreach (var type in assembly.GetTypes().Where(t => t.IsClass && !t.IsAbstract)) { - var hasRegisterOptionsAttribute = type.GetCustomAttributes(typeof(RegisterOptionsAttribute), false).Any(); - var hasBaseServiceAttribute = type.GetCustomAttributes(typeof(BaseServiceAttribute), false).Any(); - var hasBaseKeyedServiceAttribute = type.GetCustomAttributes(typeof(BaseKeyedServiceAttribute), false).Any(); + var hasRegisterOptionsAttribute = type.IsDefined(typeof(RegisterOptionsAttribute), false); + var hasBaseServiceAttribute = type.IsDefined(typeof(BaseServiceAttribute), false); + var hasBaseKeyedServiceAttribute = type.IsDefined(typeof(BaseKeyedServiceAttribute), false); + var hasDecoratorAttribute = type.IsDefined(typeof(RegisterDecoratorAttribute), false); - var typeMetadata = new TypeMetadata(type, hasRegisterOptionsAttribute, hasBaseServiceAttribute, hasBaseKeyedServiceAttribute); + var typeMetadata = new TypeMetadata(type, hasRegisterOptionsAttribute, hasBaseServiceAttribute, hasBaseKeyedServiceAttribute, hasDecoratorAttribute); typeMetadatas.Add(typeMetadata); } return typeMetadatas; } + /// /// Scans the assembly to automatically wire up services based on the attributes. /// @@ -165,12 +168,111 @@ public AutowiringBuilder ForKeyedServices() return this; } + /// + /// Scans the assembly to automatically wire up decorators based on the attributes. + /// + /// A reference to this instance after the operation has completed. + public AutowiringBuilder AddDecorators() + { + var decoratorsByService = _typeMetadatas + .Where(tm => tm.HasDecoratorAttribute) + .SelectMany(tm => tm.Type.GetCustomAttributes() + .Select(attr => new + { + ServiceType = attr.ServiceType, + Order = attr.Order, + DecoratorType = tm.Type + })) + .GroupBy(x => x.ServiceType) + .ToDictionary( + g => g.Key, + g => g.OrderBy(x => x.Order).Select(x => x.DecoratorType).ToList() + ); + + foreach (var kvp in decoratorsByService) + { + var serviceType = kvp.Key; + var decoratorTypes = kvp.Value; + + var originalDescriptorIndex = _services + .Select((sd, index) => new { sd, index }) + .FirstOrDefault(sd => sd.sd.ServiceType == serviceType); + + if (originalDescriptorIndex == null) + { + throw new InvalidOperationException($"Service type {serviceType.FullName} is not registered."); + } + + _services.RemoveAt(originalDescriptorIndex.index); + + ServiceDescriptor currentDescriptor = originalDescriptorIndex.sd; + + foreach (var decoratorType in decoratorTypes) + { + currentDescriptor = CreateDecoratorDescriptor(serviceType, decoratorType, currentDescriptor); + } + + _services.Insert(originalDescriptorIndex.index, currentDescriptor); + } + + return this; + } + + private ServiceDescriptor CreateDecoratorDescriptor(Type serviceType, Type decoratorType, ServiceDescriptor previousDescriptor) + { + Func previousImplementationFactory; + + if (previousDescriptor.ImplementationFactory != null) + { + previousImplementationFactory = previousDescriptor.ImplementationFactory; + } + else if (previousDescriptor.ImplementationInstance != null) + { + var instance = previousDescriptor.ImplementationInstance; + previousImplementationFactory = provider => instance; + } + else if (previousDescriptor.ImplementationType != null) + { + var implementationType = previousDescriptor.ImplementationType; + previousImplementationFactory = provider => ActivatorUtilities.CreateInstance(provider, implementationType); + } + else + { + throw new InvalidOperationException("Unsupported service descriptor."); + } + + return ServiceDescriptor.Describe( + serviceType, + provider => + { + var decoratorConstructor = decoratorType.GetConstructors().First(); + var parameters = decoratorConstructor.GetParameters(); + + var args = parameters.Select(p => + { + if (p.ParameterType == serviceType) + { + return previousImplementationFactory(provider); + } + else + { + return provider.GetService(p.ParameterType); + } + }).ToArray(); + + return Activator.CreateInstance(decoratorType, args); + }, + previousDescriptor.Lifetime + ); + } + /// /// Registers all configured services and options into the IServiceCollection. /// /// The IServiceCollection that services and options were registered into. public IServiceCollection Register() { + AddDecorators(); return _services; } private static Action GetKeyedRegistrationMethod(IServiceCollection services, BaseKeyedServiceAttribute attr) diff --git a/src/Bindicate/Configuration/TypeMetadata.cs b/src/Bindicate/Configuration/TypeMetadata.cs index 48d5e81..cf3f567 100644 --- a/src/Bindicate/Configuration/TypeMetadata.cs +++ b/src/Bindicate/Configuration/TypeMetadata.cs @@ -6,12 +6,14 @@ public class TypeMetadata public bool HasRegisterOptionsAttribute { get; } public bool HasBaseServiceAttribute { get; } public bool HasBaseKeyedServiceAttribute { get; } + public bool HasDecoratorAttribute { get; } - public TypeMetadata(Type type, bool hasRegisterOptionsAttribute, bool hasBaseServiceAttribute, bool hasBaseKeyedServiceAttribute) + public TypeMetadata(Type type, bool hasRegisterOptionsAttribute, bool hasBaseServiceAttribute, bool hasBaseKeyedServiceAttribute, bool hasDecoratorAttribute) { Type = type; HasRegisterOptionsAttribute = hasRegisterOptionsAttribute; HasBaseServiceAttribute = hasBaseServiceAttribute; HasBaseKeyedServiceAttribute = hasBaseKeyedServiceAttribute; + HasDecoratorAttribute = hasDecoratorAttribute; } } \ No newline at end of file From 718a52ddf7db4feccdc9a78cd8c55cb6707e45f6 Mon Sep 17 00:00:00 2001 From: Tim Maes Date: Fri, 11 Oct 2024 14:11:27 +0200 Subject: [PATCH 2/2] Update version --- src/Bindicate/Bindicate.csproj | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Bindicate/Bindicate.csproj b/src/Bindicate/Bindicate.csproj index 933c4f3..c660f22 100644 --- a/src/Bindicate/Bindicate.csproj +++ b/src/Bindicate/Bindicate.csproj @@ -11,9 +11,9 @@ README.md https://www.github.com/Tim-Maes/Bindicate di, ioc, service, collection, extensions, attribute - Add support for IOptions + Add support for Decorators LICENSE.txt - 1.5.1 + 1.6