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

Support for Decorator registration #27

Merged
merged 3 commits into from
Oct 11, 2024
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
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 @@

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 @@ -126,7 +129,7 @@
.GetMethods()
.FirstOrDefault(m => m.Name == "Configure" && m.GetParameters().Length == 2);

var specializedMethod = genericOptionsConfigureMethod.MakeGenericMethod(type);

Check warning on line 132 in src/Bindicate/Configuration/AutowiringBuilder.cs

View workflow job for this annotation

GitHub Actions / create_nuget

Dereference of a possibly null reference.

Check warning on line 132 in src/Bindicate/Configuration/AutowiringBuilder.cs

View workflow job for this annotation

GitHub Actions / run_test

Dereference of a possibly null reference.
specializedMethod.Invoke(null, new object[] { _services, configSection });
}
}
Expand Down Expand Up @@ -165,12 +168,111 @@
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);

Check warning on line 263 in src/Bindicate/Configuration/AutowiringBuilder.cs

View workflow job for this annotation

GitHub Actions / create_nuget

Possible null reference return.

Check warning on line 263 in src/Bindicate/Configuration/AutowiringBuilder.cs

View workflow job for this annotation

GitHub Actions / run_test

Possible null reference return.
},
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;
}
}
Loading