In a layered architecture, how can the Application layer know about Web URLs?

Viewed 360

I'm currently working on a .NET 5 app that is using a layered architecture (Web / Application / Infrastructure / Domain). If I am to follow the onion/clean architecture pattern, the dependencies should flow in one direction only, e.g:

Web -> Application -> Infrastructure -> Domain

I now find myself needing to send several emails from the Application layer containing specific front-end URLs. This means that the Application layer will know about the Web layer, breaking the dependency flow.

A sample use case flow would be:

  1. User makes a request, gets handled by a controller in the Web layer
  2. Controller calls a handler on the Application layer
  3. The Application layer uses an email service from the Infrastructure layer to send an email

On step #3 I'm in the Application layer but need Web URLs to construct the email body.

How can I solve for this issue?

2 Answers

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.

Consider configuring "well known urls" at the web infrastructure level (gateway or load balancer for example) so you can have "mycompany.com/user-action-1" in the email and that will translate to the proper endpoint of your web app.

Related