There is more than one way to skin a cat.
Your question is asking how something is done, but it's hard to provide an accurate answer when there are many possible different approaches. I've decided to pick the most commonly used one to answer with. Keep in mind that this is not the only possible approach.
Imagine we have an “IItemRepo” interface implemented by the Persistence mechanism.
- Where this object will be instantiated?
Services (i.e. objects that aren't DTOs) are generally instantiated by the DI container. You don't actually have to do anything here, other than register the concrete type to the interface:
services.AddScoped<IItemRepository, MyItemRepository>();
In short, what this config line does is the same as telling the DI container:
Hey, whenever you are creating an object, and its constructor asks for an IItemRepository
parameter, I want you to pass it an instance of MyItemRepository
.
You effectively do this registration for each and every service in your codebase. Then, when you want to instantiate your top level service (e.g. web controller), the DI container is capable of providing each related service, all the way down to the bottom of the call stack, with all of the dependencies that they need injected.
- Who will have knowledge of this object?
Of the object, not type? That depends on how you configure your DI container. In the example above, the method name specifies the approach.
AddTransient
means that all injected IItemRepository
dependencies will always get a freshly generated MyItemRepository
instance, it is never reused.
AddSingleton
means that all injected IItemRepository
dependencies will refer back to the same singular MyItemRepository
instance.
AddScoped
behaves just like AddSingleton
, but on a smaller level. Instead of reusing it across the entire runtime, it reuses it in a smaller scope. For web based applications, that scope is defined as the incoming web request. For a given web request, the same service instance will be reused, but a different web request will get a different service instance. Because of that, AddScoped
is the most commonly preferred option.
As to the "who has knowledge" part of the question, it's essentially anyone who has a dependency on the given type.
- How the information that it provides will end up be used by the Domain/Application layer? Factories, but how exactly?
This question is unclear. I suspect that you're thinking of factories in terms of creating the services (in this case the repository instance).
The DI container I've been speaking of is a kind of factory. It's just a really intelligent and customizable one, which is able to generate an instance of any registered type.
- Will this object be passed around from layer to layer?
Yes and no. Yes, some of the things you observe are correct, but no, your conclusion does not follow.
Yes, it is possible for services from multiple layers to both have a dependency of the same type. Yes, when that dependency is not configured to be transient, that means that both of these services from multiple layers will receive the same instance of that dependency.
On a larger scope, yes, the DI container and its configuration must contain a whole lot of information on all of your layers, aggregated together. That is the top-level application's responsibility (e.g. your Web project).
No, this does not count as "passing around data". Object initialization is different from object usage, when dealing with dependency injection.
In any application, to some degree all of its classes are connected into a single web of dependency/references. If there were two completely separated webs in your codebase, you'd have two completely different applications.
When using dependency injection, that "web" is defined using the constructor parameters. Therefore, the constructors no longer conceptually count as the data highway, but rather as application architecture.
Aren’t DTO’s the only objects allowed this kind of behavior?
You're inverting the logic here. It's not that DTO's are the only one allowed to cross the layer boundary. It's that we call something a DTO when it crosses the layer boundary.
But similar as mentioned above, when using dependency injection, constructors are part of the architecture club in your codebase, which excludes their usage as being considered data-related, but rather architecture-related. And that's a different ballgame with different rules.