-
Notifications
You must be signed in to change notification settings - Fork 5.4k
Description
This adds a mechanism to register services to be activated during startup of a host instead of on first access.
It leverages the new IHostedLifecycleService.StartingAsync() in #86511 to do this as early as possible, although for backwards compat it can also hook into the IHostedService.StartAsync() if the new StartingAsync() was not called.
It supports both a simple way to activate a service by type and an advanced case that calls an async callback which is encapsulated in the host's IHost.StartAsync().
It is located in the hosting abstractions assembly and requires no additional work for hosts other than to support IHostedService and optionally the new IHostedLifecycleService interface.
API Proposal
In the hosting abstractions assembly:
namespace Microsoft.Extensions.DependencyInjection;
public static partial class ServiceCollectionHostedServiceExtensions
{
// Simply activates the service by type
+ public static IServiceCollection AddStartupActivation<TService>
+ (this IServiceCollection services) where TService : class;
// Support the new keyed services feature
+ public static IServiceCollection AddKeyedStartupActivation<TService>
+ (this IServiceCollection services, object? serviceKey) where TService : class;
// Activates the service by type and calls an async method that allows access to the service
+ public static IServiceCollection AddStartupActivation<TService>
+ (this IServiceCollection services,
+ Func<IServiceProvider, TService, CancellationToken, Task> activatorFunc)
+ where TService : class;
// Support the new keyed services feature
+ public static IServiceCollection AddKeyedStartupActivation<TService>
+ (this IServiceCollection services, object? serviceKey,
+ Func<IServiceProvider, TService, CancellationToken, Task> activatorFunc)
+ where TService : class;
}Usage examples
[Fact]
public async void ActivateByType()
{
var hostBuilder = CreateHostBuilder(services =>
{
services
.AddSingleton<ActivateByType_Impl>()
.AddStartupActivation<ActivateByType_Impl>();
});
using (IHost host = hostBuilder.Build())
{
await host.StartAsync();
Assert.True(ActivateByType_Impl.Activated);
}
}
public class ActivateByType_Impl
{
public static bool Activated = false;
public ActivateByType_Impl()
{
Activated = true;
}
}
[Fact]
public async void ActivateByTypeWithCallback()
{
var hostBuilder = CreateHostBuilder(services =>
{
services
.AddSingleton<ActivateByTypeWithCallback_Impl>()
.AddStartupActivation<ActivateByTypeWithCallback_Impl>(async (service, sp, cancellationToken) =>
{
await service.DoSomethingAsync(cancellationToken);
});
});
using (IHost host = hostBuilder.Build())
{
await host.StartAsync();
Assert.True(ActivateByTypeWithCallback_Impl.Activated);
Assert.True(ActivateByTypeWithCallback_Impl.DidSomethingAsync_Before);
Assert.True(ActivateByTypeWithCallback_Impl.DidSomethingAsync_After);
}
}
public class ActivateByTypeWithCallback_Impl
{
public static bool Activated = false;
public static bool DidSomethingAsync_Before = false;
public static bool DidSomethingAsync_After = false;
public ActivateByTypeWithCallback_Impl()
{
Activated = true;
}
public async Task DoSomethingAsync(CancellationToken cancellationToken)
{
DidSomethingAsync_Before = true;
await Task.Delay(10, cancellationToken);
DidSomethingAsync_After = true;
}
}Prototype at https://github.com/steveharter/runtime/tree/SingletonWarmup.
Original issue description
AB#1244417
Until now a service is created when GetService method is called on the service provider, or when the service must be injected in a controller responsible for the coming http request.
A first limitation : a service whose instance is never injected or never asked (GetService) will be never instantiated. If this service do some init work (fill a memory cache for instance), this work will never be done.
Even if this service is required somewhere, we should have the option to force eager creation of the instance (when BuildServiceProvider method is called) and not when the service is required for the first time. startup != first use.
Second limitation : if the init work takes 3 seconds, I will have the the "cold start" problem.
It also has consequences on the context in which the initialization work his done. Example :
public class Foo : IFoo
{
public Foo()
{
DoSomeInitWork();
}
private void DoSomeInitWork()
{
// code
}
}Somewhere in my application :
using(var tx = new TransactionScope())
{
IFoo foo = serviceProvider.GetService<IFoo>();
// use foo
}Here, Foo will be instantiated inside the transaction. Let's say I cannot receive Foo by injection because the service I need must be obtained dynamically.
In FooTest :
public class FooTest
{
private IFoo foo;
public FooTest()
{
// service provider initialization
this.foo = serviceProvider.GetService<IFoo>();
}
}Here, Foo will be instantiated outside of the transaction.
If the DoSomeInitWork() method requires a transaction, it should first check if a transaction is ongoing before creating one.
Suggestion : add a property in the ServiceDescriptor class and a parameter for the AddSingleton méthods.