Chain of Responsibility pattern for handling cross-cutting concerns
October 27, 2019Introduction
I recall that in my junior years as a professional developer lot of systems I worked on introduced tons of duplication in regards to cross-cutting concerns. For example, most of the application-layers` methods would follow such a template:
public ResultClass DoSomething(InputClass input) {
Log.Information("DoSomethin started");
using(var unitOfWork = unitOfWorkProvider.Get()) {
// .. actual work
unitOfWork.Complete();
}
Log.Information("DoSomething completed");
}
Code regarding the unit of work, logging, error handling, etc. was mostly copied from method to method without much of a thought. Obviously, such an approach regularly resulted in various bugs. Sometimes the Complete
method would be accidentally lost during the copying, other times log message was not adjusted so it conveyed incorrect information.
Additionally, adding any other cross-cutting concern was out of the question because it would impact all methods in all services.
At that time, I thought that only compilation-time tools like postsharp could help us, by generating a "boilerplate" code automatically. Luckily, later I realized that standard design patterns were perfectly capable of handling cross cutting concerns without compromising SOLID or DRY principles.
Constructing better alternative
In general, the code I showed above followed the abstract pattern.
Handle concern A
Handle concern B
Handle concern C, ...
Do Actual Work
Complete concern ... , C
Complete concern B
Complete concern A
We could point out that basically we first build the stack of concerns and after the actual work is done we pop each concern and complete it. That could be a major design tip for us. We could devise a solution based on the actual stack of objects each handling some cross-cutting concern.
Obviously, for this approach to be extensible, each "handler" should not know about other handlers, yet, they need to cooperate. Order of the handlers does matter, so some kind of root object must be responsible for arranging them.
That's when our pattern comes into play. It is designed to do exactly what we just described. If you don't know this pattern already take a look at those great explanations on refactoring.guru or sourcemaking.com
Chain of responsibility in the wild
Although almost no one declares their system as implementing a chain of responsibility pattern, most successful, extensible libraries do, in one form or the other. Let's take a look at a few examples
ASP.NET Core middlewares
Maybe one of the most familiar ones is the concept of ASP.NET Core
middleware. The documentation defines middleware following:
Middleware is software that's assembled into an app pipeline to handle requests and responses. Each component:
- Chooses whether to pass the request to the next component in the pipeline.
- Can perform work before and after the next component in the pipeline.
As in every application of design pattern, they have their own vocabulary to describe actors in this system. Still, the cooperation pattern is implementing a chain of responsibility.
MediatR behaviors
MediatR is a library that on the top level implements the mediator pattern. It introduces an abstraction layer between the source of the command and its handler. However, each command, before it will be passed to the handler is processed by all registered pipeline behaviors.
Take a look at typical behavior for wrapping action in a transaction scope. Again, we can see the familiar next
parameter wrapped by some logic preceding and following the invocation.
public class TransactionalBehavior<TRequest, TResponse> : IPipelineBehavior<TRequest, TResponse> {
public async Task<TResponse> Handle(TRequest request, CancellationToken cancellationToken, RequestHandlerDelegate<TResponse> next) {
using (var tx = new TransactionScope(TransactionScopeAsyncFlowOption.Enabled))
{
await SomeMethodInTheCallStackAsync()
.ConfigureAwait(false);
tx.Complete();
}
}
}
Angular interceptors
@Injectable()
export class AuthorizationInterceptor implements HttpInterceptor {
intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
token = this.auth.getToken() // auth is provided via constructor.
if (token) {
// Logged in. Add Bearer token.
return next.handle(
req.clone({
headers: req.headers.append('Authorization', 'Bearer ' + token)
})
);
}
// Not logged in. Continue without modification.
return next.handle(req);
}
}
Same story. If you know what to look for (request object req
and next
handler) you immediately recognize the chain of responsibility pattern.
For more examples of angular interceptors take a look at this post from angulardepth.com
Others
- Serilog enrichers
- NServiceBus pipeline behaviors
- ASP.NET MVC filters
- Entity Framework interceptors
Applying the pattern in applications
To use this pattern effectively you need to first identify all composition roots in your application. By composition root, I mean every kind of entry point to your application. Typical ones include:
- Web endpoint
- Message queue listener
- Batch jobs
- Background tasks
It is necessary to devise a solution per composition-root because cross-cutting concerns are going to be specific for each entry-point. As an example, your web endpoints will probably need some kind of "authorization handler" whereas message queue listeners probably not. On the other hand, both endpoints could use the same "transactions handler".
After you identified the composition roots you need to make sure that each composition root's dependency injection container can be configured independently. Obviously we aim to use full-blown DI for our solution and common configuration for all composition roots would render our efforts unnecessarily harder or even impossible for less sophisticated containers.
In the end, set up a "pipeline" in front of each of your composition roots. Each request/action should go through the pipeline and be processed by handlers of cross-cutting concerns. The actual handler executing the core logic should be free of dependencies to cross cutting concerns, focused on domain logic.
From my experience, such setup is the pillar of SOLID architecture and maintainable software.
Among many concerns that could be handled in such a manner you can find:
- Logging
- Transactions
- Authorization
- Profiling
- Exception-handling
- Throttling
- Caching
- Retrying
- Timeouts
Summary
Hopefully, I conveyed the following key points:
- Chain of responsibility it a simple yet powerful solution to handling cross-cutting concerns.
- Many extensible libraries implement the pattern, knowingly or not.
- You should have a "pipeline" in front of each composition root's entry-point.