Let’s say I have some DDD service that requires some IEnumerable<Foo>
to perform some calculations. I came up with two designs:
Abstract the data access with an
IFooRepository
interface, which is quite typicalpublic class FooService { private readonly IFooRepository _fooRepository; public FooService(IFooRepository fooRepository) => _fooRepository = fooRepository; public int Calculate() { var fooModels = _fooRepository.GetAll(); return fooModels.Sum(f => f.Bar); } }
Do not rely on the
IFooRepository
abstraction and injectIEnumerable<Foo>
directlypublic class FooService { private readonly IEnumerable<Foo> _foos; public FooService(IEnumerable<Foo> foos) => _foos = foos; public int Calculate() => _foos.Sum(f => f.Bar); }
This second design seems better in my opinion as FooService
now does not care where the data is coming from and Calculate
becomes pure domain logic (ignoring the fact that IEnumerable
may come from an impure source).
Another argument for using the second design is that when IFooRepository
performs asynchronous IO over the network, usually it will be desirable to use async-await
like:
public class AsyncDbFooRepository : IFooRepository
{
public async Task<IEnumerable<Foo>> GetAll()
{
// Asynchronously fetch results from database
}
}
But as you need to async all the way down, FooService
is now forced to change its signature to async Task<int> Calculate()
. This seems to violate the dependency inversion principle.
However, there are also issues with the second design. First of all, you have to rely on the DI container (using Simple Injector as an example here) or the composition root to resolve the data access code like:
public class CompositionRoot
{
public void ComposeDependencies()
{
container.Register<IFooRepository, AsyncDbFooRepository>(Lifestyle.Scoped);
// Not sure if the syntax is right, but it demonstrates the concept
container.Register<FooService>(async () => new FooService(await GetFoos(container)));
}
private async Task<IEnumerable<Foo>> GetFoos(Container container)
{
var fooRepository = container.GetInstance<IFooRepository>();
return await fooRepository.GetAll();
}
}
Also in my specific scenario, AsyncDbFooRepository
requires some sort of runtime parameter to construct, and that means you need an abstract factory to construct AsyncDbFooRepository
.
With the abstract factory, now I have to manage the life cycles of all dependencies under AsyncDbFooRepository
(the object graph under AsyncDbFooRepository
is not trivial). I have a hunch that I am using DI incorrectly if I opt for the second design.
In summary, my questions are:
- Am I using DI incorrectly in my second design?
- How can I compose my dependencies satisfactorily for my second design?