From bb47a312a959216a63e9cc7106d23f4570465881 Mon Sep 17 00:00:00 2001 From: Martin Taillefer Date: Thu, 27 Jul 2023 14:06:21 -0700 Subject: [PATCH] Add support for auto-activated keyed singletons --- Directory.Build.targets | 2 +- docs/list-of-diagnostics.md | 3 +- .../AutoActivationExtensions.Keyed.cs | 405 ++++++++++++++++++ .../AutoActivationExtensions.cs | 109 ++--- .../AutoActivationHostedService.cs | 7 + .../AutoActivatorOptions.cs | 1 + ....DependencyInjection.AutoActivation.csproj | 4 + src/Shared/DiagnosticIds/Experiments.cs | 3 +- .../AcceptanceTest.Keyed.cs | 366 ++++++++++++++++ .../AcceptanceTest.cs | 4 +- .../AutoActivationExtensionsKeyedTests.cs | 76 ++++ 11 files changed, 927 insertions(+), 53 deletions(-) create mode 100644 src/Libraries/Microsoft.Extensions.DependencyInjection.AutoActivation/AutoActivationExtensions.Keyed.cs create mode 100644 test/Libraries/Microsoft.Extensions.DependencyInjection.AutoActivation.Tests/AcceptanceTest.Keyed.cs create mode 100644 test/Libraries/Microsoft.Extensions.DependencyInjection.AutoActivation.Tests/AutoActivationExtensionsKeyedTests.cs diff --git a/Directory.Build.targets b/Directory.Build.targets index b1fa0d3557e..ea8fb542103 100644 --- a/Directory.Build.targets +++ b/Directory.Build.targets @@ -33,7 +33,7 @@ $(NoWarn);AD0001 - $(NoWarn);EXTEXP0001;EXTEXP0002;EXTEXP0003;EXTEXP0004;EXTEXP0005;EXTEXP0006;EXTEXP0007;EXTEXP0008;EXTEXP0009;EXTEXP0010;EXTEXP0011 + $(NoWarn);EXTEXP0001;EXTEXP0002;EXTEXP0003;EXTEXP0004;EXTEXP0005;EXTEXP0006;EXTEXP0007;EXTEXP0008;EXTEXP0009;EXTEXP0010;EXTEXP0011;EXTEXP0012 $(NoWarn);NU5104 diff --git a/docs/list-of-diagnostics.md b/docs/list-of-diagnostics.md index 09a85bdfba6..3e2f6cd3653 100644 --- a/docs/list-of-diagnostics.md +++ b/docs/list-of-diagnostics.md @@ -25,4 +25,5 @@ if desired. | `EXTEXP0008` | Resource monitoring experiments | | `EXTEXP0009` | Hosting experiments | | `EXTEXP0010` | Object pool experiments | - +| `EXTEXP0011` | Document database experiments | +| `EXTEXP0012` | Auto-activation experiments | diff --git a/src/Libraries/Microsoft.Extensions.DependencyInjection.AutoActivation/AutoActivationExtensions.Keyed.cs b/src/Libraries/Microsoft.Extensions.DependencyInjection.AutoActivation/AutoActivationExtensions.Keyed.cs new file mode 100644 index 00000000000..d55636b030a --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.DependencyInjection.AutoActivation/AutoActivationExtensions.Keyed.cs @@ -0,0 +1,405 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Linq; +using Microsoft.Shared.DiagnosticIds; +using Microsoft.Shared.Diagnostics; + +namespace Microsoft.Extensions.DependencyInjection; + +public static partial class AutoActivationExtensions +{ + /// + /// Enforces singleton activation at startup time rather then at runtime. + /// + /// The type of the service to activate. + /// The to add the service to. + /// The of the service. + /// A reference to this instance after the operation has completed. + [Experimental(diagnosticId: Experiments.AutoActivation, UrlFormat = Experiments.UrlFormat)] + public static IServiceCollection ActivateKeyed( + this IServiceCollection services, + object? serviceKey) + where TService : class + { + _ = Throw.IfNull(services); + + _ = services + .AddHostedService() + .AddOptions() + .Configure(ao => + { + var constructed = typeof(IEnumerable); + if (ao.KeyedAutoActivators.Contains((constructed, serviceKey))) + { + return; + } + + if (ao.KeyedAutoActivators.Remove((typeof(TService), serviceKey))) + { + _ = ao.AutoActivators.Add(constructed); + return; + } + + _ = ao.KeyedAutoActivators.Add((typeof(TService), serviceKey)); + }); + + return services; + } + + /// + /// Enforces singleton activation at startup time rather then at runtime. + /// + /// The to add the service to. + /// The type of the service to activate. + /// The of the service. + /// A reference to this instance after the operation has completed. + [Experimental(diagnosticId: Experiments.AutoActivation, UrlFormat = Experiments.UrlFormat)] + [UnconditionalSuppressMessage( + "Trimming", + "IL2026:Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access otherwise can break functionality when trimming application code", + Justification = "Addressed with [DynamicallyAccessedMembers]")] + public static IServiceCollection ActivateKeyed( + this IServiceCollection services, + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] Type serviceType, + object? serviceKey) + { + _ = Throw.IfNull(services); + _ = Throw.IfNull(serviceType); + + _ = services + .AddHostedService() + .AddOptions() + .Configure(ao => + { + var constructed = typeof(IEnumerable<>).MakeGenericType(serviceType); + if (ao.KeyedAutoActivators.Contains((constructed, serviceKey))) + { + return; + } + + if (ao.KeyedAutoActivators.Remove((constructed, serviceKey))) + { + _ = ao.AutoActivators.Add(constructed); + return; + } + + _ = ao.KeyedAutoActivators.Add((serviceType, serviceKey)); + }); + + return services; + } + + /// + /// Adds an auto-activated singleton service of the type specified in TService with an implementation + /// type specified in TImplementation using the factory specified in implementationFactory + /// to the specified . + /// + /// The to add the service to. + /// The of the service. + /// The factory that creates the service. + /// The type of the service to add. + /// The type of the implementation to use. + /// A reference to this instance after the operation has completed. + [Experimental(diagnosticId: Experiments.AutoActivation, UrlFormat = Experiments.UrlFormat)] + public static IServiceCollection AddActivatedKeyedSingleton( + this IServiceCollection services, + object? serviceKey, + Func implementationFactory) + where TService : class + where TImplementation : class, TService + { + _ = Throw.IfNull(services); + _ = Throw.IfNull(implementationFactory); + + return services + .AddKeyedSingleton(serviceKey, implementationFactory) + .ActivateKeyed(serviceKey); + } + + /// + /// Adds an auto-activated singleton service of the type specified in TService with an implementation + /// type specified in TImplementation to the specified . + /// + /// The to add the service to. + /// The of the service. + /// The type of the service to add. + /// The type of the implementation to use. + /// A reference to this instance after the operation has completed. + [Experimental(diagnosticId: Experiments.AutoActivation, UrlFormat = Experiments.UrlFormat)] + public static IServiceCollection AddActivatedKeyedSingleton( + this IServiceCollection services, + object? serviceKey) + where TService : class + where TImplementation : class, TService + { + return services + .AddKeyedSingleton(serviceKey) + .ActivateKeyed(serviceKey); + } + + /// + /// Adds an auto-activated singleton service of the type specified in TService with a factory specified + /// in implementationFactory to the specified . + /// + /// The to add the service to. + /// The of the service. + /// The factory that creates the service. + /// The type of the service to add. + /// A reference to this instance after the operation has completed. + [Experimental(diagnosticId: Experiments.AutoActivation, UrlFormat = Experiments.UrlFormat)] + public static IServiceCollection AddActivatedKeyedSingleton( + this IServiceCollection services, + object? serviceKey, + Func implementationFactory) + where TService : class + { + return services + .AddKeyedSingleton(serviceKey, implementationFactory) + .ActivateKeyed(serviceKey); + } + + /// + /// Adds an auto-activated singleton service of the type specified in TService to the specified . + /// + /// The to add the service to. + /// The of the service. + /// The type of the service to add. + /// A reference to this instance after the operation has completed. + [Experimental(diagnosticId: Experiments.AutoActivation, UrlFormat = Experiments.UrlFormat)] + public static IServiceCollection AddActivatedKeyedSingleton( + this IServiceCollection services, + object? serviceKey) + where TService : class + { + return services + .AddKeyedSingleton(serviceKey) + .ActivateKeyed(serviceKey); + } + + /// + /// Adds an auto-activated singleton service of the type specified in serviceType to the specified + /// . + /// + /// The to add the service to. + /// The type of the service to register and the implementation to use. + /// The of the service. + /// A reference to this instance after the operation has completed. + [Experimental(diagnosticId: Experiments.AutoActivation, UrlFormat = Experiments.UrlFormat)] + public static IServiceCollection AddActivatedKeyedSingleton( + this IServiceCollection services, + Type serviceType, + object? serviceKey) + { + _ = Throw.IfNull(services); + _ = Throw.IfNull(serviceType); + + return services + .AddKeyedSingleton(serviceType, serviceKey) + .ActivateKeyed(serviceType, serviceKey); + } + + /// + /// Adds an auto-activated singleton service of the type specified in serviceType with a factory + /// specified in implementationFactory to the specified . + /// + /// The to add the service to. + /// The type of the service to register. + /// The of the service. + /// The factory that creates the service. + /// A reference to this instance after the operation has completed. + [Experimental(diagnosticId: Experiments.AutoActivation, UrlFormat = Experiments.UrlFormat)] + public static IServiceCollection AddActivatedKeyedSingleton( + this IServiceCollection services, + Type serviceType, + object? serviceKey, + Func implementationFactory) + { + _ = Throw.IfNull(services); + _ = Throw.IfNull(serviceType); + _ = Throw.IfNull(implementationFactory); + + return services + .AddKeyedSingleton(serviceType, serviceKey, implementationFactory) + .ActivateKeyed(serviceType, serviceKey); + } + + /// + /// Adds an auto-activated singleton service of the type specified in serviceType with an implementation + /// of the type specified in implementationType to the specified . + /// + /// The to add the service to. + /// The type of the service to register. + /// The of the service. + /// The implementation type of the service. + /// A reference to this instance after the operation has completed. + [Experimental(diagnosticId: Experiments.AutoActivation, UrlFormat = Experiments.UrlFormat)] + public static IServiceCollection AddActivatedKeyedSingleton( + this IServiceCollection services, + Type serviceType, + object? serviceKey, + Type implementationType) + { + _ = Throw.IfNull(services); + _ = Throw.IfNull(serviceType); + _ = Throw.IfNull(implementationType); + + return services + .AddKeyedSingleton(serviceType, serviceKey, implementationType) + .ActivateKeyed(serviceType, serviceKey); + } + + /// + /// Adds an auto-activated singleton service of the type specified in serviceType to the specified + /// if the service type hasn't already been registered. + /// + /// The to add the service to. + /// The type of the service to register. + /// The of the service. + [Experimental(diagnosticId: Experiments.AutoActivation, UrlFormat = Experiments.UrlFormat)] + public static void TryAddActivatedKeyedSingleton( + this IServiceCollection services, + Type serviceType, + object? serviceKey) + { + _ = Throw.IfNull(services); + _ = Throw.IfNull(serviceType); + + services.TryAddAndActivateKeyed(ServiceDescriptor.KeyedSingleton(serviceType, serviceKey, serviceType)); + } + + /// + /// Adds an auto-activated singleton service of the type specified in serviceType with an implementation + /// of the type specified in implementationType to the specified + /// if the service type hasn't already been registered. + /// + /// The to add the service to. + /// The type of the service to register. + /// The of the service. + /// The implementation type of the service. + [Experimental(diagnosticId: Experiments.AutoActivation, UrlFormat = Experiments.UrlFormat)] + public static void TryAddActivatedKeyedSingleton( + this IServiceCollection services, + Type serviceType, + object? serviceKey, + Type implementationType) + { + _ = Throw.IfNull(services); + _ = Throw.IfNull(serviceType); + _ = Throw.IfNull(implementationType); + + services.TryAddAndActivateKeyed(ServiceDescriptor.KeyedSingleton(serviceType, serviceKey, implementationType)); + } + + /// + /// Adds an auto-activated singleton service of the type specified in serviceType with a factory + /// specified in implementationFactory to the specified + /// if the service type hasn't already been registered. + /// + /// The to add the service to. + /// The type of the service to register. + /// The of the service. + /// The factory that creates the service. + [Experimental(diagnosticId: Experiments.AutoActivation, UrlFormat = Experiments.UrlFormat)] + public static void TryAddActivatedKeyedSingleton( + this IServiceCollection services, + Type serviceType, + object? serviceKey, + Func implementationFactory) + { + _ = Throw.IfNull(services); + _ = Throw.IfNull(serviceType); + _ = Throw.IfNull(implementationFactory); + + services.TryAddAndActivateKeyed(ServiceDescriptor.KeyedSingleton(serviceType, serviceKey, implementationFactory)); + } + + /// + /// Adds an auto-activated singleton service of the type specified in TService + /// to the specified + /// if the service type hasn't already been registered. + /// + /// The to add the service to. + /// The of the service. + /// The type of the service to add. + [Experimental(diagnosticId: Experiments.AutoActivation, UrlFormat = Experiments.UrlFormat)] + public static void TryAddActivatedKeyedSingleton( + this IServiceCollection services, + object? serviceKey) + where TService : class + { + _ = Throw.IfNull(services); + + services.TryAddAndActivateKeyed(ServiceDescriptor.KeyedSingleton(serviceKey)); + } + + /// + /// Adds an auto-activated singleton service of the type specified in TService with an implementation + /// type specified in TImplementation using the factory specified in implementationFactory + /// to the specified + /// if the service type hasn't already been registered. + /// + /// The to add the service to. + /// The of the service. + /// The type of the service to add. + /// The type of the implementation to use. + [Experimental(diagnosticId: Experiments.AutoActivation, UrlFormat = Experiments.UrlFormat)] + public static void TryAddActivatedKeyedSingleton( + this IServiceCollection services, + object? serviceKey) + where TService : class + where TImplementation : class, TService + { + _ = Throw.IfNull(services); + + services.TryAddAndActivateKeyed(ServiceDescriptor.KeyedSingleton(serviceKey)); + } + + /// + /// Adds an auto-activated singleton service of the type specified in serviceType with a factory + /// specified in implementationFactory to the specified + /// if the service type hasn't already been registered. + /// + /// The to add the service to. + /// The of the service. + /// The factory that creates the service. + /// The type of the service to add. + [Experimental(diagnosticId: Experiments.AutoActivation, UrlFormat = Experiments.UrlFormat)] + public static void TryAddActivatedKeyedSingleton( + this IServiceCollection services, + object? serviceKey, + Func implementationFactory) + where TService : class + { + _ = Throw.IfNull(services); + _ = Throw.IfNull(implementationFactory); + + services.TryAddAndActivateKeyed(ServiceDescriptor.KeyedSingleton(serviceKey, implementationFactory)); + } + + private static void TryAddAndActivateKeyed(this IServiceCollection services, ServiceDescriptor descriptor) + where TService : class + { + if (services.Any(d => d.ServiceType == descriptor.ServiceType)) + { + return; + } + + services.Add(descriptor); + _ = services.ActivateKeyed(descriptor.ServiceKey); + } + + private static void TryAddAndActivateKeyed(this IServiceCollection services, ServiceDescriptor descriptor) + { + if (services.Any(d => d.ServiceType == descriptor.ServiceType)) + { + return; + } + + services.Add(descriptor); + _ = services.ActivateKeyed(descriptor.ServiceType, descriptor.ServiceKey); + } +} diff --git a/src/Libraries/Microsoft.Extensions.DependencyInjection.AutoActivation/AutoActivationExtensions.cs b/src/Libraries/Microsoft.Extensions.DependencyInjection.AutoActivation/AutoActivationExtensions.cs index 8230443df71..aa9ae00b574 100644 --- a/src/Libraries/Microsoft.Extensions.DependencyInjection.AutoActivation/AutoActivationExtensions.cs +++ b/src/Libraries/Microsoft.Extensions.DependencyInjection.AutoActivation/AutoActivationExtensions.cs @@ -6,6 +6,7 @@ using System.Diagnostics.CodeAnalysis; using System.Linq; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Shared.DiagnosticIds; using Microsoft.Shared.Diagnostics; namespace Microsoft.Extensions.DependencyInjection; @@ -13,12 +14,12 @@ namespace Microsoft.Extensions.DependencyInjection; /// /// Extension methods for automatically activating singletons after application starts. /// -public static class AutoActivationExtensions +public static partial class AutoActivationExtensions { /// /// Enforces singleton activation at startup time rather then at runtime. /// - /// The type of the service to add. + /// The type of the service to activate. /// The to add the service to. /// A reference to this instance after the operation has completed. public static IServiceCollection Activate(this IServiceCollection services) @@ -26,24 +27,64 @@ public static IServiceCollection Activate(this IServiceCollection serv { _ = Throw.IfNull(services); - _ = services.AddHostedService() - .AddOptions() - .Configure(ao => - { - var constructed = typeof(IEnumerable); - if (ao.AutoActivators.Contains(constructed)) - { - return; - } - - if (ao.AutoActivators.Remove(typeof(TService))) - { - _ = ao.AutoActivators.Add(constructed); - return; - } - - _ = ao.AutoActivators.Add(typeof(TService)); - }); + _ = services + .AddHostedService() + .AddOptions() + .Configure(ao => + { + var constructed = typeof(IEnumerable); + if (ao.AutoActivators.Contains(constructed)) + { + return; + } + + if (ao.AutoActivators.Remove(typeof(TService))) + { + _ = ao.AutoActivators.Add(constructed); + return; + } + + _ = ao.AutoActivators.Add(typeof(TService)); + }); + + return services; + } + + /// + /// Enforces singleton activation at startup time rather then at runtime. + /// + /// The to add the service to. + /// The type of the service to activate. + /// A reference to this instance after the operation has completed. + [Experimental(diagnosticId: Experiments.AutoActivation, UrlFormat = Experiments.UrlFormat)] + [UnconditionalSuppressMessage( + "Trimming", + "IL2026:Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access otherwise can break functionality when trimming application code", + Justification = "Addressed with [DynamicallyAccessedMembers]")] + public static IServiceCollection Activate(this IServiceCollection services, [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] Type serviceType) + { + _ = Throw.IfNull(services); + _ = Throw.IfNull(serviceType); + + _ = services + .AddHostedService() + .AddOptions() + .Configure(ao => + { + var constructed = typeof(IEnumerable<>).MakeGenericType(serviceType); + if (ao.AutoActivators.Contains(constructed)) + { + return; + } + + if (ao.AutoActivators.Remove(serviceType)) + { + _ = ao.AutoActivators.Add(constructed); + return; + } + + _ = ao.AutoActivators.Add(serviceType); + }); return services; } @@ -270,34 +311,6 @@ public static void TryAddActivatedSingleton(this IServiceCollection se services.TryAddAndActivate(ServiceDescriptor.Singleton(implementationFactory)); } - [UnconditionalSuppressMessage( - "Trimming", - "IL2026:Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access otherwise can break functionality when trimming application code", - Justification = "Addressed with [DynamicallyAccessedMembers]")] - internal static IServiceCollection Activate(this IServiceCollection services, [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] Type serviceType) - { - _ = services.AddHostedService() - .AddOptions() - .Configure(ao => - { - var constructed = typeof(IEnumerable<>).MakeGenericType(serviceType); - if (ao.AutoActivators.Contains(constructed)) - { - return; - } - - if (ao.AutoActivators.Remove(serviceType)) - { - _ = ao.AutoActivators.Add(constructed); - return; - } - - _ = ao.AutoActivators.Add(serviceType); - }); - - return services; - } - private static void TryAddAndActivate(this IServiceCollection services, ServiceDescriptor descriptor) where TService : class { diff --git a/src/Libraries/Microsoft.Extensions.DependencyInjection.AutoActivation/AutoActivationHostedService.cs b/src/Libraries/Microsoft.Extensions.DependencyInjection.AutoActivation/AutoActivationHostedService.cs index b33c748e44c..1100433857c 100644 --- a/src/Libraries/Microsoft.Extensions.DependencyInjection.AutoActivation/AutoActivationHostedService.cs +++ b/src/Libraries/Microsoft.Extensions.DependencyInjection.AutoActivation/AutoActivationHostedService.cs @@ -15,6 +15,7 @@ namespace Microsoft.Extensions.DependencyInjection; internal sealed class AutoActivationHostedService : IHostedService { private readonly Type[] _autoActivators; + private readonly (Type, object?)[] _keyedAutoActivators; private readonly IServiceProvider _provider; public AutoActivationHostedService(IServiceProvider provider, IOptions options) @@ -23,6 +24,7 @@ public AutoActivationHostedService(IServiceProvider provider, IOptions AutoActivators { get; } = new(); + public HashSet<(Type serviceType, object? serviceKey)> KeyedAutoActivators { get; } = new(); } diff --git a/src/Libraries/Microsoft.Extensions.DependencyInjection.AutoActivation/Microsoft.Extensions.DependencyInjection.AutoActivation.csproj b/src/Libraries/Microsoft.Extensions.DependencyInjection.AutoActivation/Microsoft.Extensions.DependencyInjection.AutoActivation.csproj index 9b17c21e82b..94805a22c97 100644 --- a/src/Libraries/Microsoft.Extensions.DependencyInjection.AutoActivation/Microsoft.Extensions.DependencyInjection.AutoActivation.csproj +++ b/src/Libraries/Microsoft.Extensions.DependencyInjection.AutoActivation/Microsoft.Extensions.DependencyInjection.AutoActivation.csproj @@ -5,6 +5,10 @@ Fundamentals + + true + + normal 100 diff --git a/src/Shared/DiagnosticIds/Experiments.cs b/src/Shared/DiagnosticIds/Experiments.cs index db9f038280d..dd7cbeed828 100644 --- a/src/Shared/DiagnosticIds/Experiments.cs +++ b/src/Shared/DiagnosticIds/Experiments.cs @@ -10,7 +10,7 @@ namespace Microsoft.Shared.DiagnosticIds; /// /// /// When adding a new experiment, add a corresponding suppression to the root Directory.Build.targets file, and add a documentation entry to -/// docs/Experiments.md. +/// docs/list-of-diagnostics.md. /// internal static class Experiments { @@ -29,4 +29,5 @@ internal static class Experiments internal const string Hosting = "EXTEXP0009"; internal const string ObjectPool = "EXTEXP0010"; internal const string DocumentDb = "EXTEXP0011"; + internal const string AutoActivation = "EXTEXP0012"; } diff --git a/test/Libraries/Microsoft.Extensions.DependencyInjection.AutoActivation.Tests/AcceptanceTest.Keyed.cs b/test/Libraries/Microsoft.Extensions.DependencyInjection.AutoActivation.Tests/AcceptanceTest.Keyed.cs new file mode 100644 index 00000000000..c96c051f5c1 --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.DependencyInjection.AutoActivation.Tests/AcceptanceTest.Keyed.cs @@ -0,0 +1,366 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Threading.Tasks; +using Microsoft.Extensions.DependencyInjection.Test.Fakes; +using Microsoft.Extensions.DependencyInjection.Test.Helpers; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Hosting.Testing; +using Xunit; + +namespace Microsoft.Extensions.DependencyInjection.Test; + +public partial class AcceptanceTest +{ + [Fact] + public async Task CanAddAndActivateKeyedSingletonAsync() + { + var instanceCount = new InstanceCreatingCounter(); + Assert.Equal(0, instanceCount.Counter); + + var serviceKey = new object(); + using var host = await FakeHost.CreateBuilder() + .ConfigureServices(services => services + .AddSingleton(instanceCount) + .AddActivatedKeyedSingleton(serviceKey)) + .StartAsync(); + + var service = host.Services.GetKeyedService(serviceKey); + await host.StopAsync(); + + Assert.NotNull(service); + Assert.Equal(1, instanceCount.Counter); + } + + [Fact] + public async Task ShouldAddAndActivateOnlyOnce_WhenHasChildAsync_Keyed() + { + var parentCount = new InstanceCreatingCounter(); + var childCount = new InstanceCreatingCounter(); + + var serviceKey = new object(); + using var host = await FakeHost.CreateBuilder() + .ConfigureServices(services => services.AddSingleton(childCount) + .AddSingleton(parentCount) + .AddActivatedKeyedSingleton(typeof(IFakeService), serviceKey, typeof(FakeService)) + .AddActivatedKeyedSingleton(serviceKey, (sp, sk) => + { + return new FactoryService(sp.GetKeyedService(sk)!, sp.GetKeyedService(sk)!); + })) + .StartAsync(); + await host.StopAsync(); + + Assert.Equal(1, childCount.Counter); + Assert.Equal(1, parentCount.Counter); + } + + [Fact] + public async Task ShouldResolveComponentsAutomaticallyAsync_Keyed() + { + var parentCount = new InstanceCreatingCounter(); + var childCount = new InstanceCreatingCounter(); + + var serviceKey = new object(); + using var host = await FakeHost.CreateBuilder() + .ConfigureServices(services => services + .AddSingleton(childCount) + .AddSingleton(parentCount) + .AddSingleton() + .AddActivatedKeyedSingleton(serviceKey)) + .StartAsync(); + await host.StopAsync(); + + Assert.Equal(1, childCount.Counter); + Assert.Equal(1, parentCount.Counter); + } + + [Fact] + public async Task CanActivateEnumerableAsync_Keyed() + { + var fakeServiceCount = new InstanceCreatingCounter(); + var fakeFactoryCount = new InstanceCreatingCounter(); + var anotherFakeServiceCount = new AnotherFakeServiceCounter(); + + var serviceKey = new object(); + using var host = await FakeHost.CreateBuilder() + .ConfigureServices(services => services + .AddSingleton(fakeServiceCount) + .AddSingleton(fakeFactoryCount) + .AddSingleton(anotherFakeServiceCount) + .AddActivatedKeyedSingleton(serviceKey) + .AddActivatedKeyedSingleton(serviceKey) + .AddActivatedKeyedSingleton(serviceKey)) + .StartAsync(); + await host.StopAsync(); + + Assert.Equal(1, fakeServiceCount.Counter); + Assert.Equal(1, fakeFactoryCount.Counter); + Assert.Equal(1, anotherFakeServiceCount.Counter); + + await host.StopAsync(); + } + + [Fact] + public async Task CanActivateOneServiceAsync_Keyed() + { + var fakeServiceCount = new InstanceCreatingCounter(); + var anotherFakeServiceCount = new AnotherFakeServiceCounter(); + + var serviceKey = new object(); + using var host = await FakeHost.CreateBuilder() + .ConfigureServices(services => services + .AddSingleton(fakeServiceCount) + .AddSingleton(anotherFakeServiceCount) + .AddSingleton() + .AddActivatedKeyedSingleton(serviceKey)) + .StartAsync(); + await host.StopAsync(); + + Assert.Equal(0, fakeServiceCount.Counter); + Assert.Equal(1, anotherFakeServiceCount.Counter); + } + + [Fact] + public async Task ShouldActivateService_WhenTypeIsSpecifiedInTypeParameterTService_Keyed() + { + var counter = new InstanceCreatingCounter(); + var anotherFakeServiceCount = new AnotherFakeServiceCounter(); + + var serviceKey = new object(); + using var host = await FakeHost.CreateBuilder() + .ConfigureServices(services => services + .AddSingleton(counter) + .AddSingleton(anotherFakeServiceCount) + .AddActivatedKeyedSingleton(serviceKey) + .AddActivatedKeyedSingleton(serviceKey, (sp, sk) => new AnotherFakeService(sp.GetKeyedService(sk)!))) + .StartAsync(); + await host.StopAsync(); + + Assert.Equal(1, counter.Counter); + Assert.Equal(1, anotherFakeServiceCount.Counter); + } + +#if false + [Fact] + public async Task ShouldActivateService_WhenTypeIsSpecifiedInParameter() + { + var counter = new InstanceCreatingCounter(); + var anotherFakeServiceCount = new AnotherFakeServiceCounter(); + + using var host = await FakeHost.CreateBuilder() + .ConfigureServices(services => services + .AddSingleton(counter) + .AddSingleton(anotherFakeServiceCount) + .AddActivatedSingleton(typeof(FakeService)) + .AddActivatedSingleton(typeof(AnotherFakeService), _ => new AnotherFakeService(_.GetService()!))) + .StartAsync(); + await host.StopAsync(); + + Assert.Equal(1, counter.Counter); + Assert.Equal(1, anotherFakeServiceCount.Counter); + } + + [Fact] + public async Task TestStopHostAsync() + { + var counter = new InstanceCreatingCounter(); + + using var host = await FakeHost.CreateBuilder() + .ConfigureServices(services => services + .AddSingleton(counter) + .AddActivatedSingleton()) + .StartAsync(); + + Assert.Equal(1, counter.Counter); + await host.StopAsync(); + } + + [Fact] + public async Task ShouldNotActivate_WhenServiceOfTypeSpecifiedInTypeParameter_WasAlreadyAdded() + { + var counter = new InstanceCreatingCounter(); + var anotherFakeServiceCount = new AnotherFakeServiceCounter(); + + using var host = await FakeHost.CreateBuilder() + .ConfigureServices(services => + { + services + .AddSingleton(counter) + .AddSingleton(anotherFakeServiceCount) + .AddSingleton() + .AddSingleton(); + services.TryAddActivatedSingleton(typeof(FakeService)); + services.TryAddActivatedSingleton(typeof(AnotherFakeService), _ => new AnotherFakeService(_.GetService()!)); + }) + .StartAsync(); + await host.StopAsync(); + + Assert.Equal(0, counter.Counter); + Assert.Equal(0, anotherFakeServiceCount.Counter); + } + + [Fact] + public async Task ShouldNotActivate_WhenServiceOfTypeSpecifiedInParameter_WasAlreadyAdded() + { + var counter = new InstanceCreatingCounter(); + var anotherFakeServiceCount = new AnotherFakeServiceCounter(); + + using var host = await FakeHost.CreateBuilder() + .ConfigureServices(services => + { + services + .AddSingleton(counter) + .AddSingleton(anotherFakeServiceCount) + .AddSingleton() + .AddSingleton(); + services.TryAddActivatedSingleton(); + services.TryAddActivatedSingleton(_ => new AnotherFakeService(_.GetService()!)); + }) + .StartAsync(); + await host.StopAsync(); + + Assert.Equal(0, counter.Counter); + Assert.Equal(0, anotherFakeServiceCount.Counter); + } + + [Fact] + public async Task ShouldActivateOneSingleton_WhenTryAddIsCalled_WithTypeSpecifiedImplementation() + { + var counter = new InstanceCreatingCounter(); + var anotherFakeServiceCount = new AnotherFakeServiceCounter(); + + using var host = await FakeHost.CreateBuilder() + .ConfigureServices(services => + { + services + .AddSingleton(counter) + .AddSingleton(anotherFakeServiceCount); + services.TryAddActivatedSingleton(); + services.TryAddActivatedSingleton(typeof(IFakeService), typeof(AnotherFakeService)); + }) + .StartAsync(); + await host.StopAsync(); + + Assert.Equal(1, counter.Counter); + Assert.Equal(0, anotherFakeServiceCount.Counter); + } + + // ------------------------------------------------------------------------------ + [Fact] + public async Task CanActivateSingletonAsync() + { + var instanceCount = new InstanceCreatingCounter(); + Assert.Equal(0, instanceCount.Counter); + + using var host = await FakeHost.CreateBuilder() + .ConfigureServices(services => services + .AddSingleton(instanceCount) + .AddSingleton() + .Activate()) + .StartAsync(); + await host.StopAsync(); + + Assert.Equal(1, instanceCount.Counter); + + var service = host.Services.GetService(); + + Assert.NotNull(service); + Assert.Equal(1, instanceCount.Counter); + } + + [Fact] + public async Task ActivationOfNotRegisteredType_ThrowsExceptionAsync() + { + using var host = FakeHost.CreateBuilder() + .ConfigureServices(services => services.Activate()) + .Build(); + + var exception = await Assert.ThrowsAsync(() => host.StartAsync()); + + Assert.Contains(typeof(IFakeService).FullName!, exception.Message); + } + + [Fact] + public async Task CanActivateEnumerableImplicitlyAddedAsync() + { + var fakeServiceCount = new InstanceCreatingCounter(); + var fakeFactoryCount = new InstanceCreatingCounter(); + + using var host = await FakeHost.CreateBuilder() + .ConfigureServices(services => services + .AddSingleton(fakeServiceCount) + .AddSingleton(fakeFactoryCount) + .AddSingleton().Activate() + .AddSingleton().Activate()) + .StartAsync(); + await host.StopAsync(); + + Assert.Equal(1, fakeServiceCount.Counter); + Assert.Equal(1, fakeFactoryCount.Counter); + } + + [Fact] + public async Task CanActivateEnumerableExplicitlyAddedAsync() + { + var fakeServiceCount = new InstanceCreatingCounter(); + var fakeFactoryCount = new InstanceCreatingCounter(); + + using var host = await FakeHost.CreateBuilder() + .ConfigureServices(services => services + .AddSingleton(fakeServiceCount) + .AddSingleton(fakeFactoryCount) + .AddSingleton() + .AddSingleton() + .Activate>()) + .StartAsync(); + await host.StopAsync(); + + Assert.Equal(1, fakeServiceCount.Counter); + Assert.Equal(1, fakeFactoryCount.Counter); + } + + [Fact] + public async Task CanAutoActivateOpenGenericsAsEnumerableAsync() + { + var fakeServiceCount = new InstanceCreatingCounter(); + var fakeOpenGenericCount = new InstanceCreatingCounter(); + + using var host = await new HostBuilder() + .ConfigureServices(services => services + .AddSingleton(fakeServiceCount) + .AddSingleton(fakeOpenGenericCount) + .AddTransient() + .AddSingleton(typeof(IFakeOpenGenericService), typeof(FakeService)) + .AddSingleton(typeof(IFakeOpenGenericService<>), typeof(FakeOpenGenericService<>)) + .Activate>>() + .Activate>()) + .StartAsync(); + await host.StopAsync(); + + Assert.Equal(1, fakeServiceCount.Counter); + Assert.Equal(2, fakeOpenGenericCount.Counter); + } + + [Fact] + public async Task CanAutoActivateClosedGenericsAsEnumerableAsync() + { + var fakeServiceCount = new InstanceCreatingCounter(); + var fakeOpenGenericCount = new InstanceCreatingCounter(); + + using var host = await FakeHost.CreateBuilder() + .ConfigureServices(services => services + .AddSingleton(fakeServiceCount) + .AddSingleton(fakeOpenGenericCount) + .AddTransient() + .AddSingleton(typeof(IFakeOpenGenericService), typeof(FakeService)) + .AddSingleton, FakeOpenGenericService>() + .Activate>>()) + .StartAsync(); + await host.StopAsync(); + + Assert.Equal(1, fakeServiceCount.Counter); + Assert.Equal(1, fakeOpenGenericCount.Counter); + } +#endif +} diff --git a/test/Libraries/Microsoft.Extensions.DependencyInjection.AutoActivation.Tests/AcceptanceTest.cs b/test/Libraries/Microsoft.Extensions.DependencyInjection.AutoActivation.Tests/AcceptanceTest.cs index 622e9c3957a..908b705626c 100644 --- a/test/Libraries/Microsoft.Extensions.DependencyInjection.AutoActivation.Tests/AcceptanceTest.cs +++ b/test/Libraries/Microsoft.Extensions.DependencyInjection.AutoActivation.Tests/AcceptanceTest.cs @@ -12,7 +12,7 @@ namespace Microsoft.Extensions.DependencyInjection.Test; -public class AcceptanceTest +public partial class AcceptanceTest { [Fact] public async Task CanAddAndActivateSingletonAsync() @@ -34,7 +34,7 @@ public async Task CanAddAndActivateSingletonAsync() } [Fact] - public async Task SouldIgnoreComponent_WhenNoAutoStartAsync() + public async Task ShouldIgnoreComponent_WhenNoAutoStartAsync() { var instanceCount = new InstanceCreatingCounter(); Assert.Equal(0, instanceCount.Counter); diff --git a/test/Libraries/Microsoft.Extensions.DependencyInjection.AutoActivation.Tests/AutoActivationExtensionsKeyedTests.cs b/test/Libraries/Microsoft.Extensions.DependencyInjection.AutoActivation.Tests/AutoActivationExtensionsKeyedTests.cs new file mode 100644 index 00000000000..661892bc25e --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.DependencyInjection.AutoActivation.Tests/AutoActivationExtensionsKeyedTests.cs @@ -0,0 +1,76 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Linq; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Test.Fakes; +using Microsoft.Extensions.DependencyInjection.Test.Helpers; +using Xunit; + +namespace Microsoft.Extensions.DependencyInjection.Test; + +public class AutoActivationExtensionsKeyedTests +{ + [Fact] + public void AddActivatedKeyedSingleton_Throws_WhenArgumentsAreNull() + { + var serviceCollection = new ServiceCollection(); + Assert.Throws(() => AutoActivationExtensions.AddActivatedKeyedSingleton(null!, null, (_, _) => null!)); + Assert.Throws(() => AutoActivationExtensions.AddActivatedKeyedSingleton(serviceCollection, null, null!)); + + Assert.Throws(() => AutoActivationExtensions.AddActivatedKeyedSingleton(null!, null, (_, _) => null!)); + Assert.Throws(() => AutoActivationExtensions.AddActivatedKeyedSingleton(serviceCollection, null, null!)); + + Assert.Throws(() => AutoActivationExtensions.AddActivatedKeyedSingleton(null!, null)); + + Assert.Throws(() => AutoActivationExtensions.AddActivatedKeyedSingleton(null!, typeof(FakeService), null)); + Assert.Throws(() => AutoActivationExtensions.AddActivatedKeyedSingleton(serviceCollection, null!, null)); + + Assert.Throws(() => AutoActivationExtensions.AddActivatedKeyedSingleton(null!, null)); + + Assert.Throws(() => AutoActivationExtensions.AddActivatedKeyedSingleton(null!, typeof(FakeService), null, _ => null!)); + Assert.Throws(() => AutoActivationExtensions.AddActivatedKeyedSingleton(serviceCollection, null!, null, _ => null!)); + Assert.Throws(() => AutoActivationExtensions.AddActivatedKeyedSingleton(serviceCollection, typeof(FakeService), null, implementationFactory: null!)); + + Assert.Throws(() => AutoActivationExtensions.AddActivatedKeyedSingleton(null!, typeof(IFakeService), null, typeof(FakeService))); + Assert.Throws(() => AutoActivationExtensions.AddActivatedKeyedSingleton(serviceCollection, null!, null, typeof(FakeService))); + Assert.Throws(() => AutoActivationExtensions.AddActivatedKeyedSingleton(serviceCollection, typeof(IFakeService), null, implementationType: null!)); + } + + [Fact] + public void TryAddActivatedKeyedSingleton_Throws_WhenArgumentsAreNull() + { + var serviceCollection = new ServiceCollection(); + Assert.Throws(() => AutoActivationExtensions.TryAddActivatedKeyedSingleton(null!, typeof(FakeService), null)); + Assert.Throws(() => AutoActivationExtensions.TryAddActivatedKeyedSingleton(serviceCollection, null!, null)); + + Assert.Throws(() => AutoActivationExtensions.TryAddActivatedKeyedSingleton(null!, typeof(IFakeService), null, typeof(FakeService))); + Assert.Throws(() => AutoActivationExtensions.TryAddActivatedKeyedSingleton(serviceCollection, null!, null, typeof(FakeService))); + Assert.Throws(() => AutoActivationExtensions.TryAddActivatedKeyedSingleton(serviceCollection, typeof(IFakeService), null, implementationType: null!)); + + Assert.Throws(() => AutoActivationExtensions.TryAddActivatedKeyedSingleton(null!, typeof(FakeService), null, _ => null!)); + Assert.Throws(() => AutoActivationExtensions.TryAddActivatedKeyedSingleton(serviceCollection, null!, null, _ => null!)); + Assert.Throws(() => AutoActivationExtensions.TryAddActivatedKeyedSingleton(serviceCollection, typeof(FakeService), null, implementationFactory: null!)); + + Assert.Throws(() => AutoActivationExtensions.TryAddActivatedKeyedSingleton(null!, null)); + + Assert.Throws(() => AutoActivationExtensions.TryAddActivatedKeyedSingleton(null!, null)); + + Assert.Throws(() => AutoActivationExtensions.TryAddActivatedKeyedSingleton(null!, null, (_, _) => null!)); + Assert.Throws(() => AutoActivationExtensions.TryAddActivatedKeyedSingleton(serviceCollection, null, null!)); + } + + [Fact] + public void AutoActivate_Adds_OneHostedService() + { + var serviceCollection = new ServiceCollection(); + + serviceCollection.AddSingleton(new InstanceCreatingCounter()); + serviceCollection.AddActivatedKeyedSingleton(null); + Assert.Equal(1, serviceCollection.Count(d => d.ImplementationType == typeof(AutoActivationHostedService))); + + serviceCollection.AddActivatedKeyedSingleton(null); + Assert.Equal(1, serviceCollection.Count(d => d.ImplementationType == typeof(AutoActivationHostedService))); + } +}