-
Notifications
You must be signed in to change notification settings - Fork 5.3k
Description
Background and motivation
There are cases where it is necessary to inject a dependency as a result of an asynchronous operation. This usually comes up when the dependency requires input from some IO operation (like retrieving a secret from a remote store). Today, developers tend to put blocking calls in factories:
var services = new ServiceCollection();
services.AddSingleton<IRemoteConnectionFactory, RedisConnectionFactory>();
services.AddSingleton<IRemoteConnection>(sp =>
{
var factory = sp.GetRequiredService<IRemoteConnectionFactory>();
// NOOOOOO 😢
return factory.ConnectAsync().Result;
});
ServiceProvider sp = services.BuildServiceProvider();
IRemoteConnection connection = await sp.GetRequiredServiceAsync<IRemoteConnection>();
public interface IRemoteConnection
{
Task PublishAsync(string channel, string message);
Task DisposeAsync();
}
public interface IRemoteConnectionFactory
{
Task<IRemoteConnection> ConnectAsync();
}The only other viable solution is to move that async operation to method calls, which results in deferring all IO until methods are called (where things can truly be async). This is a non-trivial refactoring that might be impossible depending on the circumstance. The idea here is to provide asynchronous construction support, so that these scenarios can work.
API Proposal
Async Resolution Support
namespace Microsoft.Extensions.DependencyInjection;
public class ServiceDescriptor
{
public static ServiceDescriptor Describe(Type serviceType, Func<IServiceProvider, ValueTask<object>> asyncFactory, ServiceLifetime serviceLifetime) => null;
}
// These are extension methods that take an async factory
public static class ServiceCollectionExtensions
{
public static IServiceCollection AddSingleton<TService>(this IServiceCollection services, Func<IServiceProvider, ValueTask<TService>> asyncFactory);
public static IServiceCollection AddSingleton(this IServiceCollection services, Type serviceType, Func<IServiceProvider, ValueTask<object>> asyncFactory);
public static IServiceCollection AddScoped<TService>(this IServiceCollection services, Func<IServiceProvider, ValueTask<TService>> asyncFactory);
public static IServiceCollection AddScoped(this IServiceCollection services, Type serviceType, Func<IServiceProvider, ValueTask<object>> asyncFactory);
public static IServiceCollection AddTransient<TService>(this IServiceCollection services, Func<IServiceProvider, ValueTask<TService>> asyncFactory);
public static IServiceCollection AddTransient(this IServiceCollection services, Type serviceType, Func<IServiceProvider, ValueTask<object>> asyncFactory);
}
public static class AsyncServiceProviderExtensions
{
public static async ValueTask<T> GetRequiredServiceAsync<T>(this IServiceProvider serviceProvider);
public static async ValueTask<object> GetServiceAsync(this IServiceProvider serviceProvider, Type serviceType);
}Async Injection Support
These APIs would use the convention that async resolution is tied to ValueTask/Task<TServiceType> and would resolve the service and await the result as part of construction (see the example for more details).
namespace Microsoft.Extensions.DependencyInjection;
public delegate ValueTask<object> AsyncObjectFactory(IServiceProvider serviceProvider, object?[]? args);
public delegate ValueTask<T> AsyncObjectFactory<T>(IServiceProvider serviceProvider, object?[]? args);
public static class ActivatorUtilities
{
public static AsyncObjectFactory CreateAsyncFactory(Type type);
public static AsyncObjectFactory<T> CreateAsyncFactory<T>();
}NOTE: The generic version could be done using static abstract interface methods and would be more trim friendly.
API Usage
var services = new ServiceCollection();
// This implementation will add `Task<IRemoteConnection>` to the container.
services.AddSingleton<IRemoteConnection>(sp =>
{
var factory = sp.GetRequiredService<IRemoteConnectionFactory>();
return factory.ConnectAsync();
});
ServiceProvider sp = services.BuildServiceProvider();
IRemoteConnection connection = await sp.GetRequiredServiceAsync<IRemoteConnection>();
AsyncObjectFactory<B> factory = ActivatorUtilities.CreateAsyncFactory<B>(sp);
B dep = await factory(sp, null);
public class A
{
private IRemoteConnection _connection;
private A(IRemoteConnection connection) { _connection = connection }
// We can't use a constructor here since the container doesn't natively understand async dependencies
public static A Create(IRemoteConnection connection)
{
return new A(connection);
}
}
public class B
{
private A _a;
private B(A a) { _a = a }
// We can't use a constructor here since the container doesn't natively understand async dependencies
public static B Create(A a)
{
return new B(a);
}
}
public interface IRemoteConnection
{
Task PublishAsync(string channel, string message);
Task DisposeAsync();
}
public interface IRemoteConnectionFactory
{
Task<IRemoteConnection> ConnectAsync();
}Risks
- Need to determine what happens when async services are resolved from a sync call to GetService (we should throw).
3rd party DI containers would need to support this- There are no async constructors, we need to invent a constructor surrogate for this (see the example).
Implementation complexity (but we can handle this 😄). It's a bit easier if we do it as an extension.