Balancing Cross-Cutting Concerns in Clean Architecture
Read on: milanjovanovic.tech
Read time: 6 minutes
The .NET Weekly is brought to you by:
ABP Framework is a platform for building enterprise-grade ASP.NET Core applications. It comes with production-ready components, modular architecture, and Domain-Driven Design. Learn more here. New users get 3 months for free when purchasing a yearly subscription (available 22-26 January).
Become a Postman master with Postman Intergalactic sessions. Test your APIs with ease and collaborate on API development with workspaces. And it's free. Get Postman here!
|
Cross-cutting concerns are software aspects that affect the entire application. These are your common application-level functionalities that span several layers and tiers. Cross-cutting concerns should be centralized in one location. This prevents code duplication and tight coupling between components.
A few examples of cross-cutting concerns are:
- Authentication & Authorization
- Logging and tracing
- Exception handling
- Validation
- Caching
In today's newsletter, I'll show you how to integrate cross-cutting concerns in Clean Architecture.
Cross-Cutting Concerns in Clean Architecture
In Clean Architecture, cross-cutting concerns play an essential role in ensuring the maintainability and scalability of your system. Ideally, these concerns should be handled separately from the core business logic. This aligns with Clean Architecture's principles, emphasizing the decoupling of concerns and modularity. Your core business rules remain uncluttered, and the architecture stays clean and adaptable.
Ideally, you want to implement cross-cutting concerns in the Infrastructure layer. You can use ASP.NET Core middleware, decorators, or MediatR pipeline behaviors. Whichever approach you decide to use, the guiding idea remains the same.
Let's see how to implement logging, validation, and caching as cross-cutting concerns.
Cross-Cutting Concern #1 - Logging
Logging is a fundamental aspect of software development, allowing you to look into an application's behavior. It's vital for debugging, monitoring application health, and tracking user activities and system anomalies. In the context of Clean Architecture, logging must be implemented in a way that maintains the separation of concerns.
An elegant way to achieve this is with MediatR's IPipelineBehavior
. By encapsulating the logging logic inside a pipeline behavior, we ensure that logging is treated as a distinct concern, separate from business logic. This approach enables us to capture detailed information about requests flowing through the application.
Effective logging should be consistent, context-rich, and non-intrusive. Using Serilog's structured logging capabilities, we can create logs that are not only informative but also easily queryable. This is essential for understanding the state of the application at any given moment.
When done correctly, structured logging provides invaluable insights into your application without cluttering the core logic. It's a balance of granularity and clarity, ensuring that your logs are a helpful tool rather than a source of noise.
using Serilog.Context;
internal sealed class RequestLoggingPipelineBehavior<TRequest, TResponse>(
ILogger<RequestLoggingPipelineBehavior<TRequest, TResponse>> logger)
: IPipelineBehavior<TRequest, TResponse>
where TRequest : class
where TResponse : Result
{
public async Task<TResponse> Handle(
TRequest request,
RequestHandlerDelegate<TResponse> next,
CancellationToken cancellationToken)
{
string requestName = typeof(TRequest).Name;
logger.LogInformation(
"Processing request {RequestName}",
requestName);
TResponse result = await next();
if (result.IsSuccess)
{
logger.LogInformation(
"Completed request {RequestName}",
requestName);
}
else
{
using (LogContext.PushProperty("Error", result.Error, true))
{
logger.LogError(
"Completed request {RequestName} with error",
requestName);
}
}
return result;
}
}
Cross-Cutting Concern #2 - Validation
Validation is a critical cross-cutting concern in software engineering. It serves as the first line of defense against incorrect data entering your system. Validation guards the application against inconsistent data states and potential security vulnerabilities.
In the example below, I'm creating a validation pipeline behavior. This setup allows for a clean separation of validation logic from business logic. The pipeline behavior ensures that each request is validated before it reaches the core processing logic.
In approaching validation, it's crucial to distinguish between two types:
- Input validation
- Business rule validation
Input validation checks for the correctness and format of the data (like string length, number ranges, and date formats), ensuring it meets the basic criteria before processing.
On the other hand, business rule validation is more about ensuring that the data adheres to your domain's specific rules and logic.
Effective validation practices significantly contribute to the resilience and reliability of an application. By enforcing validation rules, you can maintain a high data quality standard and ensure a better user experience.
internal sealed class ValidationPipelineBehavior<TRequest, TResponse>(
IEnumerable<IValidator<TRequest>> validators)
: IPipelineBehavior<TRequest, TResponse>
where TRequest : class
{
public async Task<TResponse> Handle(
TRequest request,
RequestHandlerDelegate<TResponse> next,
CancellationToken cancellationToken)
{
ValidationFailure[] validationFailures = await ValidateAsync(request);
if (validationFailures.Length != 0)
{
throw new ValidationException(validationFailures);
}
return await next();
}
private async Task<ValidationFailure[]> ValidateAsync(TRequest request)
{
if (!validators.Any())
{
return [];
}
var context = new ValidationContext<TRequest>(request);
ValidationResult[] validationResults = await Task.WhenAll(
validators.Select(validator => validator.ValidateAsync(context)));
ValidationFailure[] validationFailures = validationResults
.Where(validationResult => !validationResult.IsValid)
.SelectMany(validationResult => validationResult.Errors)
.ToArray();
return validationFailures;
}
}
Cross-Cutting Concern #3: Caching
Caching is an essential cross-cutting concern in software development. It's primarily aimed at enhancing performance and scalability. Caching involves temporarily storing data in a fast-access layer. This reduces the need to fetch or calculate the same information repeatedly.
The caching pipeline behavior, which you see below, implements the Cache Aside pattern. This pattern involves checking the cache before processing the request and updating the cache with new data as needed. It's a popular caching strategy due to its simplicity and effectiveness. Here's a video tutorial if you want to see how I implemented this.
When implementing caching, it's crucial to consider:
- What to Cache: Identify data that is expensive to compute or retrieve and stable enough to be cached.
- Cache Invalidations: Determine when and how cached data should be invalidated.
- Cache Configuration: Configure cache settings like expiration and size appropriately.
Effective caching improves response times and reduces the load on your system, making it a critical strategy for building scalable .NET applications.
internal sealed class QueryCachingPipelineBehavior<TRequest, TResponse>(
ICacheService cacheService,
ILogger<QueryCachingPipelineBehavior<TRequest, TResponse>> logger)
: IPipelineBehavior<TRequest, TResponse>
where TRequest : ICachedQuery
where TResponse : Result
{
public async Task<TResponse> Handle(
TRequest request,
RequestHandlerDelegate<TResponse> next,
CancellationToken cancellationToken)
{
TResponse? cachedResult = await cacheService.GetAsync<TResponse>(
request.CacheKey,
cancellationToken);
string requestName = typeof(TRequest).Name;
if (cachedResult is not null)
{
logger.LogInformation("Cache hit for {RequestName}", requestName);
return cachedResult;
}
logger.LogInformation("Cache miss for {RequestName}", requestName);
TResponse result = await next();
if (result.IsSuccess)
{
await cacheService.SetAsync(
request.CacheKey,
result,
request.Expiration,
cancellationToken);
}
return result;
}
}
What To Do Next
Managing cross-cutting concerns such as logging, caching, validation, and exception handling is not just about technical implementation. It's about aligning these aspects with the core principles of Clean Architecture. By adopting the decoupling techniques we discussed, you can ensure that your .NET projects are robust and maintainable.
Each step you take towards refining your handling of cross-cutting concerns is a step towards a better software architecture. I encourage you to experiment with these strategies in your own .NET projects. If you want a structured guide covering these aspects in-depth, take a look at Pragmatic Clean Architecture.
Remember, the beauty of software development lies in the continuous evolution and relentless pursuit of improvement.
Hope this was helpful.
See you next week.
P.S. Important announcement about my PCA course
I'll be releasing Pragmatic Clean Architecture V2.0 next week.
I listened to your feedback, and I added many improvements.
There will be more than 3 hours of new content, with updates for 2024. All projects are upgraded to use .NET 8 and the latest language features.
If you want to become a software architect or learn how to take an application to production from scratch, this course will be your trusted guide.
If you have already bought the course before, you'll have access to the new content at no additional cost.
The price will increase to $247 (from $150 today) a week after the launch.
Oh, and did I mention I prepared a bunch of bonus content?
Let's leave that for the launch.
Stay awesome!
Interesting Articles 👀
P.S. 2 ways I can help you accelerate your software architecture skills:
|
|
Pragmatic Clean Architecture
This comprehensive course will teach you the system I use to ship production-ready applications using Clean Architecture. Learn how to apply the best practices of modern software architecture.
|
|
|
Patreon Community
Think like a senior software engineer with access to the source code I use in my YouTube videos and exclusive discounts for my courses.
|