I've recently solved this problem within my organization. In our case we have an API "market place" used by the company as a whole, then a reverse proxy used by closely integrated clients, and finally an internal load balancer for the API containers.
That's 3 layers of URL knowledge that my Web and App layers shouldn't know about (even at the Web layer it shouldn't know this because that would make our web layer have more than one responsibility rather than just be a router (e.g. via Mediatr)).
Using Rewriters in the Infrastructure
This is what Z. Danev's answer is all about. This works, but you must maintain all the rules for each of these layers, and each of those rewrites may add overhead. Also, those rules could get tricky depending on the complexity of the data you return.
It is a valid solution though. Depending on your organization this may be an easy thing, or it may be a hard one because it's maintained by other teams, need work tickets, and so on to get the job done.
Well, if you can't or don't want to do that, then...
Application Layer Dependency Inversion and Patterns
Disclaimer: This solution works great for us, but it does have one drawback: at some level, you have to maintain something that knows about the layers above. So caveat emptor.
The situation I described above is roughly analogous to your problem, though perhaps more complex (you can do the same but simplify it). Without violating your architectural principals you need to provide an interface (or more than one) that can be injected into your application layer as an application service.
We called ours ILinkBuilderService
and created a LinkBuilderService
that itself can be wired up through a DI container with individual ILinkBuilder
implementations. Each of these implementations could be a MarketPlaceBuilder
, a GatewayBuilder
, etc. and will be arranged according to a Chain of Responsibility and Strategy patterns from outermost proxy to innermost.
In this way, the builders inspect the web context (headers, request, etc.) to determine which one should handle the responsibility of building links. Your application layer (e.g. your email sender) simply calls the link building service interface with key data, and this is used to generate client-facing URLs without exposing the application layer to the web context.
Without going into too many details, these builders inspect headers like X-Forwarded-For, custom headers, and other details provided by proxies as the HTTP request hits each endpoint. Chain of Responsibility is key, because it allows the application layer to generate the correct URL no matter at which layer the request originated from.
So how does this not break the one-way flow?
Well, you push these builders one-way down into your application layer. Technically, they do reach back up to the web layer for context, but that is encapsulated. This is ok and does not violate your architecture. This is what dependency inversion is all about.
appsettings.json
for more info check this – Surgy