From 652e0afd9f772b6f747a77a948b71774c2326dc2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=92=D0=B5=D1=80=D0=B1=D0=B8=D1=86=D0=BA=D0=B8=D0=B9=20?= =?UTF-8?q?=D0=A1=D0=B5=D1=80=D0=B3=D0=B5=D0=B9?= Date: Sat, 15 Nov 2025 02:11:52 +0300 Subject: [PATCH] Adding DecorateKeyed extension methods provided by isolated ServiceCollection. --- ...iceCollectionExtensions.KeyedDecoration.cs | 150 +++++++++++ test/Scrutor.Tests/KeyedDecorationTests.cs | 254 ++++++++++++++++++ test/Scrutor.Tests/Scrutor.Tests.csproj | 1 + 3 files changed, 405 insertions(+) create mode 100644 src/Scrutor/ServiceCollectionExtensions.KeyedDecoration.cs create mode 100644 test/Scrutor.Tests/KeyedDecorationTests.cs diff --git a/src/Scrutor/ServiceCollectionExtensions.KeyedDecoration.cs b/src/Scrutor/ServiceCollectionExtensions.KeyedDecoration.cs new file mode 100644 index 00000000..1f96174d --- /dev/null +++ b/src/Scrutor/ServiceCollectionExtensions.KeyedDecoration.cs @@ -0,0 +1,150 @@ +using System; +using System.Linq; + +namespace Microsoft.Extensions.DependencyInjection; + +public static partial class ServiceCollectionExtensions +{ + public static IServiceCollection DecorateKeyed(this IServiceCollection services, object serviceKey) where TDecorator : TService => + services.DecorateKeyed(typeof(TService), serviceKey, typeof(TDecorator)); + + public static IServiceCollection DecorateKeyed(this IServiceCollection services, Type serviceType, object serviceKey, Type decoratorType) => + services.DecoratedKeyedInternal( + serviceType, + serviceKey, + decoratorType: decoratorType, + decorator: null); + + + public static IServiceCollection DecorateKeyed(this IServiceCollection services, object serviceKey, Func decorator) where TService : notnull => + services.DecorateKeyed(serviceKey, (service, _) => decorator(service)); + + public static IServiceCollection DecorateKeyed(this IServiceCollection services, object serviceKey, Func decorator) where TService : notnull => + services.DecoratedKeyedInternal( + serviceType: typeof(TService), + serviceKey, + decoratorType: null, + decorator: (service, provider) => decorator((TService)service, provider)); + + + public static IServiceCollection DecorateKeyed(this IServiceCollection services, Type serviceType, object serviceKey, Func decorator) => + services.DecorateKeyed(serviceType, serviceKey, (decorated, _) => decorator(decorated)); + + public static IServiceCollection DecorateKeyed(this IServiceCollection services, Type serviceType, object serviceKey, Func decorator) => + services.DecoratedKeyedInternal( + serviceType, + serviceKey, + decoratorType: null, + decorator); + + + private static IServiceCollection DecoratedKeyedInternal(this IServiceCollection services, + Type serviceType, + object serviceKey, + Type? decoratorType, + Func? decorator) + { + if (services == null) + throw new ArgumentNullException(nameof(services)); + if (serviceType == null) + throw new ArgumentNullException(nameof(serviceType)); + if (serviceKey == null) + throw new ArgumentNullException(nameof(serviceKey)); + if (decoratorType == null && decorator == null) + throw new ArgumentException($"Either {nameof(decoratorType)} or {nameof(decorator)} must be specified"); + + //we get the only added service descriptor for the specified key. + ServiceDescriptor existingDescriptor = services.Single(descriptor => + descriptor.ServiceType == serviceType && + descriptor.ServiceKey != null && + descriptor.ServiceKey.Equals(serviceKey)); + if (!existingDescriptor.IsKeyedService) + throw new InvalidOperationException("Existing descriptor is not a keyed service descriptor."); + + //creating a new collection for decorating. + IServiceCollection decoratingServices = new ServiceCollection(); + + //adding an existing handle without a key for the possibility of decorating it. + if (existingDescriptor.KeyedImplementationType != null) + decoratingServices.Add(new ServiceDescriptor(serviceType, + implementationType: existingDescriptor.KeyedImplementationType, + lifetime: existingDescriptor.Lifetime)); + else if (existingDescriptor.KeyedImplementationInstance != null) + decoratingServices.Add(new ServiceDescriptor(serviceType, + instance: existingDescriptor.KeyedImplementationInstance)); + else if (existingDescriptor.KeyedImplementationFactory != null) + { + object serviceKeyLocal = serviceKey; + decoratingServices.Add(new ServiceDescriptor(serviceType, + factory: (serviceProvider) => existingDescriptor.KeyedImplementationFactory(serviceProvider, serviceKeyLocal), + lifetime: existingDescriptor.Lifetime)); + } + else + throw new InvalidOperationException("No implementation found in the existing service descriptor."); + + //we are decorating the service. + if (decoratorType != null) + decoratingServices.Decorate(serviceType, decoratorType); + else if (decorator != null) + decoratingServices.Decorate(serviceType, decorator); + + //deleting the existing handle. + int existingDescriptorIndex = services.IndexOf(existingDescriptor); + services.Remove(existingDescriptor); + + //getting a decorated service descriptors. + ServiceDescriptor[] decoratedDescriptors = decoratingServices.Where(descriptor => + descriptor.ServiceType == serviceType).ToArray(); + + //we process the decorated service and the decorator wrappers by key. + bool targetServiceAdded = false; + foreach (ServiceDescriptor decoratedDescriptor in decoratedDescriptors) + { + if (!decoratedDescriptor.IsKeyedService) + { + if (targetServiceAdded) + throw new InvalidOperationException("Decorated service has already been added."); + + //adding a decorated service using a key. + if (decoratedDescriptor.ImplementationType != null) + services.Insert(existingDescriptorIndex, new ServiceDescriptor(serviceType, + serviceKey: serviceKey, + implementationType: decoratedDescriptor.ImplementationType, + lifetime: decoratedDescriptor.Lifetime)); + else if (decoratedDescriptor.ImplementationFactory != null) + services.Insert(existingDescriptorIndex, new ServiceDescriptor(serviceType, + serviceKey: serviceKey, + factory: (serviceProvider, _) => decoratedDescriptor.ImplementationFactory(serviceProvider), + lifetime: decoratedDescriptor.Lifetime)); + else + throw new InvalidOperationException("No implementations in the target service descriptor decorator."); + + targetServiceAdded = true; + } + else + { + //adding substituted keyed descriptors for the source services. + if (decoratedDescriptor.KeyedImplementationType != null) + services.Insert(existingDescriptorIndex, new ServiceDescriptor(serviceType, + serviceKey: decoratedDescriptor.ServiceKey, + implementationType: decoratedDescriptor.KeyedImplementationType, + lifetime: decoratedDescriptor.Lifetime)); + else if (decoratedDescriptor.KeyedImplementationInstance != null) + services.Insert(existingDescriptorIndex, new ServiceDescriptor(serviceType, + serviceKey: decoratedDescriptor.ServiceKey, + instance: decoratedDescriptor.KeyedImplementationInstance)); + else if (decoratedDescriptor.KeyedImplementationFactory != null) + services.Insert(existingDescriptorIndex, new ServiceDescriptor(serviceType, + serviceKey: decoratedDescriptor.ServiceKey, + factory: decoratedDescriptor.KeyedImplementationFactory, + lifetime: decoratedDescriptor.Lifetime)); + else + throw new InvalidOperationException("No implementations in the intermediate service descriptor."); + } + + existingDescriptorIndex++; + } + + return services; + } +} diff --git a/test/Scrutor.Tests/KeyedDecorationTests.cs b/test/Scrutor.Tests/KeyedDecorationTests.cs new file mode 100644 index 00000000..23e05e8d --- /dev/null +++ b/test/Scrutor.Tests/KeyedDecorationTests.cs @@ -0,0 +1,254 @@ +using System; +using System.Linq; +using Microsoft.Extensions.DependencyInjection; +using Shouldly; +using Xunit; +// ReSharper disable ClassNeverInstantiated.Local + +namespace Scrutor.Tests; + +public class KeyedDecorationTests : TestBase +{ + private interface IMyService + { + } + + + private class MyDependency + { + } + + private class MyServiceImplementation : IMyService + { + public MyDependency Dependency { get; } + + public MyServiceImplementation(MyDependency dependency) + { + ArgumentNullException.ThrowIfNull(dependency); + Dependency = dependency; + } + } + + + private class MyDependencyToFirstDecorator + { + } + + private class MyServiceFirstDecorator : IMyService + { + public IMyService Decorated { get; } + public MyDependencyToFirstDecorator Dependency { get; } + + public MyServiceFirstDecorator(IMyService decorated, MyDependencyToFirstDecorator dependency) + { + ArgumentNullException.ThrowIfNull(decorated); + ArgumentNullException.ThrowIfNull(dependency); + Decorated = decorated; + Dependency = dependency; + } + } + + + private class MyDependencyToSecondDecorator + { + } + + private class MyServiceSecondDecorator : IMyService + { + public IMyService Decorated { get; } + public MyDependencyToSecondDecorator Dependency { get; } + + public MyServiceSecondDecorator(IMyService decorated, MyDependencyToSecondDecorator dependency) + { + ArgumentNullException.ThrowIfNull(decorated); + ArgumentNullException.ThrowIfNull(dependency); + Decorated = decorated; + Dependency = dependency; + } + } + + private class MyServiceThirdDecorator : IMyService + { + public IMyService Decorated { get; } + + public MyServiceThirdDecorator(IMyService decorated) + { + ArgumentNullException.ThrowIfNull(decorated); + Decorated = decorated; + } + } + + private const string serviceKey = "myServiceKey"; + + private static void CreateServiceCollection(IServiceCollection services) + { + services + .AddTransient() + .AddTransient() + .AddTransient(); + + services.AddKeyedTransient(serviceKey); + services.DecorateKeyed(serviceKey); + services.DecorateKeyed(serviceKey); + services.DecorateKeyed(serviceKey); + } + + [Fact] + public void Decorating_WithInvalidKeys_ShouldThrow() + { + Should.Throw(() => + { + ConfigureProvider(services => + { + services + .AddTransient() + .AddTransient() + .AddTransient(); + + services.AddKeyedScoped(serviceKey); + services.DecorateKeyed("anotherKey"); + }); + }); + } + + [Fact] + public void ServiceCollection_KeyedDescriptor_ShouldBeSingle() + { + ConfigureProvider(services => + { + CreateServiceCollection(services); + ServiceDescriptor[] myServices = services.Where(x => + x.ServiceType == typeof(IMyService) && + x.ServiceKey != null && + x.ServiceKey.Equals(serviceKey)).ToArray(); + myServices.Length.ShouldBe(1); + }); + } + + [Fact] + public void ServiceProvider_ShouldBuild() + { + Should.NotThrow(() => ConfigureProvider(CreateServiceCollection)); + } + + [Fact] + public void MyService_ShouldResolve() + { + IServiceProvider serviceProvider = ConfigureProvider(CreateServiceCollection); + Should.NotThrow(() => serviceProvider.GetRequiredKeyedService(serviceKey)); + } + + [Fact] + public void MyService_Hierarchy_ShouldByCorrect() + { + IServiceProvider serviceProvider = ConfigureProvider(CreateServiceCollection); + IMyService myService = serviceProvider.GetRequiredKeyedService(serviceKey); + myService.ShouldBeAssignableTo(); + + MyServiceThirdDecorator thirdDecorator = (MyServiceThirdDecorator)myService; + thirdDecorator.Decorated.ShouldBeAssignableTo(); + + MyServiceSecondDecorator secondDecorator = (MyServiceSecondDecorator)thirdDecorator.Decorated; + secondDecorator.Decorated.ShouldBeAssignableTo(); + + MyServiceFirstDecorator firstDecorator = (MyServiceFirstDecorator)secondDecorator.Decorated; + firstDecorator.Decorated.ShouldBeAssignableTo(); + } +} + +public class MultiKeyedDecorationTests : TestBase +{ + private interface IMyService + { + public string ServiceKey { get; } + } + + private class MyImpl : IMyService + { + public string ServiceKey { get; } + + public MyImpl(string serviceKey) + { + ArgumentNullException.ThrowIfNull(serviceKey); + ServiceKey = serviceKey; + } + } + + private class MyFirstDecorator : IMyService + { + private readonly IMyService _decorated; + + public MyFirstDecorator(IMyService decorated) + { + ArgumentNullException.ThrowIfNull(decorated); + _decorated = decorated; + } + + public string ServiceKey => _decorated.ServiceKey; + } + + private class MySecondDecorator : IMyService + { + private readonly IMyService _decorated; + + public MySecondDecorator(IMyService decorated) + { + ArgumentNullException.ThrowIfNull(decorated); + _decorated = decorated; + } + + public string ServiceKey => _decorated.ServiceKey; + } + + private const string myKey1 = "myKey1"; + private const string myKey2 = "myKey2"; + private const string myKey3 = "myKey3"; + + private static void CreateServiceCollection(IServiceCollection services) + { + services.AddKeyedTransient(myKey1, (sp, key) => new MyImpl((string)key!)); + services.DecorateKeyed(myKey1); + services.DecorateKeyed(myKey1); + + services.AddKeyedTransient(myKey2, (sp, key) => new MyImpl((string)key!)); + services.DecorateKeyed(myKey2); + services.DecorateKeyed(myKey2); + + services.AddKeyedTransient(myKey3, (sp, key) => new MyImpl((string)key!)); + services.DecorateKeyed(myKey3); + } + + [Fact] + public void ServiceProvider_ShouldBuild() + { + Should.NotThrow(() => ConfigureProvider(CreateServiceCollection)); + } + + [Fact] + public void KeyedServices_ShouldBeDecorated() + { + IServiceProvider serviceProvider = ConfigureProvider(CreateServiceCollection); + IMyService service1 = serviceProvider.GetRequiredKeyedService(myKey1); + IMyService service2 = serviceProvider.GetRequiredKeyedService(myKey2); + IMyService service3 = serviceProvider.GetRequiredKeyedService(myKey3); + + service1.ShouldBeAssignableTo(); + service2.ShouldBeAssignableTo(); + service3.ShouldBeAssignableTo(); + } + + [Fact] + public void KeyedServices_ShouldHaveDifferentKeys() + { + IServiceProvider serviceProvider = ConfigureProvider(CreateServiceCollection); + IMyService service1 = serviceProvider.GetRequiredKeyedService(myKey1); + IMyService service2 = serviceProvider.GetRequiredKeyedService(myKey2); + IMyService service3 = serviceProvider.GetRequiredKeyedService(myKey3); + + service1.ServiceKey.ShouldBe(myKey1); + service2.ServiceKey.ShouldBe(myKey2); + service3.ServiceKey.ShouldBe(myKey3); + + service1.ShouldNotBe(service2); + } +} diff --git a/test/Scrutor.Tests/Scrutor.Tests.csproj b/test/Scrutor.Tests/Scrutor.Tests.csproj index 6e958b51..363ea098 100644 --- a/test/Scrutor.Tests/Scrutor.Tests.csproj +++ b/test/Scrutor.Tests/Scrutor.Tests.csproj @@ -10,6 +10,7 @@ +