Search code examples
c#.netdependency-injectionlazy-loading

Resolve Injecting indirect service with lazy loading


I would like some advice on resolving dependency inject in the context of 'indirect inject'. consider the example below.

Consider I have three interfaces, namely: IAdminService, IEmailService, and IBillService. Each has a single implementation type, i.e. AdminService, EmailService, and BillService.

here is an example of IAdminService with its implementing type.

public interface IAdminService 
{
    void DoSomething();
}

public class AdminService : IAdminService 
{
    IServiceA A { get; }

    public AdminService (IServiceA A)
    {
        this.A = A;
    }

    public void DoSomething(){
    {
        A.QuerySomething();
    }
}

On top of that I have an IServiceManagement abstraction, that makes use of lazy loading. see below.

public interface IServiceManagement
{
    IAdminService AdminService{ get;  }
    IEmailService IEmailService{ get; }
    IBillService BillService { get; }
}

public sealed class ServiceManagement : IServiceManagement
{
    private readonly Lazy<IAdminService> _adminService;
    private readonly Lazy<IEmailService> _emailService;
    private readonly Lazy<IBillService> _billService;

    public ServiceManagement (
        IServiceA ServiceA,
        IServiceB ServiceB,
        IserviceC serviceC,
        ...)
    {
        _adminService = new Lazy<IAdminService>(() => new AdminService(ServiceA));
        _emailService = new Lazy<IEmailService>(() => new EmailService(...));
        _billService = new Lazy<IBillService>(() => new BillService(...));
    }

    public IAdminService AdminService => _adminService.Value;
    public IEmailService EmailService => _emailService.Value;
    public IBillService BillService => _billService.Value;
}

Service are registered as a scoped service (Web API), see extension method below below.

public static IServiceCollection RegisterLogicServices(
    this IServiceCollection services)
{
    services.AddScoped<IServiceManagement, ServiceManagement>();

    //generic services like UnitOfWork for E.F. Core etc.
    services.AddScoped<IServiceA , ServiceA>();
    services.AddScoped<IServiceB , ServiceB>();
    services.AddScoped<IServiceC , ServiceC>();

    return services;
}

For example, to make use of IServiceManagement at the controller level, I inject IServiceManagement as constructor argument, allowing me to access all publicly exposed methods of the service, as follows:

public DoSomethingController(IServiceManagement manager)
{
  _manager = manager
}

public IActionResult ExecuteSomething()
{
    _manager.AdminService.DoSomething();

    return Ok();
}

Everything works perfectly at this point.

What I'm wondering, however, is whether it is possible to have scope of IServiceManagement in IAdminService, so i would like to execute something in IEmailService in IAdminService via IServiceManagement.

A way I can think of doing this is by using IServiceProvider, and passing in provider.GetRequiredService<IServiceManagement>() as a property of the constructor, like below

public ServiceManagement (
    IServiceA ServiceA,
    IServiceB ServiceB,
    IserviceC serviceC,
    IServiceProvider provider,
    ...)
{
    _adminService = new Lazy<IAdminService>(
        () => new AdminService(
            ServiceA,
            provider.GetRequiredService<IServiceManagement>()));
    _emailService = new Lazy<IEmailService>(() => new EmailService(...));
    _billService= new Lazy<IBillService>(() => new BillService(...));
}

I'd do that for all of the services, cause there might be a scenario where IAdminService calls a method in IEmailService, and that same method in IEmailService might call a method in IBillService, etc.

What i would like to know, is making use of IServiceProvider for this use case a valid one, for example,

  • is it valid in the context of Dependency Injection?
  • would I run into issues when accessing anything from IServiceProvider?
  • would this cause performance degradation if the call tree is to big (or because its scoped it should be fine)?
  • etc.

And lets take the extreme case and say I inject IServiceManagment into a Background Service, would that cause any issues.

If you have another solution that solves my worries, please mention it, i'd like to learn as much as possible on it.


Solution

  • is it valid in the context of Dependency Injection?

    No, it probably isn't.

    What's missing from your question is why exactly you require lazy loading. This is important, because in general injection constructors should be simple. Having simple injection constructors makes object construction fast and diminishes the need to do lazy loading. Without lazy loading, there is no need in having the IServiceManagement around. This simplifies your solution and removes your current issue: the cyclic dependency.

    But even when it's impossible to remove a slow loading dependency, wrapping dependencies in an IServiceManagement abstraction is not a proper solution. By doing this, you are leaking implementation details: the fact that a service is slow to load. Consuming dependencies shouldn't be aware of that. On top of that, by exposing all of IServiceManagement's dependency, you're creating a solution where you hide the actual number of dependencies that a class uses, and making it harder to test classes, because you have to manage an IServiceManagement implementations in your tests as well. And in the end, IServiceManagement will become that one single dependency that all classes depend upon; much like the Service Locator anti-pattern.

    Instead, the use of proxy implementations is a more elegant solution. In this case you implement, for instance, an IAdminService implementation that allows lazy loading the real IAdminService implementation. For instance:

    public sealed class LazyAdminService : IAdminService
    {
        private readonly Lazy<IAdminService> lazy;
        public LazyAdminService(Lazy<IAdminService> lazy) => this.lazy = lazy;
    
        public void DoSomething() => this.lazy.Value.DoSomething();
    }
    

    Assuming your using MS.DI as your DI Container, you can wire this up as follows:

    services.AddScoped<AdminService>();
    services.AddScoped<IAdminService>(c => new LazyAdminService(
        new Lazy<IAdminService>(() => c.GetRequiredService<AdminService>()));
    

    This is more elegant, because the consumers of IAdminService have no notion of AdminService requiring lazy initialization, and they didn't require any change once AdminService becoming lazily initialized.