I have a bit of a weird case involving DI, specifically in resolving implementation at runtime from within the same service. I'm aware that I could inject a service provider, but that would seemingly violate the dependency inversion principle.
Also, apologies if this ends up being more of a architectural/design question; I've recently switched from .NET Framework development and still getting acquainted with the limitations of DI. Note that I've simplified & changed the business context for obvious reasons, so keep in mind that the hierarchy/structure is the important part... For this question, I've decided to go with the classic example of an online retailer.
Project Overview/Example:
core library (.NET Class Library) - IRetailerService: public service consumed by client apps └ IOrderService: facade/aggregate services injected into ^ ├ IInventoryManager: internal components injected into facade/aggregate services as well as other components ├ IProductRespository └ IPriceEstimator
public class RetailerService : IRetailerService
{
private readonly IOrderService _orderService;
public OrderService( IOrderService orderService, ... ) { //... set injected components }
async Task IRetailerService.Execute( Guid id )
{
await _orderService.Get( id );
}
async Task IRetailerService.Execute( Guid id, User user )
{
await _orderService.Get( id, user );
}
}
internal class OrderService : IOrderService
{
public OrderService( IInventoryManager inventoryManager, IProductRespository productRepo, ... ) { }
async Task<object> IOrderService.Get( Guid id )
{
//... do stuff with the injected components
await _inventoryManager.Execute( ...args );
await _productRepo.Execute( ...args );
}
async Task<object> IOrderService.Get( Guid id, User user ) { }
}
The Problem:
Lets say I want to log IOrderService.Get( Guid id, User user )
, but only when this override with the User
is provided - this includes logging inside the injected components (InventoryManager, IProductRepository, etc.) as well.
The only solutions I can see at the moment are to either:
- Add an additional layer to this hierarchy & use named registration with scope lifetimes to determine if a null vs logging implementation is passed down.
- Inject the service provider into the public facing service
IRetailerService
, and somehow pass down the correct implementation.
I think my ideal solution would be some type of decorator/middleware to control this... I've only given the core library code; but there is also a WebApi project within the solution that references this library. Any ideas/guidance would be greatly appreciated.
CodePudding user response:
You can resolve your dependencies in the IOrderService.Get
method at runtime so that each method has its own dependencies. Nevertheless this doesn't fully resolve your problem. Nested dependencies IInventoryManager inventoryManager, IProductRespository productRepo, ...
should be able to enable logging as well.
So instead you may use:
internal class OrderService : IOrderService
{
public OrderService( IServiceProvider serviceProvider) { }
async Task<object> IOrderService.Get( Guid id )
{
var inventoryManager = (IInventoryManager)serviceProvider.GetService(typeof(IInventoryManager));
inventoryManager.Logging = false;
var productRepo = (IProductRespository)serviceProvider.GetService(typeof(IProductRespository));
productRepo.Logging = false;
//... do stuff with the injected components
await inventoryManager.Execute( ...args );
await productRepo.Execute( ...args );
}
async Task<object> IOrderService.Get( Guid id, User user ) {
var inventoryManager = (IInventoryManager)serviceProvider.GetService(typeof(IInventoryManager));
inventoryManager.Logging = false;
var productRepo = (IProductRespository)serviceProvider.GetService(typeof(IProductRespository));
productRepo.Logging = true;
//... do stuff with the injected components
await inventoryManager.Execute( ...args );
await productRepo.Execute( ...args );
}
}
You may also provide a Factory / Builder with a parameter to enable logging. But in any case because you want a different behavior in nested classes starting from a same root class, this may be complicated.
Another option is to provide 2 implementations of IOrderService, one that include logging, and the other not. But I'm not sure this may help you because you had probably good reasons to provide an overload to the method and not split them into separate services. And this doesn't resolve the issue for nested injections.
Last option may be to use a singleton LoggingOptions class. Each dependency has a dependency on this class and because this is a singleton, each time you enter your overload you set it to true and so all classes are informed of your intent to log. Nevertheless this highly depends of your architecture. If both methods may be called nearly on the same time, this may break the nested dependencies logging behavior or interrupt the logging at any time.
Take a look at this question this may help. By considering this question, you may provide a Factory for each of your dependency (including nested ones) that would set logging behavior on each call to the overload method.
CodePudding user response:
I would recommend using a factory to create the order service, and any downstream dependencies that need the logger. Here is a fully worked example:
void Main()
{
var serviceProvider = new ServiceCollection()
.AddScoped<IRetailerService, RetailerService>()
.AddScoped<IInventoryManager, InventoryManager>()
.AddScoped<IOrderServiceFactory, OrderServiceFactory>()
.BuildServiceProvider();
var retailerService = serviceProvider.GetRequiredService<IRetailerService>();
Console.WriteLine("Running without user");
retailerService.Execute(Guid.NewGuid());
Console.WriteLine("Running with user");
retailerService.Execute(Guid.NewGuid(), new User());
}
public enum OrderMode
{
WithUser,
WithoutUser
}
public interface IOrderServiceFactory
{
IOrderService Get(OrderMode mode);
}
public class OrderServiceFactory : IOrderServiceFactory
{
private readonly IServiceProvider _provider;
public OrderServiceFactory(IServiceProvider provider)
{
_provider = provider;
}
public IOrderService Get(OrderMode mode)
{
// Create the right sort of order service - resolve dependencies either by new-ing them up (if they need the
// logger) or by asking the service provider (if they don't need the logger).
return mode switch
{
OrderMode.WithUser => new OrderService(new UserLogger(), _provider.GetRequiredService<IInventoryManager>()),
OrderMode.WithoutUser => new OrderService(new NullLogger(), _provider.GetRequiredService<IInventoryManager>())
};
}
}
public interface IRetailerService
{
Task Execute(Guid id);
Task Execute(Guid id, User user);
}
public interface IOrderService
{
Task Get(Guid id);
Task Get(Guid id, User user);
}
public class User { }
public class RetailerService : IRetailerService
{
private readonly IOrderServiceFactory _orderServiceFactory;
public RetailerService(
IOrderServiceFactory orderServiceFactory)
{
_orderServiceFactory = orderServiceFactory;
}
async Task IRetailerService.Execute(Guid id)
{
var orderService = _orderServiceFactory.Get(OrderMode.WithoutUser);
await orderService.Get(id);
}
async Task IRetailerService.Execute(Guid id, User user)
{
var orderService = _orderServiceFactory.Get(OrderMode.WithUser);
await orderService.Get(id, user);
}
}
public interface ISpecialLogger
{
public void Log(string message);
}
public class UserLogger : ISpecialLogger
{
public void Log(string message)
{
Console.WriteLine(message);
}
}
public class NullLogger : ISpecialLogger
{
public void Log(string message)
{
// Do nothing.
}
}
public interface IInventoryManager { }
public class InventoryManager : IInventoryManager { }
internal class OrderService : IOrderService
{
private readonly ISpecialLogger _logger;
public OrderService(ISpecialLogger logger, IInventoryManager inventoryManager)
{
_logger = logger;
}
public async Task Get(Guid id)
{
_logger.Log("This is the 'id-only' method");
}
public async Task Get(Guid id, User user)
{
_logger.Log("This is the 'id-and-user' method");
}
}
Using this, you get the following output:
Running without user
Running with user
This is the 'id-and-user' method
The factory lets you have complete control of how the downstream components are generated, so you can get as complicated as you want.