Skip to content

Commit

Permalink
Support for Decorator registration
Browse files Browse the repository at this point in the history
Support for Decorator registration
  • Loading branch information
Tim-Maes authored Oct 11, 2024
2 parents 71dcfec + 718a52d commit 7b781da
Show file tree
Hide file tree
Showing 5 changed files with 240 additions and 7 deletions.
98 changes: 98 additions & 0 deletions src/Bindicate.Tests/Decorator/RegisterDecoratorAttributeTests.cs
Original file line number Diff line number Diff line change
@@ -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<IOperation>();

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<LoggingOperationDecorator>();
}
}

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();
}
}
}
31 changes: 31 additions & 0 deletions src/Bindicate/Attributes/Decorator/RegisterDecoratorAttribute.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
namespace Bindicate.Attributes;

/// <summary>
/// Specifies that a class is a decorator for a specified service type.
/// Decorators are applied in the order specified by the <see cref="Order"/> property.
/// </summary>
[AttributeUsage(AttributeTargets.Class, AllowMultiple = true)]
public class RegisterDecoratorAttribute : Attribute
{
/// <summary>
/// Gets the type of the service to be decorated.
/// </summary>
public Type ServiceType { get; }

/// <summary>
/// Gets the order in which the decorator should be applied.
/// Lower values are applied first.
/// </summary>
public int Order { get; }

/// <summary>
/// Initializes a new instance of the <see cref="RegisterDecoratorAttribute"/> class.
/// </summary>
/// <param name="serviceType">The type of the service to be decorated.</param>
/// <param name="order">The order in which the decorator should be applied. Lower values are applied first.</param>
public RegisterDecoratorAttribute(Type serviceType, int order = 0)
{
ServiceType = serviceType;
Order = order;
}
}
4 changes: 2 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>Add support for IOptions</PackageReleaseNotes>
<PackageReleaseNotes>Add support for Decorators</PackageReleaseNotes>
<PackageLicenseFile>LICENSE.txt</PackageLicenseFile>
<Version>1.5.1</Version>
<Version>1.6</Version>
</PropertyGroup>

<ItemGroup>
Expand Down
110 changes: 106 additions & 4 deletions src/Bindicate/Configuration/AutowiringBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -25,23 +25,26 @@ public AutowiringBuilder(IServiceCollection services, Assembly targetAssembly)

AddAutowiringForAssembly();
}

private List<TypeMetadata> ScanAssembly(Assembly assembly)
{
var typeMetadatas = new List<TypeMetadata>();

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;
}


/// <summary>
/// Scans the assembly to automatically wire up services based on the attributes.
/// </summary>
Expand Down Expand Up @@ -165,12 +168,111 @@ public AutowiringBuilder ForKeyedServices()
return this;
}

/// <summary>
/// Scans the assembly to automatically wire up decorators based on the attributes.
/// </summary>
/// <returns>A reference to this instance after the operation has completed.</returns>
public AutowiringBuilder AddDecorators()
{
var decoratorsByService = _typeMetadatas
.Where(tm => tm.HasDecoratorAttribute)
.SelectMany(tm => tm.Type.GetCustomAttributes<RegisterDecoratorAttribute>()
.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<IServiceProvider, object> 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
);
}

/// <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()
{
AddDecorators();
return _services;
}
private static Action<Type, object, Type> GetKeyedRegistrationMethod(IServiceCollection services, BaseKeyedServiceAttribute attr)
Expand Down
4 changes: 3 additions & 1 deletion src/Bindicate/Configuration/TypeMetadata.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
}

0 comments on commit 7b781da

Please sign in to comment.