What you are getting at is the fundamental difference between a library, which is not prescriptive and a framework, which is prescriptive.
Using a library has less initial cognitive load, but using a framework usually has a higher initial cognitive load, and once you know the framework, it does a lot for you.
Ok, so all that aside, what I do with a layered architecture is provide nothing prescriptive in the library in terms of IoC (so no registrations, no nothing). A library should, then, be very reusable without thinking too much about dependencies.
For a framework, I will provide sane defaults for the registrations, and make them overridable. Pragmatically, that comes down to
- framework registers the things it knows about in each of the layers using concrete implementations for the interfaces. These concrete implementations are what's built into the framework.
- framework provides "convenience methods", abstract registration classes or profiles that wire up dependencies at the right level of abstraction to allow the caller to override these defaults.
API Scenario
For example, a framework that supports APIs needs to build URIs to related resources knows that it needs a context-aware IUriBuilder
, and an INavigationModel
to navigate to related resources; together these collaborate to allow an API to provide links along with resources.
That's the level of abstraction that will be imposed on the calling layer. The framework does not impose a lower level of abstraction on the caller because then the caller knows too much, and the caller may break what the framework needs.
Let's say the framework offers a rudimentary API-behind-a-reverse-proxy default. It registers all that for you. If that is not sufficient, your caller calls, let's say, a method called RegisterLinkBuildingService(...)
. This method replaces arbitrary, individual registrations through ConfigureServices
with a prescriptive method that forces the caller to provide specific concrete services the framework needs.
So, you say, OK, I need link building that is aware of all the proxies, API gateways, and such between my cloud and the rest of the world. You wouldn't register all of that at the API layer, because much of it is needed in whole forms at the framework layer*. Instead, you call that Register.. method above with your concrete services.
This integrates the two (or more) layers through contractual registration interfaces, encapsulating what is needed, but allowing upper layers some level of freedom.