Async Initialisation for Transient ASP.NET core

Viewed 805

I have a transient service that has to do async initialization (connecting to a different server). It exposes a property public Task InitTask which one can wait till the service is initlized. It also exposes subservices that can be savely accessed after the InitTask has finished.

public class Service
{
    public Task InitTask { get; }

    public ISubService1 SubService1 { get; }
    public ISubService2 SubService2 { get; }
    public ISubService3 SubService3 { get; }
}

All functionallity that the connection to the other server provides is capulated by these sub services. Normally I would inject the main service and then wait for it to be finished initlaization and then use one of these sub services. But I would like to just inject these subservices.

At first I tried

services.AddTransient<Service>()
    .AddTransient(provider =>
    {
        var server = provider.GetService<Service>();
        return server.SubService1;
    })
    .AddTransient(provider =>
    {
        var server = provider.GetService<Service>();
        return server.SubService2;
    })
    .AddTransient(provider =>
    {
        var server = provider.GetService<Service>();
        return server.SubService3;
    });

but this has the obvious problem of not awaiting the InitTask of the main service.

Then (since the sub-services are exposed via interfaces) I tried to code wrapper classes for the sub-services like

public class WrapperSubService1 : ISubService1
{
    private readonly Service server;

    public WrapperSubService1(Service server)
    {
        this.server = server;
    }

    private async ValueTask<ISubService1> GetSubService1Async()
    {
        await server.InitTask
        return server.SubService1;
    }

    // interface implementations

    public async Task<Example> GetExampleAsync(...) 
    {
        var subService1 = await this.GetSubService1Async();

        return await subService1.GetExampleAsync(...);
    }

    // many more (also some events and properties)
}

and do at startup

services.AddTransient<Service>()
    .AddTransient<ISubService1, WrapperSubService1>()
    .AddTransient<ISubService2, WrapperSubService2>()
    .AddTransient<ISubService3, WrapperSubService3>();

but this has also an obvious flaw: code duplication.

What I would wish for would be something like:

services.AddTransient<Service>()
    .AddTransient(async provider =>
    {
        var server = provider.GetService<Service>();
        await server.InitTask;
        return server.SubService1;
    })
    .AddTransient(async provider =>
    {
        var server = provider.GetService<Service>();
        await server.InitTask;
        return server.SubService2;
    })
    .AddTransient(async provider =>
    {
        var server = provider.GetService<Service>();
        await server.InitTask;
        return server.SubService3;
    });

but this then exposes just Task<SubService1> for injection.

Any ideas?

3 Answers

I think you can actually make use of explicit interface implementations in this case:

Change the Service class to

public class Service : ISubService1, ISubService2, ISubService3
{
    public Task InitTask { get; }

    public ISubService1 SubService1 { get; }
    public ISubService2 SubService2 { get; }
    public ISubService3 SubService3 { get; }

....


    private async ValueTask<ISubService1> GetSubService1Async()
    {
        await server.InitTask
        return server.SubService1;
    }

    // interface implementations

    async Task<Example> ISubService1.GetExampleAsync(...) 
    {
        var subService1 = await this.GetSubService1Async();

        return await subService1.GetExampleAsync(...);
    }

....

    async Task<Example> ISubService2.GetExampleAsync(...) 
    {
        var subService1 = await this.GetSubService2Async();

        return await subService1.GetExampleAsync(...);
    }
// and so on
}

The only solutions I've found for this kind of situation is to either:

  1. Make your initialization synchronous. On ASP.NET in particular, I think this is a fine option, since it makes sense to not receive requests until the initialization is complete.
  2. Have a separate "schema deployer" project that does that kind of initialization (creating queues/tables) before your service is even deployed. This is the solution I tend to use in modern apps.

I do think exposing Task<SubService1> is fine.

As for the code that resolves the service, it looks like:

var subService1 = await serviceProvider.GetService<Task<SubService1>>();
var result = await subService1.GetExampleAsync();

It looks graceful. Or you can write an extension method like:

public static Task<T> GetServiceAsync<T>(this IServiceProvider provider) => provider.GetService<Task<T>>();

Then

var subService1 = await serviceProvider.GetServiceAsync<SubService1>();

would be more clear.

Additionally, the design of Microsoft.Extensions.DependencyInjection based default DI does not work like an object container, but just an IoC interface (This is different from the well-known DI Containers like autofac). We are not supposed to retrieve every single object directly from the provider, even though it may look more graceful.

Therefore getting a subservice directly from the main service subService1 = provider.GetService<MainService>().GetService1Async(); would be the recommended way.

Related