diff --git a/.github/SECURITY.md b/.github/SECURITY.md new file mode 100644 index 00000000..da9c516d --- /dev/null +++ b/.github/SECURITY.md @@ -0,0 +1,5 @@ +## Security contact information + +To report a security vulnerability, please use the +[Tidelift security contact](https://tidelift.com/security). +Tidelift will coordinate the fix and disclosure. diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index e0a13f1e..db6546c4 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -8,6 +8,7 @@ jobs: if: "!contains(github.event.head_commit.message, '[skip ci]')" env: + CI: true DOTNET_SKIP_FIRST_TIME_EXPERIENCE: 1 DOTNET_CLI_TELEMETRY_OPTOUT: 1 DOTNET_NOLOGO: true @@ -16,14 +17,70 @@ jobs: - name: Checkout uses: actions/checkout@v4 - - name: Setup .NET 9 SDK + - name: Setup .NET 10 SDK uses: actions/setup-dotnet@v4 with: - dotnet-version: "9.x" + dotnet-version: "10.x" - - name: Test - run: dotnet test --collect:"XPlat Code Coverage" + - name: Install dotnet-coverage + run: dotnet tool install -g dotnet-coverage + + - name: Test with coverage + run: dotnet-coverage collect --output coverage.cobertura.xml --output-format cobertura "dotnet test" - name: Update codecov if: startsWith(github.repository, 'khellang/') - uses: codecov/codecov-action@v1 + uses: codecov/codecov-action@v5 + with: + files: coverage.cobertura.xml + token: ${{ secrets.CODECOV_TOKEN }} + fail_ci_if_error: false + + - name: Pack NuGet package + run: dotnet pack src/Scrutor/Scrutor.csproj --configuration Release --output ./artifacts + + - name: Upload NuGet package artifact + uses: actions/upload-artifact@v4 + with: + name: nuget-packages + path: | + ./artifacts/*.nupkg + ./artifacts/*.snupkg + + - name: Push to GitHub Packages + if: github.event_name == 'push' && startsWith(github.repository, 'khellang/') + run: dotnet nuget push ./artifacts/*.*nupkg --source https://nuget.pkg.github.com/${{ github.repository_owner }}/index.json --api-key ${{ secrets.GITHUB_TOKEN }} --skip-duplicate + + publish: + runs-on: ubuntu-latest + needs: build + if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/') && startsWith(github.repository, 'khellang/') + permissions: + id-token: write + contents: read + + env: + DOTNET_SKIP_FIRST_TIME_EXPERIENCE: 1 + DOTNET_CLI_TELEMETRY_OPTOUT: 1 + DOTNET_NOLOGO: true + + steps: + - name: Download NuGet package artifact + uses: actions/download-artifact@v4 + with: + name: nuget-packages + path: ./artifacts + + - name: Setup .NET 10 SDK + uses: actions/setup-dotnet@v4 + with: + dotnet-version: "10.x" + + - name: Authenticate to NuGet.org with OIDC + uses: NuGet/login@v1 + id: login + with: + user: khellang + + - name: Push to NuGet.org + run: dotnet nuget push ./artifacts/*.nupkg --api-key ${{steps.login.outputs.NUGET_API_KEY}} --source https://api.nuget.org/v3/index.json --skip-duplicate diff --git a/NuGet.Config b/NuGet.Config index dbf0180d..6bd4065a 100644 --- a/NuGet.Config +++ b/NuGet.Config @@ -3,6 +3,6 @@ - + diff --git a/README.md b/README.md index 7144c98e..48628160 100644 --- a/README.md +++ b/README.md @@ -66,6 +66,25 @@ collection.Scan(scan => scan .AsImplementedInterfaces()); ``` +#### Scanning compiled view (UI) types +By default, Scrutor excludes compiler-generated types from the `.AddClasses()` type filters. When loading views from a framework such as [Avalonia UI](https://avaloniaui.net/), we need to opt in to compiler-generated types, like this: + +```csharp +.AddClasses(classes => classes + // Opt-in to compiler-generated types + .WithAttribute() + // Optionally filter types to reduce number of service registrations. + .InNamespaces("MyApp.Desktop.Views") + .AssignableToAny( + typeof(Window), + typeof(UserControl) + ) + .AsSelf() + .WithSingletonLifetime() +``` + +With some UI frameworks, these compiler-generated views implement quite a few interfaces, so unless you need them, it's probably best to register these classes `.AsSelf()`; in other words, be very precise with your filters that accept compiler generated types. + ### Decoration ```csharp @@ -87,3 +106,11 @@ var serviceProvider = collection.BuildServiceProvider(); // OtherDecorator -> Decorator -> Decorated var instance = serviceProvider.GetRequiredService(); ``` + +## Sponsors + +[Entity Framework Extensions](https://entityframework-extensions.net/?utm_source=khellang&utm_medium=Scrutor) and [Dapper Plus](https://dapper-plus.net/?utm_source=khellang&utm_medium=Scrutor) are major sponsors and proud to contribute to the development of Scrutor. + +[![Entity Framework Extensions](https://raw.githubusercontent.com/khellang/khellang/refs/heads/master/.github/entity-framework-extensions-sponsor.png)](https://entityframework-extensions.net/bulk-insert?utm_source=khellang&utm_medium=Scrutor) + +[![Dapper Plus](https://raw.githubusercontent.com/khellang/khellang/refs/heads/master/.github/dapper-plus-sponsor.png)](https://dapper-plus.net/bulk-insert?utm_source=khellang&utm_medium=Scrutor) diff --git a/global.json b/global.json new file mode 100644 index 00000000..1d364c6a --- /dev/null +++ b/global.json @@ -0,0 +1,9 @@ +{ + "sdk": { + "version": "10.0.100", + "rollForward": "latestFeature" + }, + "test": { + "runner": "Microsoft.Testing.Platform" + } +} diff --git a/src/Scrutor/AttributeSelector.cs b/src/Scrutor/AttributeSelector.cs index 0026ded4..af3ee630 100644 --- a/src/Scrutor/AttributeSelector.cs +++ b/src/Scrutor/AttributeSelector.cs @@ -37,7 +37,9 @@ void ISelector.Populate(IServiceCollection services, RegistrationStrategy? regis foreach (var serviceType in serviceTypes) { - var descriptor = new ServiceDescriptor(serviceType, type, attribute.Lifetime); + var descriptor = attribute.ServiceKey is null + ? new ServiceDescriptor(serviceType, type, attribute.Lifetime) + : new ServiceDescriptor(serviceType, attribute.ServiceKey, type, attribute.Lifetime); strategy.Apply(services, descriptor); } @@ -47,6 +49,6 @@ void ISelector.Populate(IServiceCollection services, RegistrationStrategy? regis private static IEnumerable GetDuplicates(IEnumerable attributes) { - return attributes.GroupBy(s => s.ServiceType).SelectMany(grp => grp.Skip(1)); + return attributes.GroupBy(s => new { s.ServiceType, s.ServiceKey }).SelectMany(grp => grp.Skip(1)); } } diff --git a/src/Scrutor/DecoratedService.cs b/src/Scrutor/DecoratedService.cs new file mode 100644 index 00000000..9b86bdf8 --- /dev/null +++ b/src/Scrutor/DecoratedService.cs @@ -0,0 +1,26 @@ +using System; +using System.Collections.Generic; + +namespace Scrutor; + +/// +/// A handle to a decorated service. This can be used to resolve decorated services from an +/// using . +/// +/// The type of services which were decorated. +public sealed class DecoratedService +{ + internal DecoratedService(Type serviceType, IReadOnlyList serviceKeys) + { + if (!typeof(TService).IsAssignableFrom(serviceType)) + throw new ArgumentException($"The type {serviceType} is not assignable to the service type {typeof(TService)}"); + + ServiceType = serviceType; + ServiceKeys = serviceKeys; + } + + internal Type ServiceType { get; } + internal IReadOnlyList ServiceKeys { get; } // In descending order of precedence + + internal DecoratedService Downcast() => new(ServiceType, ServiceKeys); +} diff --git a/src/Scrutor/ILifetimeSelector.cs b/src/Scrutor/ILifetimeSelector.cs index d473110b..9bab980d 100644 --- a/src/Scrutor/ILifetimeSelector.cs +++ b/src/Scrutor/ILifetimeSelector.cs @@ -1,5 +1,5 @@ -using System; -using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection; +using System; namespace Scrutor; @@ -29,4 +29,16 @@ public interface ILifetimeSelector : IServiceTypeSelector /// Registers each matching concrete type with a lifetime based on the provided . /// IImplementationTypeSelector WithLifetime(Func selector); + + /// + /// Registers each matching concrete type with the specified . + /// + /// The service key to use for registration. + ILifetimeSelector WithServiceKey(object serviceKey); + + /// + /// Registers each matching concrete type with a service key based on the provided . + /// + /// A function to determine the service key for each type. + ILifetimeSelector WithServiceKey(Func selector); } diff --git a/src/Scrutor/ImplementationTypeFilter.cs b/src/Scrutor/ImplementationTypeFilter.cs index 243e64b8..5afe35d4 100644 --- a/src/Scrutor/ImplementationTypeFilter.cs +++ b/src/Scrutor/ImplementationTypeFilter.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Runtime.CompilerServices; namespace Scrutor; @@ -13,6 +14,8 @@ public ImplementationTypeFilter(ISet types) internal ISet Types { get; private set; } + internal bool IncludeCompilerGeneratedTypes { get; private set; } + public IImplementationTypeFilter AssignableTo() { return AssignableTo(typeof(T)); @@ -48,6 +51,7 @@ public IImplementationTypeFilter WithAttribute(Type attributeType) { Preconditions.NotNull(attributeType, nameof(attributeType)); + IncludeCompilerGeneratedTypes |= attributeType == typeof(CompilerGeneratedAttribute); return Where(t => t.HasAttribute(attributeType)); } diff --git a/src/Scrutor/ImplementationTypeSelector.cs b/src/Scrutor/ImplementationTypeSelector.cs index 7da2c9ae..28c7b54a 100644 --- a/src/Scrutor/ImplementationTypeSelector.cs +++ b/src/Scrutor/ImplementationTypeSelector.cs @@ -3,6 +3,7 @@ using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Reflection; +using System.Runtime.CompilerServices; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyModel; @@ -29,7 +30,7 @@ public IServiceTypeSelector AddClasses() public IServiceTypeSelector AddClasses(bool publicOnly) { - var classes = GetNonAbstractClasses(publicOnly); + var classes = GetNonAbstractClasses(publicOnly, includeCompilerGenerated: false); return AddSelector(classes); } @@ -43,12 +44,15 @@ public IServiceTypeSelector AddClasses(Action action, { Preconditions.NotNull(action, nameof(action)); - var classes = GetNonAbstractClasses(publicOnly); + var classes = GetNonAbstractClasses(publicOnly, includeCompilerGenerated: true); var filter = new ImplementationTypeFilter(classes); action(filter); + if (!filter.IncludeCompilerGeneratedTypes) + filter.WithoutAttribute(); + return AddSelector(filter.Types); } @@ -144,8 +148,8 @@ private IServiceTypeSelector AddSelector(ISet types) return selector; } - private ISet GetNonAbstractClasses(bool publicOnly) + private ISet GetNonAbstractClasses(bool publicOnly, bool includeCompilerGenerated) { - return Types.Where(t => t.IsNonAbstractClass(publicOnly)).ToHashSet(); + return Types.Where(t => t.IsNonAbstractClass(publicOnly, includeCompilerGenerated)).ToHashSet(); } } diff --git a/src/Scrutor/LifetimeSelector.cs b/src/Scrutor/LifetimeSelector.cs index 595ab23d..3e07acd8 100644 --- a/src/Scrutor/LifetimeSelector.cs +++ b/src/Scrutor/LifetimeSelector.cs @@ -1,9 +1,9 @@ -using System; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyModel; +using System; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.Reflection; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.DependencyModel; namespace Scrutor; @@ -24,6 +24,8 @@ public LifetimeSelector(ServiceTypeSelector inner, IEnumerable typeMaps public Func? SelectorFn { get; set; } + public Func? ServiceKeySelectorFn { get; set; } + public IImplementationTypeSelector WithSingletonLifetime() { return WithLifetime(ServiceLifetime.Singleton); @@ -54,6 +56,22 @@ public IImplementationTypeSelector WithLifetime(Func sele return this; } + public ILifetimeSelector WithServiceKey(object serviceKey) + { + Preconditions.NotNull(serviceKey, nameof(serviceKey)); + + return WithServiceKey(_ => serviceKey); + } + + public ILifetimeSelector WithServiceKey(Func selector) + { + Preconditions.NotNull(selector, nameof(selector)); + + Inner.PropagateServiceKey(selector); + + return this; + } + #region Chain Methods [ExcludeFromCodeCoverage] @@ -231,6 +249,7 @@ void ISelector.Populate(IServiceCollection services, RegistrationStrategy? strat strategy ??= RegistrationStrategy.Append; var lifetimes = new Dictionary(); + var serviceKeys = new Dictionary(); foreach (var typeMap in TypeMaps) { @@ -244,8 +263,10 @@ void ISelector.Populate(IServiceCollection services, RegistrationStrategy? strat } var lifetime = GetOrAddLifetime(lifetimes, implementationType); - - var descriptor = new ServiceDescriptor(serviceType, implementationType, lifetime); + var serviceKey = GetOrAddServiceKey(serviceKeys, implementationType); + var descriptor = serviceKey is not null + ? new ServiceDescriptor(serviceType, serviceKey, implementationType, lifetime) + : new ServiceDescriptor(serviceType, implementationType, lifetime); strategy.Apply(services, descriptor); } @@ -256,8 +277,11 @@ void ISelector.Populate(IServiceCollection services, RegistrationStrategy? strat foreach (var serviceType in typeFactoryMap.ServiceTypes) { var lifetime = GetOrAddLifetime(lifetimes, typeFactoryMap.ImplementationType); + var serviceKey = GetOrAddServiceKey(serviceKeys, typeFactoryMap.ImplementationType); - var descriptor = new ServiceDescriptor(serviceType, typeFactoryMap.ImplementationFactory, lifetime); + var descriptor = serviceKey is not null + ? new ServiceDescriptor(serviceType, serviceKey, WrapImplementationFactory(typeFactoryMap.ImplementationFactory), lifetime) + : new ServiceDescriptor(serviceType, typeFactoryMap.ImplementationFactory, lifetime); strategy.Apply(services, descriptor); } @@ -277,4 +301,23 @@ private ServiceLifetime GetOrAddLifetime(Dictionary lifet return lifetime; } + + private object? GetOrAddServiceKey(Dictionary serviceKeys, Type implementationType) + { + if (serviceKeys.TryGetValue(implementationType, out var serviceKey)) + { + return serviceKey; + } + + serviceKey = ServiceKeySelectorFn?.Invoke(implementationType); + + serviceKeys[implementationType] = serviceKey; + + return serviceKey; + } + + private static Func WrapImplementationFactory(Func factory) + { + return (sp, _) => factory(sp); + } } diff --git a/src/Scrutor/MaybeNullWhenAttribute.cs b/src/Scrutor/MaybeNullWhenAttribute.cs new file mode 100644 index 00000000..6dac1a60 --- /dev/null +++ b/src/Scrutor/MaybeNullWhenAttribute.cs @@ -0,0 +1,19 @@ +#if NETFRAMEWORK + +using System; + +/// Specifies that when a method returns , the parameter will not be null even if the corresponding type allows it. +[AttributeUsage(AttributeTargets.Parameter, Inherited = false)] +internal sealed class NotNullWhenAttribute : Attribute +{ + /// Initializes the attribute with the specified return value condition. + /// + /// The return value condition. If the method returns this value, the associated parameter will not be null. + /// + public NotNullWhenAttribute(bool returnValue) => ReturnValue = returnValue; + + /// Gets the return value condition. + public bool ReturnValue { get; } +} + +#endif diff --git a/src/Scrutor/ReflectionExtensions.cs b/src/Scrutor/ReflectionExtensions.cs index e7dcde4d..5e83ebf3 100644 --- a/src/Scrutor/ReflectionExtensions.cs +++ b/src/Scrutor/ReflectionExtensions.cs @@ -9,7 +9,7 @@ namespace Scrutor; internal static class ReflectionExtensions { - public static bool IsNonAbstractClass(this Type type, bool publicOnly) + public static bool IsNonAbstractClass(this Type type, bool publicOnly, bool includeCompilerGenerated) { if (type.IsSpecialName) { @@ -18,7 +18,7 @@ public static bool IsNonAbstractClass(this Type type, bool publicOnly) if (type.IsClass && !type.IsAbstract) { - if (type.HasAttribute()) + if (!includeCompilerGenerated && type.HasAttribute()) { return false; } diff --git a/src/Scrutor/Scrutor.csproj b/src/Scrutor/Scrutor.csproj index a0a63185..6dbd7fa2 100644 --- a/src/Scrutor/Scrutor.csproj +++ b/src/Scrutor/Scrutor.csproj @@ -1,9 +1,9 @@  Register services using assembly scanning and a fluent API. - 6.0.1 + v Kristian Hellang - net462;netstandard2.0;net8.0 + net462;netstandard2.0;net8.0;net10.0 $(NoWarn);CS1591 latest enable @@ -25,6 +25,9 @@ snupkg 6.0.0.0 + + true + true @@ -32,10 +35,11 @@ + - - + + diff --git a/src/Scrutor/ServiceCollectionExtensions.Decoration.cs b/src/Scrutor/ServiceCollectionExtensions.Decoration.cs index 189ffad7..db6f3383 100644 --- a/src/Scrutor/ServiceCollectionExtensions.Decoration.cs +++ b/src/Scrutor/ServiceCollectionExtensions.Decoration.cs @@ -1,6 +1,8 @@ using Scrutor; using System; +using System.Diagnostics.CodeAnalysis; using JetBrains.Annotations; +using System.Collections.Generic; // ReSharper disable once CheckNamespace namespace Microsoft.Extensions.DependencyInjection; @@ -19,10 +21,206 @@ public static partial class ServiceCollectionExtensions /// If the argument is null. public static IServiceCollection Decorate(this IServiceCollection services) where TDecorator : TService + { + return services.Decorate(out _); + } + + /// + /// Decorates all registered services of type + /// using the specified type . + /// + /// The services to add to. + /// If the argument is null. + public static bool TryDecorate(this IServiceCollection services) + where TDecorator : TService + { + return services.TryDecorate(out _); + } + + /// + /// Decorates all registered services of the specified + /// using the specified . + /// + /// The services to add to. + /// The type of services to decorate. + /// The type to decorate existing services with. + /// If no service of the specified has been registered. + /// If either the , + /// or arguments are null. + public static IServiceCollection Decorate(this IServiceCollection services, Type serviceType, Type decoratorType) + { + return services.Decorate(serviceType, decoratorType, out _); + } + + /// + /// Decorates all registered services of the specified + /// using the specified . + /// + /// The services to add to. + /// The type of services to decorate. + /// The type to decorate existing services with. + /// If either the , + /// or arguments are null. + public static bool TryDecorate(this IServiceCollection services, Type serviceType, Type decoratorType) + { + return services.TryDecorate(serviceType, decoratorType, out _); + } + + /// + /// Decorates all registered services of type + /// using the function. + /// + /// The type of services to decorate. + /// The services to add to. + /// The decorator function. + /// If no service of has been registered. + /// If either the + /// or arguments are null. + public static IServiceCollection Decorate(this IServiceCollection services, Func decorator) where TService : notnull + { + return services.Decorate(decorator, out _); + } + + /// + /// Decorates all registered services of type + /// using the function. + /// + /// The type of services to decorate. + /// The services to add to. + /// The decorator function. + /// If either the + /// or arguments are null. + public static bool TryDecorate(this IServiceCollection services, Func decorator) where TService : notnull + { + return services.TryDecorate(decorator, out _); + } + + /// + /// Decorates all registered services of type + /// using the function. + /// + /// The type of services to decorate. + /// The services to add to. + /// The decorator function. + /// If no service of has been registered. + /// If either the + /// or arguments are null. + public static IServiceCollection Decorate(this IServiceCollection services, Func decorator) where TService : notnull + { + return services.Decorate(decorator, out _); + } + + /// + /// Decorates all registered services of type + /// using the function. + /// + /// The type of services to decorate. + /// The services to add to. + /// The decorator function. + /// If either the + /// or arguments are null. + public static bool TryDecorate(this IServiceCollection services, Func decorator) where TService : notnull + { + return services.TryDecorate(decorator, out _); + } + + /// + /// Decorates all registered services of the specified + /// using the function. + /// + /// The services to add to. + /// The type of services to decorate. + /// The decorator function. + /// If no service of the specified has been registered. + /// If either the , + /// or arguments are null. + public static IServiceCollection Decorate(this IServiceCollection services, Type serviceType, Func decorator) + { + return services.Decorate(serviceType, decorator, out _); + } + + /// + /// Decorates all registered services of the specified + /// using the function. + /// + /// The services to add to. + /// The type of services to decorate. + /// The decorator function. + /// If either the , + /// or arguments are null. + public static bool TryDecorate(this IServiceCollection services, Type serviceType, Func decorator) + { + return services.TryDecorate(serviceType, decorator, out _); + } + + /// + /// Decorates all registered services of the specified + /// using the function. + /// + /// The services to add to. + /// The type of services to decorate. + /// The decorator function. + /// If no service of the specified has been registered. + /// If either the , + /// or arguments are null. + public static IServiceCollection Decorate(this IServiceCollection services, Type serviceType, Func decorator) + { + return services.Decorate(serviceType, decorator, out _); + } + + /// + /// Decorates all registered services of the specified + /// using the function. + /// + /// The services to add to. + /// The type of services to decorate. + /// The decorator function. + /// If either the , + /// or arguments are null. + public static bool TryDecorate(this IServiceCollection services, Type serviceType, Func decorator) + { + return services.TryDecorate(serviceType, decorator, out _); + } + + /// + /// Decorates all registered services using the specified . + /// + /// The services to add to. + /// The strategy for decorating services. + /// If no registered service matched the specified . + public static IServiceCollection Decorate(this IServiceCollection services, DecorationStrategy strategy) + { + return services.Decorate(strategy, out _); + } + + /// + /// Decorates all registered services using the specified . + /// + /// The services to add to. + /// The strategy for decorating services. + public static bool TryDecorate(this IServiceCollection services, DecorationStrategy strategy) + { + return services.TryDecorate(strategy, out _); + } + + + /// + /// Decorates all registered services of type + /// using the specified type . + /// + /// The services to add to. + /// A handle to the service which was decorated. Using this, the service can be retrieved from the service provider via + /// . + /// If no service of the type has been registered. + /// If the argument is null. + public static IServiceCollection Decorate(this IServiceCollection services, out DecoratedService decorated) + where TDecorator : TService { Preconditions.NotNull(services, nameof(services)); - return services.Decorate(typeof(TService), typeof(TDecorator)); + services = services.Decorate(typeof(TService), typeof(TDecorator), out var decoratedObj); + decorated = decoratedObj.Downcast(); + return services; } /// @@ -30,13 +228,17 @@ public static IServiceCollection Decorate(this IServiceCol /// using the specified type . /// /// The services to add to. + /// A handle to the service which was decorated. Using this, the service can be retrieved from the service provider via + /// . /// If the argument is null. - public static bool TryDecorate(this IServiceCollection services) + public static bool TryDecorate(this IServiceCollection services, [NotNullWhen(true)] out DecoratedService? decorated) where TDecorator : TService { Preconditions.NotNull(services, nameof(services)); - return services.TryDecorate(typeof(TService), typeof(TDecorator)); + var success = services.TryDecorate(typeof(TService), typeof(TDecorator), out var decoratedObj); + decorated = success ? decoratedObj!.Downcast() : null; + return success; } /// @@ -46,16 +248,18 @@ public static bool TryDecorate(this IServiceCollection ser /// The services to add to. /// The type of services to decorate. /// The type to decorate existing services with. + /// A handle to the service which was decorated. Using this, the service can be retrieved from the service provider via + /// . /// If no service of the specified has been registered. /// If either the , /// or arguments are null. - public static IServiceCollection Decorate(this IServiceCollection services, Type serviceType, Type decoratorType) + public static IServiceCollection Decorate(this IServiceCollection services, Type serviceType, Type decoratorType, out DecoratedService decorated) { Preconditions.NotNull(services, nameof(services)); Preconditions.NotNull(serviceType, nameof(serviceType)); Preconditions.NotNull(decoratorType, nameof(decoratorType)); - return services.Decorate(DecorationStrategy.WithType(serviceType, serviceKey: null, decoratorType)); + return services.Decorate(DecorationStrategy.WithType(serviceType, serviceKey: null, decoratorType), out decorated); } /// @@ -65,15 +269,17 @@ public static IServiceCollection Decorate(this IServiceCollection services, Type /// The services to add to. /// The type of services to decorate. /// The type to decorate existing services with. + /// A handle to the service which was decorated. Using this, the service can be retrieved from the service provider via + /// . /// If either the , /// or arguments are null. - public static bool TryDecorate(this IServiceCollection services, Type serviceType, Type decoratorType) + public static bool TryDecorate(this IServiceCollection services, Type serviceType, Type decoratorType, [NotNullWhen(true)] out DecoratedService? decorated) { Preconditions.NotNull(services, nameof(services)); Preconditions.NotNull(serviceType, nameof(serviceType)); Preconditions.NotNull(decoratorType, nameof(decoratorType)); - return services.TryDecorate(DecorationStrategy.WithType(serviceType, serviceKey: null, decoratorType)); + return services.TryDecorate(DecorationStrategy.WithType(serviceType, serviceKey: null, decoratorType), out decorated); } /// @@ -83,15 +289,17 @@ public static bool TryDecorate(this IServiceCollection services, Type serviceTyp /// The type of services to decorate. /// The services to add to. /// The decorator function. + /// A handle to the service which was decorated. Using this, the service can be retrieved from the service provider via + /// . /// If no service of has been registered. /// If either the /// or arguments are null. - public static IServiceCollection Decorate(this IServiceCollection services, Func decorator) where TService : notnull + public static IServiceCollection Decorate(this IServiceCollection services, Func decorator, out DecoratedService decorated) where TService : notnull { Preconditions.NotNull(services, nameof(services)); Preconditions.NotNull(decorator, nameof(decorator)); - return services.Decorate((service, _) => decorator(service)); + return services.Decorate((service, _) => decorator(service), out decorated); } /// @@ -101,14 +309,16 @@ public static IServiceCollection Decorate(this IServiceCollection serv /// The type of services to decorate. /// The services to add to. /// The decorator function. + /// A handle to the service which was decorated. Using this, the service can be retrieved from the service provider via + /// . /// If either the /// or arguments are null. - public static bool TryDecorate(this IServiceCollection services, Func decorator) where TService : notnull + public static bool TryDecorate(this IServiceCollection services, Func decorator, [NotNullWhen(true)] out DecoratedService? decorated) where TService : notnull { Preconditions.NotNull(services, nameof(services)); Preconditions.NotNull(decorator, nameof(decorator)); - return services.TryDecorate((service, _) => decorator(service)); + return services.TryDecorate((service, _) => decorator(service), out decorated); } /// @@ -118,15 +328,19 @@ public static bool TryDecorate(this IServiceCollection services, Func< /// The type of services to decorate. /// The services to add to. /// The decorator function. + /// A handle to the service which was decorated. Using this, the service can be retrieved from the service provider via + /// . /// If no service of has been registered. /// If either the /// or arguments are null. - public static IServiceCollection Decorate(this IServiceCollection services, Func decorator) where TService : notnull + public static IServiceCollection Decorate(this IServiceCollection services, Func decorator, out DecoratedService decorated) where TService : notnull { Preconditions.NotNull(services, nameof(services)); Preconditions.NotNull(decorator, nameof(decorator)); - return services.Decorate(typeof(TService), (service, provider) => decorator((TService)service, provider)); + services = services.Decorate(typeof(TService), (service, provider) => decorator((TService)service, provider), out var decoratedObj); + decorated = decoratedObj.Downcast(); + return services; } /// @@ -136,14 +350,18 @@ public static IServiceCollection Decorate(this IServiceCollection serv /// The type of services to decorate. /// The services to add to. /// The decorator function. + /// A handle to the service which was decorated. Using this, the service can be retrieved from the service provider via + /// . /// If either the /// or arguments are null. - public static bool TryDecorate(this IServiceCollection services, Func decorator) where TService : notnull + public static bool TryDecorate(this IServiceCollection services, Func decorator, [NotNullWhen(true)] out DecoratedService? decorated) where TService : notnull { Preconditions.NotNull(services, nameof(services)); Preconditions.NotNull(decorator, nameof(decorator)); - return services.TryDecorate(typeof(TService), (service, provider) => decorator((TService)service, provider)); + var success = services.TryDecorate(typeof(TService), (service, provider) => decorator((TService)service, provider), out var decoratedObj); + decorated = success ? decoratedObj!.Downcast() : null; + return success; } /// @@ -153,16 +371,18 @@ public static bool TryDecorate(this IServiceCollection services, Func< /// The services to add to. /// The type of services to decorate. /// The decorator function. + /// A handle to the service which was decorated. Using this, the service can be retrieved from the service provider via + /// . /// If no service of the specified has been registered. /// If either the , /// or arguments are null. - public static IServiceCollection Decorate(this IServiceCollection services, Type serviceType, Func decorator) + public static IServiceCollection Decorate(this IServiceCollection services, Type serviceType, Func decorator, out DecoratedService decorated) { Preconditions.NotNull(services, nameof(services)); Preconditions.NotNull(serviceType, nameof(serviceType)); Preconditions.NotNull(decorator, nameof(decorator)); - return services.Decorate(serviceType, (decorated, _) => decorator(decorated)); + return services.Decorate(serviceType, (decorated, _) => decorator(decorated), out decorated); } /// @@ -172,15 +392,17 @@ public static IServiceCollection Decorate(this IServiceCollection services, Type /// The services to add to. /// The type of services to decorate. /// The decorator function. + /// A handle to the service which was decorated. Using this, the service can be retrieved from the service provider via + /// . /// If either the , /// or arguments are null. - public static bool TryDecorate(this IServiceCollection services, Type serviceType, Func decorator) + public static bool TryDecorate(this IServiceCollection services, Type serviceType, Func decorator, [NotNullWhen(true)] out DecoratedService? decorated) { Preconditions.NotNull(services, nameof(services)); Preconditions.NotNull(serviceType, nameof(serviceType)); Preconditions.NotNull(decorator, nameof(decorator)); - return services.TryDecorate(serviceType, (decorated, _) => decorator(decorated)); + return services.TryDecorate(serviceType, (decorated, _) => decorator(decorated), out decorated); } /// @@ -190,16 +412,18 @@ public static bool TryDecorate(this IServiceCollection services, Type serviceTyp /// The services to add to. /// The type of services to decorate. /// The decorator function. + /// A handle to the service which was decorated. Using this, the service can be retrieved from the service provider via + /// . /// If no service of the specified has been registered. /// If either the , /// or arguments are null. - public static IServiceCollection Decorate(this IServiceCollection services, Type serviceType, Func decorator) + public static IServiceCollection Decorate(this IServiceCollection services, Type serviceType, Func decorator, out DecoratedService decorated) { Preconditions.NotNull(services, nameof(services)); Preconditions.NotNull(serviceType, nameof(serviceType)); Preconditions.NotNull(decorator, nameof(decorator)); - return services.Decorate(DecorationStrategy.WithFactory(serviceType, serviceKey: null, decorator)); + return services.Decorate(DecorationStrategy.WithFactory(serviceType, serviceKey: null, decorator), out decorated); } /// @@ -209,15 +433,17 @@ public static IServiceCollection Decorate(this IServiceCollection services, Type /// The services to add to. /// The type of services to decorate. /// The decorator function. + /// A handle to the service which was decorated. Using this, the service can be retrieved from the service provider via + /// . /// If either the , /// or arguments are null. - public static bool TryDecorate(this IServiceCollection services, Type serviceType, Func decorator) + public static bool TryDecorate(this IServiceCollection services, Type serviceType, Func decorator, [NotNullWhen(true)] out DecoratedService? decorated) { Preconditions.NotNull(services, nameof(services)); Preconditions.NotNull(serviceType, nameof(serviceType)); Preconditions.NotNull(decorator, nameof(decorator)); - return services.TryDecorate(DecorationStrategy.WithFactory(serviceType, serviceKey: null, decorator)); + return services.TryDecorate(DecorationStrategy.WithFactory(serviceType, serviceKey: null, decorator), out decorated); } /// @@ -225,10 +451,12 @@ public static bool TryDecorate(this IServiceCollection services, Type serviceTyp /// /// The services to add to. /// The strategy for decorating services. + /// A handle to the service which was decorated. Using this, the service can be retrieved from the service provider via + /// . /// If no registered service matched the specified . - public static IServiceCollection Decorate(this IServiceCollection services, DecorationStrategy strategy) + public static IServiceCollection Decorate(this IServiceCollection services, DecorationStrategy strategy, out DecoratedService decorated) { - if (services.TryDecorate(strategy)) + if (services.TryDecorate(strategy, out decorated!)) { return services; } @@ -241,12 +469,14 @@ public static IServiceCollection Decorate(this IServiceCollection services, Deco /// /// The services to add to. /// The strategy for decorating services. - public static bool TryDecorate(this IServiceCollection services, DecorationStrategy strategy) + /// A handle to the service which was decorated. Using this, the service can be retrieved from the service provider via + /// . + public static bool TryDecorate(this IServiceCollection services, DecorationStrategy strategy, [NotNullWhen(true)] out DecoratedService? decorated) { Preconditions.NotNull(services, nameof(services)); Preconditions.NotNull(strategy, nameof(strategy)); - var decorated = false; + var decoratedKeys = new List(); for (var i = services.Count - 1; i >= 0; i--) { @@ -260,21 +490,23 @@ public static bool TryDecorate(this IServiceCollection services, DecorationStrat var serviceKey = GetDecoratorKey(serviceDescriptor); if (serviceKey is null) { + decorated = null; return false; } // Insert decorated services.Add(serviceDescriptor.WithServiceKey(serviceKey)); + decoratedKeys.Add(serviceKey); // Replace decorator services[i] = serviceDescriptor.WithImplementationFactory(strategy.CreateDecorator(serviceDescriptor.ServiceType, serviceKey)); - - decorated = true; } - return decorated; + decorated = new DecoratedService(strategy.ServiceType, decoratedKeys); + return decoratedKeys.Count > 0; } + /// /// Returns true if the specified service is decorated. /// diff --git a/src/Scrutor/ServiceDescriptorAttribute.cs b/src/Scrutor/ServiceDescriptorAttribute.cs index 4a613466..47e8965a 100644 --- a/src/Scrutor/ServiceDescriptorAttribute.cs +++ b/src/Scrutor/ServiceDescriptorAttribute.cs @@ -13,16 +13,21 @@ public ServiceDescriptorAttribute() : this(null) { } public ServiceDescriptorAttribute(Type? serviceType) : this(serviceType, ServiceLifetime.Transient) { } - public ServiceDescriptorAttribute(Type? serviceType, ServiceLifetime lifetime) + public ServiceDescriptorAttribute(Type? serviceType, ServiceLifetime lifetime) : this(serviceType, lifetime, null) { } + + public ServiceDescriptorAttribute(Type? serviceType, ServiceLifetime lifetime, object? serviceKey) { ServiceType = serviceType; Lifetime = lifetime; + ServiceKey = serviceKey; } public Type? ServiceType { get; } public ServiceLifetime Lifetime { get; } + public object? ServiceKey { get; } + public IEnumerable GetServiceTypes(Type fallbackType) { if (ServiceType is null) @@ -60,4 +65,8 @@ public sealed class ServiceDescriptorAttribute : ServiceDescriptorAttr public ServiceDescriptorAttribute() : base(typeof(TService)) { } public ServiceDescriptorAttribute(ServiceLifetime lifetime) : base(typeof(TService), lifetime) { } + + public ServiceDescriptorAttribute(object? serviceKey) : base(typeof(TService), ServiceLifetime.Transient, serviceKey) { } + + public ServiceDescriptorAttribute(ServiceLifetime lifetime, object? serviceKey) : base(typeof(TService), lifetime, serviceKey) { } } diff --git a/src/Scrutor/ServiceProviderExtensions.cs b/src/Scrutor/ServiceProviderExtensions.cs new file mode 100644 index 00000000..8e1b446d --- /dev/null +++ b/src/Scrutor/ServiceProviderExtensions.cs @@ -0,0 +1,35 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Microsoft.Extensions.DependencyInjection; + +namespace Scrutor; + +public static class ServiceProviderExtensions +{ + /// + /// Get all decorated services of type from the . + /// + /// The type of services which were decorated. + /// The to retrieve the service objects from. + /// A handle to a decorated service, obtainable from a + /// overload which omits one as an out parameter. + /// A service object of type . + public static IEnumerable GetDecoratedServices(this IServiceProvider serviceProvider, DecoratedService decoratedType) + { + return decoratedType.ServiceKeys.Reverse().Select(key => (TService)serviceProvider.GetRequiredKeyedService(decoratedType.ServiceType, key)); + } + + /// + /// Get decorated service of type from the . + /// + /// The type of service which was decorated. + /// The to retrieve the service object from. + /// A handle to a decorated service, obtainable from a + /// overload which omits one as an out parameter. + /// A service object of type . + public static TService GetRequiredDecoratedService(this IServiceProvider serviceProvider, DecoratedService decoratedType) + { + return (TService)serviceProvider.GetRequiredKeyedService(decoratedType.ServiceType, decoratedType.ServiceKeys[0]); + } +} diff --git a/src/Scrutor/ServiceTypeSelector.cs b/src/Scrutor/ServiceTypeSelector.cs index 4bed2357..c971e552 100644 --- a/src/Scrutor/ServiceTypeSelector.cs +++ b/src/Scrutor/ServiceTypeSelector.cs @@ -1,10 +1,10 @@ -using System; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyModel; +using System; using System.Collections; using System.Collections.Generic; using System.Linq; using System.Reflection; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.DependencyModel; namespace Scrutor; @@ -207,6 +207,14 @@ internal void PropagateLifetime(Func selectorFn) } } + internal void PropagateServiceKey(Func selectorFn) + { + foreach (var selector in Selectors.OfType()) + { + selector.ServiceKeySelectorFn = selectorFn; + } + } + void ISelector.Populate(IServiceCollection services, RegistrationStrategy? registrationStrategy) { if (Selectors.Count == 0) diff --git a/test/Scrutor.Tests/DecorationTests.cs b/test/Scrutor.Tests/DecorationTests.cs index dbbeaf0c..dc389c86 100644 --- a/test/Scrutor.Tests/DecorationTests.cs +++ b/test/Scrutor.Tests/DecorationTests.cs @@ -43,6 +43,33 @@ public void CanDecorateMultipleLevels() _ = Assert.IsType(innerDecorator.Inner); } + [Fact] + public void CanDecorateMultipleLevelsAndReturnDecoratedServiceHandles() + { + DecoratedService decorated1 = null; + DecoratedService decorated2 = null; + + var provider = ConfigureProvider(services => + { + services.AddSingleton(); + + services.Decorate(out decorated1); + services.Decorate(out decorated2); + }); + + var instance = provider.GetRequiredService(); + + var outerDecorator = Assert.IsType(instance); + var innerDecorator = Assert.IsType(outerDecorator.Inner); + _ = Assert.IsType(innerDecorator.Inner); + + var underlying1 = provider.GetRequiredDecoratedService(decorated1); + var underlying2 = provider.GetRequiredDecoratedService(decorated2); + + Assert.Equal(innerDecorator, underlying2); + Assert.Equal(innerDecorator.Inner, underlying1); + } + [Fact] public void CanDecorateDifferentServices() { @@ -62,6 +89,33 @@ public void CanDecorateDifferentServices() Assert.All(instances, x => Assert.IsType(x)); } + [Fact] + public void CanDecorateDifferentServicesAndReturnDecoratedServiceHandles() + { + DecoratedService decorated = null; + + var provider = ConfigureProvider(services => + { + services.AddSingleton(); + services.AddSingleton(); + + services.Decorate(out decorated); + }); + + var instances = provider + .GetRequiredService>() + .ToArray(); + + Assert.Equal(2, instances.Length); + Assert.All(instances, x => Assert.IsType(x)); + + var underlyings = provider.GetDecoratedServices(decorated).ToArray(); + + Assert.Equal(2, underlyings.Length); + Assert.IsType(underlyings[0]); + Assert.IsType(underlyings[1]); + } + [Fact] public void ShouldAddServiceKeyToExistingServiceDescriptor() { diff --git a/test/Scrutor.Tests/KeyedServiceTests.cs b/test/Scrutor.Tests/KeyedServiceTests.cs new file mode 100644 index 00000000..b28c003e --- /dev/null +++ b/test/Scrutor.Tests/KeyedServiceTests.cs @@ -0,0 +1,329 @@ +using Microsoft.Extensions.DependencyInjection; +using System; +using System.Linq; +using Xunit; + +namespace Scrutor.Tests; + +public class KeyedServiceTests : TestBase +{ + private IServiceCollection Collection { get; } = new ServiceCollection(); + + [Fact] + public void CanRegisterKeyedServiceWithStringKey() + { + Collection.Scan(scan => scan + .FromTypes(typeof(KeyedTransientService)) + .UsingAttributes()); + + Assert.Single(Collection); + + var service = Collection.Single(); + Assert.Equal(typeof(IKeyedTestService), service.ServiceType); + Assert.Equal(typeof(KeyedTransientService), service.KeyedImplementationType); + Assert.Equal("test-key", service.ServiceKey); + Assert.Equal(ServiceLifetime.Transient, service.Lifetime); + Assert.True(service.IsKeyedService); + } + + [Fact] + public void CanRegisterKeyedServiceWithGenericAttribute() + { + Collection.Scan(scan => scan + .FromTypes(typeof(GenericKeyedService)) + .UsingAttributes()); + + Assert.Single(Collection); + + var service = Collection.Single(); + Assert.Equal(typeof(IKeyedTestService), service.ServiceType); + Assert.Equal(typeof(GenericKeyedService), service.KeyedImplementationType); + Assert.Equal("generic-key", service.ServiceKey); + Assert.Equal(ServiceLifetime.Scoped, service.Lifetime); + Assert.True(service.IsKeyedService); + } + + [Fact] + public void CanRegisterMultipleKeyedServicesOnSameType() + { + Collection.Scan(scan => scan + .FromTypes(typeof(MultipleKeyedService)) + .UsingAttributes()); + + Assert.Equal(2, Collection.Count); + + var services = Collection.ToArray(); + + var service1 = services.First(s => s.ServiceKey?.ToString() == "key1"); + Assert.Equal(typeof(IKeyedTestService), service1.ServiceType); + Assert.Equal(typeof(MultipleKeyedService), service1.KeyedImplementationType); + Assert.Equal(ServiceLifetime.Transient, service1.Lifetime); + + var service2 = services.First(s => s.ServiceKey?.ToString() == "key2"); + Assert.Equal(typeof(IKeyedTestService), service2.ServiceType); + Assert.Equal(typeof(MultipleKeyedService), service2.KeyedImplementationType); + Assert.Equal(ServiceLifetime.Singleton, service2.Lifetime); + } + + [Fact] + public void CanRegisterMixedKeyedAndNonKeyedServices() + { + Collection.Scan(scan => scan + .FromTypes(typeof(MixedKeyedService)) + .UsingAttributes()); + + Assert.Equal(2, Collection.Count); + + var keyedService = Collection.First(s => s.IsKeyedService); + Assert.Equal(typeof(IKeyedTestService), keyedService.ServiceType); + Assert.Equal(typeof(MixedKeyedService), keyedService.KeyedImplementationType); + Assert.Equal("mixed-key", keyedService.ServiceKey); + Assert.Equal(ServiceLifetime.Scoped, keyedService.Lifetime); + + var nonKeyedService = Collection.First(s => !s.IsKeyedService); + Assert.Equal(typeof(IKeyedTestService), nonKeyedService.ServiceType); + Assert.Equal(typeof(MixedKeyedService), nonKeyedService.ImplementationType); + Assert.Null(nonKeyedService.ServiceKey); + Assert.Equal(ServiceLifetime.Transient, nonKeyedService.Lifetime); + } + + [Fact] + public void CanResolveKeyedServices() + { + var provider = ConfigureProvider(services => + { + services.Scan(scan => scan + .FromTypes(typeof(KeyedTransientService), typeof(GenericKeyedService), typeof(MultipleKeyedService)) + .UsingAttributes()); + }); + + var keyedTransient = provider.GetRequiredKeyedService("test-key"); + Assert.IsType(keyedTransient); + + using var scope = provider.CreateScope(); + var genericKeyed = scope.ServiceProvider.GetRequiredKeyedService("generic-key"); + Assert.IsType(genericKeyed); + + var multipleKeyed1 = provider.GetRequiredKeyedService("key1"); + Assert.IsType(multipleKeyed1); + + var multipleKeyed2 = provider.GetRequiredKeyedService("key2"); + Assert.IsType(multipleKeyed2); + + // Verify they are different instances for transient services + var anotherKeyedTransient = provider.GetRequiredKeyedService("test-key"); + Assert.NotSame(keyedTransient, anotherKeyedTransient); + + // Verify singleton behavior + var anotherMultipleKeyed2 = provider.GetRequiredKeyedService("key2"); + Assert.Same(multipleKeyed2, anotherMultipleKeyed2); + } + + [Fact] + public void KeyedServicesAreIsolatedFromNonKeyedServices() + { + var provider = ConfigureProvider(services => + { + services.Scan(scan => scan + .FromTypes(typeof(MixedKeyedService)) + .UsingAttributes()); + }); + + using var scope = provider.CreateScope(); + var keyedService = scope.ServiceProvider.GetRequiredKeyedService("mixed-key"); + var nonKeyedService = provider.GetRequiredService(); + + Assert.IsType(keyedService); + Assert.IsType(nonKeyedService); + Assert.NotSame(keyedService, nonKeyedService); + } + + [Fact] + public void CanRegisterKeyedServiceWithObjectKey() + { + Collection.Scan(scan => scan + .FromTypes(typeof(ObjectKeyedService)) + .UsingAttributes()); + + Assert.Single(Collection); + + var service = Collection.Single(); + Assert.Equal(typeof(IKeyedTestService), service.ServiceType); + Assert.Equal(typeof(ObjectKeyedService), service.KeyedImplementationType); + Assert.Equal(42, service.ServiceKey); + Assert.True(service.IsKeyedService); + } + + [Fact] + public void CanResolveKeyedServiceWithObjectKey() + { + var provider = ConfigureProvider(services => + { + services.Scan(scan => scan + .FromTypes(typeof(ObjectKeyedService)) + .UsingAttributes()); + }); + + var keyedService = provider.GetRequiredKeyedService(42); + Assert.IsType(keyedService); + } + + [Fact] + public void CanRegisterKeyedServiceWithEnumKey() + { + Collection.Scan(scan => scan + .FromTypes(typeof(EnumKeyedService)) + .UsingAttributes()); + + Assert.Single(Collection); + + var service = Collection.Single(); + Assert.Equal(typeof(IKeyedTestService), service.ServiceType); + Assert.Equal(typeof(EnumKeyedService), service.KeyedImplementationType); + Assert.Equal(TestEnum.Value1, service.ServiceKey); + Assert.True(service.IsKeyedService); + } + + [Fact] + public void CanRegisterKeyedServiceWithDifferentServiceTypes() + { + Collection.Scan(scan => scan + .FromTypes(typeof(MultiServiceKeyedService)) + .UsingAttributes()); + + Assert.Equal(2, Collection.Count); + + var keyedService = Collection.First(s => s.ServiceType == typeof(IKeyedTestService)); + Assert.Equal(typeof(MultiServiceKeyedService), keyedService.KeyedImplementationType); + Assert.Equal("service-key", keyedService.ServiceKey); + + var otherKeyedService = Collection.First(s => s.ServiceType == typeof(IOtherKeyedTestService)); + Assert.Equal(typeof(MultiServiceKeyedService), otherKeyedService.KeyedImplementationType); + Assert.Equal("other-key", otherKeyedService.ServiceKey); + } + + [Fact] + public void ThrowsWhenResolvingNonExistentKeyedService() + { + var provider = ConfigureProvider(services => + { + services.Scan(scan => scan + .FromTypes(typeof(KeyedTransientService)) + .UsingAttributes()); + }); + + Assert.Throws(() => + provider.GetRequiredKeyedService("non-existent-key")); + } + + [Fact] + public void CanRegisterKeyedServiceWithNullServiceType() + { + Collection.Scan(scan => scan + .FromTypes(typeof(KeyedServiceWithNullServiceType)) + .UsingAttributes()); + + // Should register for the implementation type and all its interfaces + Assert.Equal(2, Collection.Count); // IKeyedTestService and KeyedServiceWithNullServiceType itself + + var services = Collection.ToArray(); + Assert.All(services, s => + { + Assert.Equal("null-service-type-key", s.ServiceKey); + Assert.True(s.IsKeyedService); + }); + } + + + [Fact] + public void AllowsSameServiceTypeWithDifferentKeys() + { + Collection.Scan(scan => scan + .FromTypes(typeof(SameServiceTypeDifferentKeys)) + .UsingAttributes()); + + Assert.Equal(2, Collection.Count); + + var service1 = Collection.First(s => s.ServiceKey?.ToString() == "key1"); + var service2 = Collection.First(s => s.ServiceKey?.ToString() == "key2"); + + Assert.Equal(typeof(IKeyedTestService), service1.ServiceType); + Assert.Equal(typeof(IKeyedTestService), service2.ServiceType); + Assert.NotEqual(service1.ServiceKey, service2.ServiceKey); + } + + [Fact] + public void CanRegisterServiceWithNullKey() + { + Collection.Scan(scan => scan + .FromTypes(typeof(NullKeyedService)) + .UsingAttributes()); + + Assert.Single(Collection); + + var service = Collection.Single(); + Assert.Equal(typeof(IKeyedTestService), service.ServiceType); + Assert.Equal(typeof(NullKeyedService), service.ImplementationType); + Assert.Null(service.ServiceKey); + Assert.False(service.IsKeyedService); + } + + [Fact] + public void CanResolveServiceWithNullKey() + { + var provider = ConfigureProvider(services => + { + services.Scan(scan => scan + .FromTypes(typeof(NullKeyedService)) + .UsingAttributes()); + }); + + var service = provider.GetRequiredService(); + Assert.IsType(service); + } +} + +// Test interfaces and classes for keyed services +public interface IKeyedTestService { } +public interface IOtherKeyedTestService { } + +[ServiceDescriptor(typeof(IKeyedTestService), ServiceLifetime.Transient, "test-key")] +public class KeyedTransientService : IKeyedTestService { } + +[ServiceDescriptor(ServiceLifetime.Scoped, "generic-key")] +public class GenericKeyedService : IKeyedTestService { } + +[ServiceDescriptor(typeof(IKeyedTestService), ServiceLifetime.Transient, "key1")] +[ServiceDescriptor(typeof(IKeyedTestService), ServiceLifetime.Singleton, "key2")] +public class MultipleKeyedService : IKeyedTestService { } + +[ServiceDescriptor(typeof(IKeyedTestService), ServiceLifetime.Scoped, "mixed-key")] +[ServiceDescriptor(typeof(IKeyedTestService), ServiceLifetime.Transient)] +public class MixedKeyedService : IKeyedTestService { } + +[ServiceDescriptor(typeof(IKeyedTestService), ServiceLifetime.Transient, 42)] +public class ObjectKeyedService : IKeyedTestService { } + +public enum TestEnum +{ + Value1, + Value2 +} + +[ServiceDescriptor(typeof(IKeyedTestService), ServiceLifetime.Transient, TestEnum.Value1)] +public class EnumKeyedService : IKeyedTestService { } + +[ServiceDescriptor(typeof(IKeyedTestService), ServiceLifetime.Transient, "service-key")] +[ServiceDescriptor(typeof(IOtherKeyedTestService), ServiceLifetime.Scoped, "other-key")] +public class MultiServiceKeyedService : IKeyedTestService, IOtherKeyedTestService { } + +[ServiceDescriptor(null, ServiceLifetime.Transient, "null-service-type-key")] +public class KeyedServiceWithNullServiceType : IKeyedTestService { } + + +[ServiceDescriptor(typeof(IKeyedTestService), ServiceLifetime.Transient, "key1")] +[ServiceDescriptor(typeof(IKeyedTestService), ServiceLifetime.Scoped, "key2")] +public class SameServiceTypeDifferentKeys : IKeyedTestService { } + +[ServiceDescriptor(typeof(IKeyedTestService), ServiceLifetime.Transient, null)] +public class NullKeyedService : IKeyedTestService { } diff --git a/test/Scrutor.Tests/ScanningTests.cs b/test/Scrutor.Tests/ScanningTests.cs index 23ce5a3a..bda4e6d0 100644 --- a/test/Scrutor.Tests/ScanningTests.cs +++ b/test/Scrutor.Tests/ScanningTests.cs @@ -569,6 +569,85 @@ public void AsSelfWithInterfacesHandlesOpenGenericTypes() .WithSingletonLifetime()); }); } + + [Fact] + public void ShouldAllowOptInToCompilerGeneratedTypes() + { + var provider = ConfigureProvider(services => + { + services.Scan(scan => scan + .FromAssemblyOf() + .AddClasses(classes => classes + .WithAttribute() + .AssignableTo() + ) + .AsSelf() + .WithTransientLifetime()); + }); + + var compilerGeneratedSubclass = provider.GetService(); + Assert.NotNull(compilerGeneratedSubclass); + } + + [Fact] + public void CanRegisterWithServiceKey() + { + Collection.Scan(scan => scan + .FromTypes() + .AsImplementedInterfaces(x => x != typeof(IOtherInheritance)) + .WithServiceKey("my-key") + .WithSingletonLifetime()); + + Assert.Equal(2, Collection.Count); + + Assert.All(Collection, x => + { + Assert.Equal(ServiceLifetime.Singleton, x.Lifetime); + Assert.Equal(typeof(ITransientService), x.ServiceType); + Assert.True(x.IsKeyedService); + Assert.Equal("my-key", x.ServiceKey); + }); + } + + [Fact] + public void CanRegisterWithServiceKeySelector() + { + Collection.Scan(scan => scan + .FromTypes() + .AsImplementedInterfaces(x => x != typeof(IOtherInheritance)) + .WithServiceKey(type => type.Name) + .WithSingletonLifetime()); + + Assert.Equal(2, Collection.Count); + + var service1 = Collection.First(x => x.ServiceKey as string == nameof(TransientService1)); + Assert.Equal(typeof(ITransientService), service1.ServiceType); + Assert.Equal(ServiceLifetime.Singleton, service1.Lifetime); + Assert.True(service1.IsKeyedService); + + var service2 = Collection.First(x => x.ServiceKey as string == nameof(TransientService2)); + Assert.Equal(typeof(ITransientService), service2.ServiceType); + Assert.Equal(ServiceLifetime.Singleton, service2.Lifetime); + Assert.True(service2.IsKeyedService); + } + + [Fact] + public void CanResolveKeyedServices() + { + Collection.Scan(scan => scan + .FromTypes() + .AsSelf() + .WithServiceKey(type => type.Name) + .WithTransientLifetime()); + + var provider = Collection.BuildServiceProvider(); + + var service1 = provider.GetRequiredKeyedService(nameof(TransientService1)); + var service2 = provider.GetRequiredKeyedService(nameof(TransientService2)); + + Assert.NotNull(service1); + Assert.NotNull(service2); + } } // ReSharper disable UnusedTypeParameter @@ -652,7 +731,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 { } @@ -664,6 +743,11 @@ public interface IMixedAttribute { } [ServiceDescriptor(typeof(IMixedAttribute), ServiceLifetime.Scoped)] [ServiceDescriptor(ServiceLifetime.Singleton)] public class MixedAttribute : IMixedAttribute { } + + public abstract class AllowedCompilerGeneratedBase { } + + [CompilerGenerated] + public class AllowedCompilerGeneratedSubclass : AllowedCompilerGeneratedBase { } } namespace Scrutor.Tests.ChildNamespace diff --git a/test/Scrutor.Tests/Scrutor.Tests.csproj b/test/Scrutor.Tests/Scrutor.Tests.csproj index 6e958b51..a8e30f66 100644 --- a/test/Scrutor.Tests/Scrutor.Tests.csproj +++ b/test/Scrutor.Tests/Scrutor.Tests.csproj @@ -1,7 +1,9 @@  - net9.0 + net10.0 latest + Exe + true @@ -9,10 +11,9 @@ - - - - - + + + +