From 1c4812d245fe543f6d822e8860ba478ca566f76a Mon Sep 17 00:00:00 2001 From: Martin Taillefer Date: Thu, 27 Jul 2023 12:10:40 -0700 Subject: [PATCH] Improve DependencyInjection.AutoActivationExtensions (#4219) - Avoid calling MakeGenericType when possible. - Delete the now-useless Hosting.StartupINitialization component. Co-authored-by: Martin Taillefer --- .../AutoActivationExtensions.cs | 135 +++++---- .../Hosting.StartupInitialization.csproj | 36 --- .../IStartupInitializationBuilder.cs | 43 --- .../IStartupInitializer.cs | 21 -- .../Internal/FunctionDerivedInitializer.cs | 23 -- .../Internal/StartupHostedService.cs | 62 ----- .../Internal/StartupInitializationBuilder.cs | 70 ----- .../StartupInitializationOptionsValidator.cs | 11 - .../StartupInitializationExtensions.cs | 81 ------ .../StartupInitializationOptions.cs | 22 -- .../AcceptanceTest.cs | 68 ++++- ...Hosting.StartupInitialization.Tests.csproj | 20 -- .../Internal/Database.cs | 26 -- .../Internal/DatabaseInitializer.cs | 26 -- .../Internal/DummyHostedService.cs | 33 --- .../Internal/TestResources.cs | 24 -- .../StartupInitializationAcceptanceTest.cs | 263 ------------------ .../StartupInitializationExtensionsTest.cs | 40 --- 18 files changed, 147 insertions(+), 857 deletions(-) delete mode 100644 src/ToBeMoved/Hosting.StartupInitialization/Hosting.StartupInitialization.csproj delete mode 100644 src/ToBeMoved/Hosting.StartupInitialization/IStartupInitializationBuilder.cs delete mode 100644 src/ToBeMoved/Hosting.StartupInitialization/IStartupInitializer.cs delete mode 100644 src/ToBeMoved/Hosting.StartupInitialization/Internal/FunctionDerivedInitializer.cs delete mode 100644 src/ToBeMoved/Hosting.StartupInitialization/Internal/StartupHostedService.cs delete mode 100644 src/ToBeMoved/Hosting.StartupInitialization/Internal/StartupInitializationBuilder.cs delete mode 100644 src/ToBeMoved/Hosting.StartupInitialization/Internal/StartupInitializationOptionsValidator.cs delete mode 100644 src/ToBeMoved/Hosting.StartupInitialization/StartupInitializationExtensions.cs delete mode 100644 src/ToBeMoved/Hosting.StartupInitialization/StartupInitializationOptions.cs delete mode 100644 test/ToBeMoved/Hosting.StartupInitialization.Tests/Hosting.StartupInitialization.Tests.csproj delete mode 100644 test/ToBeMoved/Hosting.StartupInitialization.Tests/Internal/Database.cs delete mode 100644 test/ToBeMoved/Hosting.StartupInitialization.Tests/Internal/DatabaseInitializer.cs delete mode 100644 test/ToBeMoved/Hosting.StartupInitialization.Tests/Internal/DummyHostedService.cs delete mode 100644 test/ToBeMoved/Hosting.StartupInitialization.Tests/Internal/TestResources.cs delete mode 100644 test/ToBeMoved/Hosting.StartupInitialization.Tests/StartupInitializationAcceptanceTest.cs delete mode 100644 test/ToBeMoved/Hosting.StartupInitialization.Tests/StartupInitializationExtensionsTest.cs diff --git a/src/Libraries/Microsoft.Extensions.DependencyInjection.AutoActivation/AutoActivationExtensions.cs b/src/Libraries/Microsoft.Extensions.DependencyInjection.AutoActivation/AutoActivationExtensions.cs index 1bb89cc1479..8230443df71 100644 --- a/src/Libraries/Microsoft.Extensions.DependencyInjection.AutoActivation/AutoActivationExtensions.cs +++ b/src/Libraries/Microsoft.Extensions.DependencyInjection.AutoActivation/AutoActivationExtensions.cs @@ -26,7 +26,26 @@ public static IServiceCollection Activate(this IServiceCollection serv { _ = Throw.IfNull(services); - return services.Activate(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; } /// @@ -46,7 +65,26 @@ public static IServiceCollection AddActivatedSingleton(implementationFactory).Activate(typeof(TService)); + return services + .AddSingleton(implementationFactory) + .Activate(); + } + + /// + /// 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 type of the service to add. + /// The type of the implementation to use. + /// A reference to this instance after the operation has completed. + public static IServiceCollection AddActivatedSingleton(this IServiceCollection services) + where TService : class + where TImplementation : class, TService + { + return services + .AddSingleton() + .Activate(); } /// @@ -60,7 +98,9 @@ public static IServiceCollection AddActivatedSingleton(this IServiceCollection services, Func implementationFactory) where TService : class { - return services.AddActivatedSingleton(typeof(TService), implementationFactory); + return services + .AddSingleton(implementationFactory) + .Activate(); } /// @@ -72,11 +112,13 @@ public static IServiceCollection AddActivatedSingleton(this IServiceCo public static IServiceCollection AddActivatedSingleton(this IServiceCollection services) where TService : class { - return services.AddActivatedSingleton(typeof(TService)); + return services + .AddSingleton() + .Activate(); } /// - /// Adds an autoactivated singleton service of the type specified in serviceType to the specified + /// Adds an auto-activated singleton service of the type specified in serviceType to the specified /// . /// /// The to add the service to. @@ -87,26 +129,13 @@ public static IServiceCollection AddActivatedSingleton(this IServiceCollection s _ = Throw.IfNull(services); _ = Throw.IfNull(serviceType); - return services.AddSingleton(serviceType).Activate(serviceType); - } - - /// - /// Adds an autoactivated 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 type of the service to add. - /// The type of the implementation to use. - /// A reference to this instance after the operation has completed. - public static IServiceCollection AddActivatedSingleton(this IServiceCollection services) - where TService : class - where TImplementation : class, TService - { - return services.AddActivatedSingleton(typeof(TService), typeof(TImplementation)); + return services + .AddSingleton(serviceType) + .Activate(serviceType); } /// - /// Adds an autoactivated singleton service of the type specified in serviceType with a factory + /// 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. @@ -119,11 +148,13 @@ public static IServiceCollection AddActivatedSingleton(this IServiceCollection s _ = Throw.IfNull(serviceType); _ = Throw.IfNull(implementationFactory); - return services.AddSingleton(serviceType, implementationFactory).Activate(serviceType); + return services + .AddSingleton(serviceType, implementationFactory) + .Activate(serviceType); } /// - /// Adds an autoactivated singleton service of the type specified in serviceType with an implementation + /// 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. @@ -136,7 +167,9 @@ public static IServiceCollection AddActivatedSingleton(this IServiceCollection s _ = Throw.IfNull(serviceType); _ = Throw.IfNull(implementationType); - return services.AddSingleton(serviceType, implementationType).Activate(serviceType); + return services + .AddSingleton(serviceType, implementationType) + .Activate(serviceType); } /// @@ -199,7 +232,7 @@ public static void TryAddActivatedSingleton(this IServiceCollection se { _ = Throw.IfNull(services); - services.TryAddAndActivate(ServiceDescriptor.Singleton(typeof(TService), typeof(TService))); + services.TryAddAndActivate(ServiceDescriptor.Singleton()); } /// @@ -217,7 +250,7 @@ public static void TryAddActivatedSingleton(this ISer { _ = Throw.IfNull(services); - services.TryAddAndActivate(ServiceDescriptor.Singleton(typeof(TService), typeof(TImplementation))); + services.TryAddAndActivate(ServiceDescriptor.Singleton()); } /// @@ -234,27 +267,16 @@ public static void TryAddActivatedSingleton(this IServiceCollection se _ = Throw.IfNull(services); _ = Throw.IfNull(implementationFactory); - services.TryAddAndActivate(ServiceDescriptor.Singleton(typeof(TService), implementationFactory)); - } - - private static void TryAddAndActivate(this IServiceCollection services, ServiceDescriptor descriptor) - { - if (services.Any(d => d.ServiceType == descriptor.ServiceType)) - { - return; - } - - services.Add(descriptor); - _ = services.Activate(descriptor.ServiceType); + 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]")] - private static IServiceCollection Activate(this IServiceCollection services, [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] Type serviceType) + internal static IServiceCollection Activate(this IServiceCollection services, [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] Type serviceType) { - _ = services.AddHostedServiceIfNotExist() + _ = services.AddHostedService() .AddOptions() .Configure(ao => { @@ -276,23 +298,26 @@ private static IServiceCollection Activate(this IServiceCollection services, [Dy return services; } - // exclude from coverage until https://github.com/microsoft/codecoverage/issues/38 is addressed - [ExcludeFromCodeCoverage] - private static IServiceCollection AddHostedServiceIfNotExist(this IServiceCollection services) + private static void TryAddAndActivate(this IServiceCollection services, ServiceDescriptor descriptor) + where TService : class { -#if NETFRAMEWORK - var autoActivationHostedServiceType = typeof(AutoActivationHostedService); + if (services.Any(d => d.ServiceType == descriptor.ServiceType)) + { + return; + } + + services.Add(descriptor); + _ = services.Activate(); + } - // This loop is needed only for older .NET versions where there's no check - // if the service was already added to the IServiceCollection. - foreach (var service in services) + private static void TryAddAndActivate(this IServiceCollection services, ServiceDescriptor descriptor) + { + if (services.Any(d => d.ServiceType == descriptor.ServiceType)) { - if (service.ImplementationType == autoActivationHostedServiceType) - { - return services; - } + return; } -#endif - return services.AddHostedService(); + + services.Add(descriptor); + _ = services.Activate(descriptor.ServiceType); } } diff --git a/src/ToBeMoved/Hosting.StartupInitialization/Hosting.StartupInitialization.csproj b/src/ToBeMoved/Hosting.StartupInitialization/Hosting.StartupInitialization.csproj deleted file mode 100644 index a3391002d01..00000000000 --- a/src/ToBeMoved/Hosting.StartupInitialization/Hosting.StartupInitialization.csproj +++ /dev/null @@ -1,36 +0,0 @@ - - - - Microsoft.Extensions.Hosting.Testing.StartupInitialization - Microsoft.Extensions.Hosting.Testing - Provides infrastructure to execute asynchronous functions on server startups - Fundamentals - Application Bootstrap - true - true - - - - normal - 100 - 80 - - - - - - - - - - - - - - - - - - - - diff --git a/src/ToBeMoved/Hosting.StartupInitialization/IStartupInitializationBuilder.cs b/src/ToBeMoved/Hosting.StartupInitialization/IStartupInitializationBuilder.cs deleted file mode 100644 index def62bee983..00000000000 --- a/src/ToBeMoved/Hosting.StartupInitialization/IStartupInitializationBuilder.cs +++ /dev/null @@ -1,43 +0,0 @@ -// 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; -using System.Threading.Tasks; -using Microsoft.Extensions.DependencyInjection; - -namespace Microsoft.Extensions.Hosting.Testing; - -/// -/// Configure service startup initialization. -/// -public interface IStartupInitializationBuilder -{ - /// - /// Gets services used add initializers. - /// - public IServiceCollection Services { get; } - - /// - /// Adds initializer of given type to be executed at service startup. - /// - /// - /// The initializers should be pure functions, i.e. they shouldn't hold any state. - /// They are used in transient manner, and the implementation is not guaranteed to be reachable by GC after startup time. - /// - /// Type of the initializer to add. - /// Instance of for further configuration. - public IStartupInitializationBuilder AddInitializer() - where T : class, IStartupInitializer; - - /// - /// Add ad-hoc initializer to be executed at service startup. - /// - /// - /// Note, that there is no indempotency semantics while calling this API. - /// Therefore, this interface is not recommended for library authors. - /// - /// Initializer to execute. - /// Instance of for further configuration. - public IStartupInitializationBuilder AddInitializer(Func initializer); -} diff --git a/src/ToBeMoved/Hosting.StartupInitialization/IStartupInitializer.cs b/src/ToBeMoved/Hosting.StartupInitialization/IStartupInitializer.cs deleted file mode 100644 index c45bc1a8902..00000000000 --- a/src/ToBeMoved/Hosting.StartupInitialization/IStartupInitializer.cs +++ /dev/null @@ -1,21 +0,0 @@ -// 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; -using System.Threading.Tasks; - -namespace Microsoft.Extensions.Hosting.Testing; - -/// -/// Holds the initialization function, so we can pass it through . -/// -public interface IStartupInitializer -{ - /// - /// Short startup initialization job. - /// - /// Cancellation token. - /// New . - public Task InitializeAsync(CancellationToken token); -} diff --git a/src/ToBeMoved/Hosting.StartupInitialization/Internal/FunctionDerivedInitializer.cs b/src/ToBeMoved/Hosting.StartupInitialization/Internal/FunctionDerivedInitializer.cs deleted file mode 100644 index 1609635cd91..00000000000 --- a/src/ToBeMoved/Hosting.StartupInitialization/Internal/FunctionDerivedInitializer.cs +++ /dev/null @@ -1,23 +0,0 @@ -// 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; -using System.Threading.Tasks; - -namespace Microsoft.Extensions.Hosting.Testing.Internal; - -internal sealed class FunctionDerivedInitializer : IStartupInitializer -{ - private readonly Func _action; - private readonly IServiceProvider _provider; - - public FunctionDerivedInitializer(IServiceProvider provider, Func action) - { - _provider = provider; - _action = action; - } - - public Task InitializeAsync(CancellationToken token) - => _action(_provider, token); -} diff --git a/src/ToBeMoved/Hosting.StartupInitialization/Internal/StartupHostedService.cs b/src/ToBeMoved/Hosting.StartupInitialization/Internal/StartupHostedService.cs deleted file mode 100644 index 8237b48862d..00000000000 --- a/src/ToBeMoved/Hosting.StartupInitialization/Internal/StartupHostedService.cs +++ /dev/null @@ -1,62 +0,0 @@ -// 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.Linq; -using System.Threading; -using System.Threading.Tasks; -using Microsoft.Extensions.Options; -using Microsoft.Shared.Diagnostics; - -namespace Microsoft.Extensions.Hosting.Testing.Internal; - -internal sealed class StartupHostedService : IHostedService -{ - internal readonly TimeSpan Timeout; - - private readonly TimeProvider _timeProvider; - private IStartupInitializer[] _initializers; - - public StartupHostedService(IOptions options, - IEnumerable initializers, TimeProvider? timeProvider = null, IDebuggerState? debugger = null) - { - Timeout = Throw.IfMemberNull(options, options.Value).Timeout; - _initializers = initializers.ToArray(); - _timeProvider = timeProvider ?? TimeProvider.System; - - if (debugger?.IsAttached ?? DebuggerState.System.IsAttached) - { - Timeout = System.Threading.Timeout.InfiniteTimeSpan; - } - } - - public async Task StartAsync(CancellationToken cancellationToken) - { - using var tcts = _timeProvider.CreateCancellationTokenSource(Timeout); - - using var cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, tcts.Token); - cts.Token.ThrowIfCancellationRequested(); - - var tasks = _initializers.Select(initializer => initializer.InitializeAsync(cts.Token)); - - try - { - await Task.WhenAll(tasks).ConfigureAwait(false); - } - catch (TaskCanceledException e) when (!cancellationToken.IsCancellationRequested) - { - throw new TaskCanceledException( - message: $"Exceeded maximum server initialization time of {Timeout}. Adjust {nameof(StartupInitializationOptions)} or split your work into smaller chunks.", - innerException: e); - } - - // StartupHostedService will be in the memory for the lifetime of the process. - // Looking at codebase, startup initializers are often holding many objects, so to allow GC to trace less of them, - // we are allowing to collect the initializers. - _initializers = Array.Empty(); - } - - public Task StopAsync(CancellationToken cancellationToken) - => Task.CompletedTask; -} diff --git a/src/ToBeMoved/Hosting.StartupInitialization/Internal/StartupInitializationBuilder.cs b/src/ToBeMoved/Hosting.StartupInitialization/Internal/StartupInitializationBuilder.cs deleted file mode 100644 index c82281e9cae..00000000000 --- a/src/ToBeMoved/Hosting.StartupInitialization/Internal/StartupInitializationBuilder.cs +++ /dev/null @@ -1,70 +0,0 @@ -// 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; -using System.Threading.Tasks; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.DependencyInjection.Extensions; -using Microsoft.Extensions.Options.Validation; -using Microsoft.Shared.Diagnostics; - -namespace Microsoft.Extensions.Hosting.Testing.Internal; - -/// -/// Builds server initialization phase. -/// -internal sealed class StartupInitializationBuilder : IStartupInitializationBuilder -{ - /// - /// Gets services used to configure initializers. - /// - public IServiceCollection Services { get; } - - /// - /// Initializes a new instance of the class. - /// - /// Service collection used for configuration. - public StartupInitializationBuilder(IServiceCollection services) - { - Services = services; - - RegisterHostedService(services); - _ = Services.AddValidatedOptions(); - } - - /// - /// Adds initializer of given type to be executed at service startup. - /// - /// Type of the initializer to add. - /// Instance of for further configuration. - public IStartupInitializationBuilder AddInitializer() - where T : class, IStartupInitializer - { - Services.TryAddTransient(); - - return this; - } - - /// - public IStartupInitializationBuilder AddInitializer(Func initializer) - { - _ = Throw.IfNull(initializer); - - _ = Services.AddTransient(provider => new FunctionDerivedInitializer(provider, initializer)); - - return this; - } - - private static void RegisterHostedService(IServiceCollection services) - { - if (services.Count != 0 && services[0].ImplementationType == typeof(StartupHostedService)) - { - return; - } - - services - .RemoveAll() - .Insert(0, ServiceDescriptor.Singleton()); - } -} diff --git a/src/ToBeMoved/Hosting.StartupInitialization/Internal/StartupInitializationOptionsValidator.cs b/src/ToBeMoved/Hosting.StartupInitialization/Internal/StartupInitializationOptionsValidator.cs deleted file mode 100644 index 20b926881e5..00000000000 --- a/src/ToBeMoved/Hosting.StartupInitialization/Internal/StartupInitializationOptionsValidator.cs +++ /dev/null @@ -1,11 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using Microsoft.Extensions.Options; - -namespace Microsoft.Extensions.Hosting.Testing.Internal; - -[OptionsValidator] -internal sealed partial class StartupInitializationOptionsValidator : IValidateOptions -{ -} diff --git a/src/ToBeMoved/Hosting.StartupInitialization/StartupInitializationExtensions.cs b/src/ToBeMoved/Hosting.StartupInitialization/StartupInitializationExtensions.cs deleted file mode 100644 index 00371c48685..00000000000 --- a/src/ToBeMoved/Hosting.StartupInitialization/StartupInitializationExtensions.cs +++ /dev/null @@ -1,81 +0,0 @@ -// 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.Diagnostics.CodeAnalysis; -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Hosting.Testing.Internal; -using Microsoft.Extensions.Options; -using Microsoft.Extensions.Options.Validation; -using Microsoft.Shared.Diagnostics; - -namespace Microsoft.Extensions.Hosting.Testing; - -/// -/// Extensions for configuring startup initialization. -/// -public static class StartupInitializationExtensions -{ - /// - /// Adds function that will be executed before application starts. - /// - /// - /// Use it for one time initialization logic. - /// Sequence of execution is not guaranteed. - /// - /// Service collection use to register initialization function. - /// Services passed for further configuration. - public static IStartupInitializationBuilder AddStartupInitialization(this IServiceCollection services) - { - _ = Throw.IfNull(services); - - return new StartupInitializationBuilder(services); - } - - /// - /// Adds function that will be executed before application starts. - /// - /// - /// Use it for one time initialization logic. - /// Sequence of execution is not guaranteed. - /// - /// Service collection use to register initialization function. - /// Configure startup initializers. - /// Services passed for further configuration. - public static IStartupInitializationBuilder AddStartupInitialization(this IServiceCollection services, Action configure) - { - _ = Throw.IfNull(services); - _ = Throw.IfNull(configure); - - _ = services.AddValidatedOptions() - .Configure(configure); - - return new StartupInitializationBuilder(services); - } - - /// - /// Adds function that will be executed before application starts. - /// - /// - /// Use it for one time initialization logic. - /// Sequence of execution is not guaranteed. - /// - /// Service collection use to register initialization function. - /// Configure startup initializers with config. - /// Services passed for further configuration. - [DynamicDependency(DynamicallyAccessedMemberTypes.PublicProperties | DynamicallyAccessedMemberTypes.PublicParameterlessConstructor, typeof(StartupInitializationOptions))] - [UnconditionalSuppressMessage("Trimming", - "IL2026:Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access otherwise can break functionality when trimming application code", - Justification = "Addressed by [DynamicDependency]")] - public static IStartupInitializationBuilder AddStartupInitialization(this IServiceCollection services, IConfigurationSection section) - { - _ = Throw.IfNull(services); - _ = Throw.IfNull(section); - - _ = services.AddValidatedOptions(); - _ = services.Configure(section); - - return new StartupInitializationBuilder(services); - } -} diff --git a/src/ToBeMoved/Hosting.StartupInitialization/StartupInitializationOptions.cs b/src/ToBeMoved/Hosting.StartupInitialization/StartupInitializationOptions.cs deleted file mode 100644 index b61e3f7cf06..00000000000 --- a/src/ToBeMoved/Hosting.StartupInitialization/StartupInitializationOptions.cs +++ /dev/null @@ -1,22 +0,0 @@ -// 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 Microsoft.Shared.Data.Validation; - -namespace Microsoft.Extensions.Hosting.Testing; - -/// -/// Configures startup initialization logic. -/// -public class StartupInitializationOptions -{ - /// - /// Gets or sets maximum time allowed for initialization logic. - /// - /// - /// The default value is 30 seconds. - /// - [TimeSpan("00:00:05", "01:00:00")] - public TimeSpan Timeout { get; set; } = TimeSpan.FromSeconds(30); -} diff --git a/test/Libraries/Microsoft.Extensions.DependencyInjection.AutoActivation.Tests/AcceptanceTest.cs b/test/Libraries/Microsoft.Extensions.DependencyInjection.AutoActivation.Tests/AcceptanceTest.cs index 150a26e6760..622e9c3957a 100644 --- a/test/Libraries/Microsoft.Extensions.DependencyInjection.AutoActivation.Tests/AcceptanceTest.cs +++ b/test/Libraries/Microsoft.Extensions.DependencyInjection.AutoActivation.Tests/AcceptanceTest.cs @@ -101,6 +101,31 @@ public async Task CanActivateEnumerableAsync() var fakeFactoryCount = new InstanceCreatingCounter(); var anotherFakeServiceCount = new AnotherFakeServiceCounter(); + using var host = await FakeHost.CreateBuilder() + .ConfigureServices(services => services + .AddSingleton(fakeServiceCount) + .AddSingleton(fakeFactoryCount) + .AddSingleton(anotherFakeServiceCount) + .AddActivatedSingleton(typeof(IFakeService), typeof(FakeService)) + .AddActivatedSingleton(typeof(IFakeService), typeof(FakeOneMultipleService)) + .AddActivatedSingleton(typeof(IFakeService), typeof(AnotherFakeService))) + .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 CanActivateEnumerableAsync_WithTypeArg() + { + var fakeServiceCount = new InstanceCreatingCounter(); + var fakeFactoryCount = new InstanceCreatingCounter(); + var anotherFakeServiceCount = new AnotherFakeServiceCounter(); + using var host = await FakeHost.CreateBuilder() .ConfigureServices(services => services .AddSingleton(fakeServiceCount) @@ -251,7 +276,7 @@ public async Task ShouldActivateOneSingleton_WhenTryAddIsCalled_WithTypeSpecifie services .AddSingleton(counter) .AddSingleton(anotherFakeServiceCount); - services.TryAddActivatedSingleton(); + services.TryAddActivatedSingleton(typeof(IFakeService), typeof(FakeService)); services.TryAddActivatedSingleton(typeof(IFakeService), typeof(AnotherFakeService)); }) .StartAsync(); @@ -261,6 +286,28 @@ public async Task ShouldActivateOneSingleton_WhenTryAddIsCalled_WithTypeSpecifie Assert.Equal(0, anotherFakeServiceCount.Counter); } + [Fact] + public async Task ShouldActivateOneSingleton_WhenTryAddIsCalled_WithTypeSpecifiedImplementation_WithTypeArg() + { + 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(); + }) + .StartAsync(); + await host.StopAsync(); + + Assert.Equal(1, counter.Counter); + Assert.Equal(0, anotherFakeServiceCount.Counter); + } + // ------------------------------------------------------------------------------ [Fact] public async Task CanActivateSingletonAsync() @@ -302,6 +349,25 @@ 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(typeof(IFakeService)) + .AddSingleton().Activate(typeof(IFakeService))) + .StartAsync(); + await host.StopAsync(); + + Assert.Equal(1, fakeServiceCount.Counter); + Assert.Equal(1, fakeFactoryCount.Counter); + } + + [Fact] + public async Task CanActivateEnumerableImplicitlyAddedAsync_WithTypeArg() + { + var fakeServiceCount = new InstanceCreatingCounter(); + var fakeFactoryCount = new InstanceCreatingCounter(); + using var host = await FakeHost.CreateBuilder() .ConfigureServices(services => services .AddSingleton(fakeServiceCount) diff --git a/test/ToBeMoved/Hosting.StartupInitialization.Tests/Hosting.StartupInitialization.Tests.csproj b/test/ToBeMoved/Hosting.StartupInitialization.Tests/Hosting.StartupInitialization.Tests.csproj deleted file mode 100644 index f513cc76a81..00000000000 --- a/test/ToBeMoved/Hosting.StartupInitialization.Tests/Hosting.StartupInitialization.Tests.csproj +++ /dev/null @@ -1,20 +0,0 @@ - - - - Microsoft.Extensions.Hosting.Testing.StartupInitialization.Tests - Microsoft.Extensions.Hosting.Testing.StartupInitialization.Test - Tests for Microsoft.Extensions.Hosting.Testing.StartupInitialization - - - - - - - - - - - - - - diff --git a/test/ToBeMoved/Hosting.StartupInitialization.Tests/Internal/Database.cs b/test/ToBeMoved/Hosting.StartupInitialization.Tests/Internal/Database.cs deleted file mode 100644 index 51446a5eb8a..00000000000 --- a/test/ToBeMoved/Hosting.StartupInitialization.Tests/Internal/Database.cs +++ /dev/null @@ -1,26 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using System.Threading.Tasks; -using Microsoft.Extensions.Logging; - -namespace Microsoft.Extensions.Hosting.Testing.StartupInitialization.Test; - -public class Database -{ - private readonly ILogger _logger; - - public const string LogMessage = "HEY I WAS INITIALIZED AT STARTUP IN ASYNC WAY HEHE"; - - public Database(ILogger logger) - { - _logger = logger; - } - - public Task Initialize() - { - _logger.LogInformation(LogMessage); - - return Task.CompletedTask; - } -} diff --git a/test/ToBeMoved/Hosting.StartupInitialization.Tests/Internal/DatabaseInitializer.cs b/test/ToBeMoved/Hosting.StartupInitialization.Tests/Internal/DatabaseInitializer.cs deleted file mode 100644 index 3e349866ac8..00000000000 --- a/test/ToBeMoved/Hosting.StartupInitialization.Tests/Internal/DatabaseInitializer.cs +++ /dev/null @@ -1,26 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using System.Threading; -using System.Threading.Tasks; -using Microsoft.Extensions.Logging; - -namespace Microsoft.Extensions.Hosting.Testing.StartupInitialization.Test; - -public class DatabaseInitializer : IStartupInitializer -{ - private readonly ILogger _logger; - public const string LogMessage = "HEY I WAS INITIALIZED AT STARTUP IN ASYNC WAY HEHE"; - - public DatabaseInitializer(ILogger logger) - { - _logger = logger; - } - - public Task InitializeAsync(CancellationToken token) - { - _logger.LogInformation(LogMessage); - - return Task.CompletedTask; - } -} diff --git a/test/ToBeMoved/Hosting.StartupInitialization.Tests/Internal/DummyHostedService.cs b/test/ToBeMoved/Hosting.StartupInitialization.Tests/Internal/DummyHostedService.cs deleted file mode 100644 index 0ca01e74f23..00000000000 --- a/test/ToBeMoved/Hosting.StartupInitialization.Tests/Internal/DummyHostedService.cs +++ /dev/null @@ -1,33 +0,0 @@ -// 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.Diagnostics.CodeAnalysis; -using System.Threading; -using System.Threading.Tasks; -using Microsoft.Extensions.Hosting; - -namespace Microsoft.Extensions.Hosting.Testing.StartupInitialization.Test; - -#pragma warning disable SA1402 // File may only contain a single type - -[SuppressMessage("Minor Code Smell", "S3717:Track use of \"NotImplementedException\"", Justification = "Not applicable.")] -public class DummyHostedService : IHostedService -{ - public Task StartAsync(CancellationToken cancellationToken) => throw new NotImplementedException(); - public Task StopAsync(CancellationToken cancellationToken) => throw new NotImplementedException(); -} - -[SuppressMessage("Minor Code Smell", "S3717:Track use of \"NotImplementedException\"", Justification = "Not applicable.")] -public class DummyHostedService2 : IHostedService -{ - public Task StartAsync(CancellationToken cancellationToken) => throw new NotImplementedException(); - public Task StopAsync(CancellationToken cancellationToken) => throw new NotImplementedException(); -} - -[SuppressMessage("Minor Code Smell", "S3717:Track use of \"NotImplementedException\"", Justification = "Not applicable.")] -public class DummyHostedService3 : IHostedService -{ - public Task StartAsync(CancellationToken cancellationToken) => throw new NotImplementedException(); - public Task StopAsync(CancellationToken cancellationToken) => throw new NotImplementedException(); -} diff --git a/test/ToBeMoved/Hosting.StartupInitialization.Tests/Internal/TestResources.cs b/test/ToBeMoved/Hosting.StartupInitialization.Tests/Internal/TestResources.cs deleted file mode 100644 index aeca5012f59..00000000000 --- a/test/ToBeMoved/Hosting.StartupInitialization.Tests/Internal/TestResources.cs +++ /dev/null @@ -1,24 +0,0 @@ -// 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 Microsoft.Extensions.Configuration; - -namespace Microsoft.Extensions.Hosting.Testing.StartupInitialization.Test; - -public static class TestResources -{ - public static IConfigurationSection GetSection(TimeSpan timeout) - { - StartupInitializationOptions options; - - return new ConfigurationBuilder() - .AddInMemoryCollection(new Dictionary - { - { $"{nameof(StartupInitializationOptions)}:{nameof(options.Timeout)}", timeout.ToString() }, - }) - .Build() - .GetSection($"{nameof(StartupInitializationOptions)}"); - } -} diff --git a/test/ToBeMoved/Hosting.StartupInitialization.Tests/StartupInitializationAcceptanceTest.cs b/test/ToBeMoved/Hosting.StartupInitialization.Tests/StartupInitializationAcceptanceTest.cs deleted file mode 100644 index 3380bbe6c2d..00000000000 --- a/test/ToBeMoved/Hosting.StartupInitialization.Tests/StartupInitializationAcceptanceTest.cs +++ /dev/null @@ -1,263 +0,0 @@ -// 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.Linq; -using System.Threading.Tasks; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Hosting.Testing.Internal; -using Microsoft.Extensions.Options; -using Microsoft.Extensions.Time.Testing; -using Microsoft.Shared.Diagnostics; -using Xunit; - -namespace Microsoft.Extensions.Hosting.Testing.StartupInitialization.Test; - -public class StartupInitializationAcceptanceTest -{ - [Fact] - public async Task Initialization_Functions_Are_Executed_On_Startup_In_Async_Manner_And_Logs_Message() - { - using var host = FakeHost.CreateBuilder() - .ConfigureServices((_, services) => services - .AddSingleton() - .AddStartupInitialization() - .AddInitializer(async static (sp, _) => - { - var db = sp.GetService(); - - Assert.NotNull(db); - await db!.Initialize(); - })) - .Build(); - - await host.StartAsync(); - await host.StopAsync(); - - var logMessages = host.GetFakeLogCollector().GetSnapshot().Select(x => x.Message); - - Assert.Contains(Database.LogMessage, logMessages); - } - - [Fact] - public async Task Initialization_Functions_Are_Executed_On_Startup_In_Async_Manner_And_Logs_Message_When_Using_Interface_Registration() - { - using var host = FakeHost.CreateBuilder() - .ConfigureServices((_, services) => services - .AddStartupInitialization() - .AddInitializer()) - .Build(); - - await host.StartAsync(); - await host.StopAsync(); - - var logMessages = host.GetFakeLogCollector().GetSnapshot().Select(x => x.Message); - - Assert.Contains(Database.LogMessage, logMessages); - } - - [Fact] - public void Initializers_Are_Indempotent_When_Provided_As_Interface() - { - using var sp = new ServiceCollection() - .AddLogging() - .AddStartupInitialization() - .AddInitializer() - .AddInitializer() - .AddInitializer() - .AddInitializer() - .Services - .BuildServiceProvider(); - - var i = sp.GetRequiredService>(); - - Assert.Single(i); - } - - [Fact] - public void Initializers_Are_Not_Indempotent_When_Provided_As_Anonymous_Function() - { - using var sp = new ServiceCollection() - .AddLogging() - .AddStartupInitialization() - .AddInitializer((_, _) => Task.CompletedTask) - .AddInitializer((_, _) => Task.CompletedTask) - .AddInitializer((_, _) => Task.CompletedTask) - .AddInitializer((_, _) => Task.CompletedTask) - .AddInitializer((_, _) => Task.CompletedTask) - .Services - .BuildServiceProvider(); - - var i = sp.GetRequiredService>().ToArray(); - - Assert.Equal(5, i.Length); - } - - [Fact] - public async Task Initialization_Functions_Are_Not_Executed_On_Startup_So_There_Is_No_Log_Message() - { - using var host = FakeHost.CreateBuilder() - .ConfigureServices((_, services) => services - .AddSingleton()) - .Build(); - - await host.StartAsync(); - await host.StopAsync(); - - var logMessages = host.GetFakeLogCollector().GetSnapshot().Select(x => x.Message); - - Assert.DoesNotContain(Database.LogMessage, logMessages); - } - - [Fact] - public void When_Registering_Multiple_Hosted_Services_StartupService_Is_First() - { - const int RegisteredHostedServices = 4; - - using var host = FakeHost.CreateBuilder() - .ConfigureServices((_, services) => services - .AddSingleton() - .AddHostedService() - .AddHostedService() - .AddHostedService() - .AddStartupInitialization() - .AddInitializer(async static (sp, _) => - { - var db = sp.GetService(); - - Assert.NotNull(db); - await db!.Initialize(); - })) - .Build(); - - var jobs = host.Services - .GetRequiredService>() - ?.ToArray(); - - Assert.NotNull(jobs); - - // In case FakeHost is adding some stuff. - Assert.True(jobs!.Length >= RegisteredHostedServices); - Assert.IsAssignableFrom(jobs[0]); - } - - [Fact] - public async Task Initialization_Function_Times_Out_When_It_Takes_Longer_Than_Options() - { - var fiveSeconds = TimeSpan.FromSeconds(5); - var timeProvider = new FakeTimeProvider(DateTimeOffset.UtcNow); - - using var host = FakeHost.CreateBuilder() - .ConfigureServices(s => s - .AddSingleton(timeProvider) - .AddStartupInitialization(x => x.Timeout = fiveSeconds) - .AddInitializer((_, ct) => - { - timeProvider.Advance(fiveSeconds); - return Task.Delay(-1, ct); - })) - .Build(); - - var e = await Assert.ThrowsAsync(() => host.StartAsync()); - - Assert.Contains(fiveSeconds.ToString(), e.Message); - Assert.Contains(nameof(StartupInitializationOptions), e.Message); - } - - [Fact] - public async Task Initialization_Function_Is_Cancelled_Without_Message_When_HostBuilder_Is_Canceled() - { - var fiveSeconds = TimeSpan.FromSeconds(5); - var timeProvider = new FakeTimeProvider(DateTimeOffset.UtcNow); - - var twoSeconds = TimeSpan.FromSeconds(2); - using var cts = timeProvider.CreateCancellationTokenSource(twoSeconds); - using var host = FakeHost.CreateBuilder() - .ConfigureServices(s => s - .AddSingleton(timeProvider) - .AddStartupInitialization(x => x.Timeout = fiveSeconds) - .AddInitializer((_, ct) => - { - timeProvider.Advance(twoSeconds); - return Task.Delay(-1, ct); - })) - .Build(); - - var e = await Assert.ThrowsAsync(() => host.StartAsync(cts.Token)); - - Assert.DoesNotContain(fiveSeconds.ToString(), e.Message); - Assert.DoesNotContain(nameof(StartupInitializationOptions), e.Message); - } - - [Fact] - public void Can_Use_Configuration_Section_To_Configure_StartupInitializationOptions() - { - var timeout = TimeSpan.FromSeconds(29); - - var o = new ServiceCollection() - .AddStartupInitialization(TestResources.GetSection(timeout)) - .Services - .BuildServiceProvider() - .GetRequiredService>(); - - Assert.NotNull(o?.Value); - Assert.Equal(timeout, o!.Value.Timeout); - } - - [Theory] - [InlineData(60000)] - [InlineData(4)] - public void When_Setting_Initialization_Timeout_Out_Of_Boundary_Validator_Throws(int seconds) - { - var o = new ServiceCollection() - .AddStartupInitialization(x => x.Timeout = TimeSpan.FromSeconds(seconds)) - .Services - .BuildServiceProvider() - .GetRequiredService>(); - - Assert.Throws(() => o?.Value); - } - - [Fact] - public void StartupHostedService_Gets_Registered_Only_Once_In_DI_And_It_Is_First() - { - using var sp = new ServiceCollection() - .AddStartupInitialization() - .Services - .AddStartupInitialization() - .Services - .AddStartupInitialization() - .Services - .AddStartupInitialization() - .Services - .AddStartupInitialization(_ => { }) - .Services - .AddStartupInitialization(_ => { }) - .Services - .BuildServiceProvider(); - - var s = sp.GetRequiredService>().ToArray(); - - Assert.IsAssignableFrom(s[0]); - Assert.Equal(1, s.Count(x => x is StartupHostedService)); - } - - [Fact] - public void When_Debugger_Is_Attached_Hosted_Service_Timeout_Is_Set_To_Infinite() - { - using var provider = new ServiceCollection() - .AddAttachedDebuggerState() - .AddStartupInitialization() - .AddInitializer((_, _) => Task.CompletedTask) - .Services - .BuildServiceProvider(); - - var service = provider - .GetRequiredService>() - .FirstOrDefault(x => x is StartupHostedService); - - Assert.IsAssignableFrom(service); - Assert.Equal(((StartupHostedService)service!).Timeout, System.Threading.Timeout.InfiniteTimeSpan); - } -} diff --git a/test/ToBeMoved/Hosting.StartupInitialization.Tests/StartupInitializationExtensionsTest.cs b/test/ToBeMoved/Hosting.StartupInitialization.Tests/StartupInitializationExtensionsTest.cs deleted file mode 100644 index 93cc96b31dc..00000000000 --- a/test/ToBeMoved/Hosting.StartupInitialization.Tests/StartupInitializationExtensionsTest.cs +++ /dev/null @@ -1,40 +0,0 @@ -// 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 Microsoft.Extensions.Configuration; -using Microsoft.Extensions.DependencyInjection; -using Xunit; - -namespace Microsoft.Extensions.Hosting.Testing.StartupInitialization.Test; -public class StartupInitializationExtensionsTest -{ - [Fact] - public void Public_API_Throws_On_Nulls() - { - var s = new ServiceCollection(); - - Assert.Throws(() => s.AddStartupInitialization((Action)null!)); - Assert.Throws(() => s.AddStartupInitialization((IConfigurationSection)null!)); - Assert.Throws(() => s.AddStartupInitialization().AddInitializer(null!)); - } - - [Fact] - public void Startup_Initializers_Are_Registered_As_Transient_So_They_Do_Not_Waste_Memory_After_They_Are_Used() - { - var s = new ServiceCollection() - .AddLogging() - .AddStartupInitialization() - .AddInitializer() - .Services; - - using var sp = s.BuildServiceProvider(); - - var first = sp.GetRequiredService(); - var second = sp.GetRequiredService(); - - Assert.IsAssignableFrom(first); - Assert.IsAssignableFrom(second); - Assert.NotEqual(first, second); - } -}