From 292454aba4b9de211277ee015a6d3fa8a18249f3 Mon Sep 17 00:00:00 2001 From: jcambert Date: Mon, 7 Jul 2025 14:34:06 +0200 Subject: [PATCH] Add Keyed Service name support --- src/Scrutor/IImplementationTypeFilter.cs | 6 ++++ src/Scrutor/ImplementationTypeFilter.cs | 6 +++- src/Scrutor/LifetimeSelector.cs | 6 +++- src/Scrutor/Scrutor.csproj | 2 +- src/Scrutor/ServiceKeyAttribute.cs | 11 +++++++ test/Scrutor.Tests/ScanningTests.cs | 39 ++++++++++++++++++++++-- 6 files changed, 65 insertions(+), 5 deletions(-) create mode 100644 src/Scrutor/ServiceKeyAttribute.cs diff --git a/src/Scrutor/IImplementationTypeFilter.cs b/src/Scrutor/IImplementationTypeFilter.cs index 15458765..d176ede3 100644 --- a/src/Scrutor/IImplementationTypeFilter.cs +++ b/src/Scrutor/IImplementationTypeFilter.cs @@ -32,6 +32,12 @@ public interface IImplementationTypeFilter : IFluentInterface /// If the argument is null. IImplementationTypeFilter AssignableToAny(IEnumerable types); + /// + /// Will match all types that are assignable to ServiceKeyAttribute />. + /// + /// + IImplementationTypeFilter WithKeyName(); + /// /// Will match all types that has an attribute of type defined. /// diff --git a/src/Scrutor/ImplementationTypeFilter.cs b/src/Scrutor/ImplementationTypeFilter.cs index 5afe35d4..2da68c98 100644 --- a/src/Scrutor/ImplementationTypeFilter.cs +++ b/src/Scrutor/ImplementationTypeFilter.cs @@ -42,6 +42,10 @@ public IImplementationTypeFilter AssignableToAny(IEnumerable types) return Where(t => types.Any(t.IsBasedOn)); } + + public IImplementationTypeFilter WithKeyName() + => WithAttribute(); + public IImplementationTypeFilter WithAttribute() where T : Attribute { return WithAttribute(typeof(T)); @@ -52,7 +56,7 @@ public IImplementationTypeFilter WithAttribute(Type attributeType) Preconditions.NotNull(attributeType, nameof(attributeType)); IncludeCompilerGeneratedTypes |= attributeType == typeof(CompilerGeneratedAttribute); - return Where(t => t.HasAttribute(attributeType)); + return Where(t => t.HasAttribute(attributeType) ); } public IImplementationTypeFilter WithAttribute(Func predicate) where T : Attribute diff --git a/src/Scrutor/LifetimeSelector.cs b/src/Scrutor/LifetimeSelector.cs index 595ab23d..cd183e43 100644 --- a/src/Scrutor/LifetimeSelector.cs +++ b/src/Scrutor/LifetimeSelector.cs @@ -245,7 +245,11 @@ void ISelector.Populate(IServiceCollection services, RegistrationStrategy? strat var lifetime = GetOrAddLifetime(lifetimes, implementationType); - var descriptor = new ServiceDescriptor(serviceType, implementationType, lifetime); + ServiceKeyAttribute? attr = implementationType.GetCustomAttribute(); + + ServiceDescriptor descriptor = attr != null + ? new ServiceDescriptor(serviceType, attr.Name, implementationType, lifetime) + : new ServiceDescriptor(serviceType, implementationType, lifetime); strategy.Apply(services, descriptor); } diff --git a/src/Scrutor/Scrutor.csproj b/src/Scrutor/Scrutor.csproj index b43c51b4..bd4f35fb 100644 --- a/src/Scrutor/Scrutor.csproj +++ b/src/Scrutor/Scrutor.csproj @@ -7,7 +7,7 @@ $(NoWarn);CS1591 latest enable - true + false true Scrutor Dependency;Injection;DI;Scanning;Conventions;Decoration diff --git a/src/Scrutor/ServiceKeyAttribute.cs b/src/Scrutor/ServiceKeyAttribute.cs new file mode 100644 index 00000000..e45afd00 --- /dev/null +++ b/src/Scrutor/ServiceKeyAttribute.cs @@ -0,0 +1,11 @@ +using JetBrains.Annotations; +using System; + +namespace Scrutor; + +[PublicAPI] +[AttributeUsage(AttributeTargets.Class, AllowMultiple = false)] +public sealed class ServiceKeyAttribute(string name): Attribute +{ + public string Name { get; } = name ?? throw new ArgumentNullException(nameof(name)); +} diff --git a/test/Scrutor.Tests/ScanningTests.cs b/test/Scrutor.Tests/ScanningTests.cs index 4dcbbf2e..c523e7bb 100644 --- a/test/Scrutor.Tests/ScanningTests.cs +++ b/test/Scrutor.Tests/ScanningTests.cs @@ -584,10 +584,35 @@ public void ShouldAllowOptInToCompilerGeneratedTypes() .AsSelf() .WithTransientLifetime()); }); - + var compilerGeneratedSubclass = provider.GetService(); Assert.NotNull(compilerGeneratedSubclass); } + + + [Fact] + public void KeyedTransientService() + { + ServiceProvider provider = ConfigureProvider(services => + { + services.Scan(scan => scan + .FromAssemblyOf() + .AddClasses(classes => classes.AssignableTo()) + .AsImplementedInterfaces() + .WithTransientLifetime()); + + var namedServices = services.GetDescriptors(); + + Assert.Equal(2, namedServices.Count(x => x.ServiceType == typeof(INamedService))); + }); + + var transientKeyedService1 = provider.GetKeyedService("t1"); + Assert.NotNull(transientKeyedService1); + + var transientKeyedService2 = provider.GetKeyedService("t1"); + Assert.NotNull(transientKeyedService2); + + } } // ReSharper disable UnusedTypeParameter @@ -597,6 +622,7 @@ public interface ITransientService { } [ServiceDescriptor(typeof(ITransientService))] public class TransientService1 : ITransientService { } + public class TransientService2 : ITransientService, IOtherInheritance { } public class TransientService : ITransientService, IEnumerable @@ -671,7 +697,7 @@ public class DefaultAttributes : IDefault3Level2, IDefault1, IDefault2 { } [CompilerGenerated] public class CompilerGenerated { } - public class CombinedService2: IDefault1, IDefault2, IDefault3Level2 { } + public class CombinedService2 : IDefault1, IDefault2, IDefault3Level2 { } public interface IGenericAttribute { } @@ -688,6 +714,15 @@ public abstract class AllowedCompilerGeneratedBase { } [CompilerGenerated] public class AllowedCompilerGeneratedSubclass : AllowedCompilerGeneratedBase { } + + + public interface INamedService { } + + [ServiceKey("t1")] + public class NameService1 : INamedService { } + + [ServiceKey("t2")] + public class NameService2 : INamedService { } } namespace Scrutor.Tests.ChildNamespace