4

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?

marc_s
  • 732,580
  • 175
  • 1,330
  • 1,459
Ackdari
  • 3,222
  • 1
  • 16
  • 33
  • Related: https://stackoverflow.com/questions/32512266/how-to-perform-async-initalization-of-lazy-injection – Steven Jun 22 '20 at 11:20
  • Related: https://stackoverflow.com/questions/43240405/async-provider-in-net-core-di – Steven Jun 22 '20 at 11:20
  • Related: https://stackoverflow.com/questions/45924027/avoiding-all-di-antipatterns-for-types-requiring-asynchronous-initialization – Steven Jun 22 '20 at 11:20
  • 2
    Why are the sub-services defined as properties of `Service`? If they can be used in isolation, but depend on `Service`, then why not inject `Service` into your sub-services? – Johnathan Barclay Jun 22 '20 at 11:29

3 Answers3

0

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
}
Legacy Code
  • 650
  • 6
  • 10
0

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.
Stephen Cleary
  • 437,863
  • 77
  • 675
  • 810
0

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.

Alsein
  • 4,268
  • 1
  • 15
  • 35