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,
IServiceProvider
?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.
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.