Aspect-Oriented Programming
Aspect who? 🤨
Yeah, yeah, I know. It is such a weird paradigm, people are barely talking about it 😒. However, it is quite powerful. It allows you to structure utility logic separately from your business logic 🧩.
In this article, we will discuss what Aspect-Oriented Programming is. Its terminology. How it can be implemented. What libraries already exist. But most importantly, you will be able to apply it in practice.
In case you are interested in other programming paradigms look here:
- Structured Programming
- Object-Oriented Programming
- Aspect-Oriented Programming
- Functional Programming
- Event-Driven Programming
- Reactive Programming
Are you ready?😃 Let us begin.
Aspect-Oriented Programming
Before diving into Aspect-oriented programming, let’s understand why we even need it.
Object-oriented programming allows us to encapsulate data and behaviors into different classes. Which is great 😃! However, it is not always possible to encapsulate. Which is lame 😒. Sometimes you have a behavior that affects multiple classes and you cannot figure out where to place it. This behavior is called cross-cutting concerns.
Cross-cutting concerns are behaviors of a program that affect several classes (modules) without the possibility of being encapsulated in any of them.
There are multiple examples of those behaviors, like validation, logging, exception handling, authentication, cache, transaction, feature flags, etc.
Let’s think of it. When you write logs, you probably write them in multiple classes, different modules, and so on. Aspect-oriented programming allows you to encapsulate and reuse that logic.
Aspect-oriented programming (AOP) is a programming paradigm that aims to encapsulate and reuse cross-cutting concerns.
There is also another hidden benefit of AOP. It allows extending the behavior of the class without modifying it or breaking the open/closed principle😱.
Sounds fantastic, doesn’t it? Let’s see it in practice 😉.
Terminology
Imagine you are working on an e-shop project. Its main functionality (core concern/business logic) is about managing orders.
However, if you look at the methods we have, you can notice that the main logic is hidden by tons of additional behaviors.
class OrderService
{
public void DeleteOrder(DeleteOrderRequest request)
{
if (!FeatureManager.IsFeatureEnabled("Order.Delete"))
{
return;
}
PermissionService.Authorize(request);
Logger.LogInfo($"Start deleting order at {DateTime.UtcNow}");
// actual logic
. . .
Logger.LogInfo($"Finish deleting order at {DateTime.UtcNow}");
}
public void UpdateOrder(UpdateOrderRequest request)
{
if (!FeatureManager.IsFeatureEnabled("Order.Update"))
{
return;
}
PermissionService.Authorize(request);
Logger.LogInfo($"Start updating order at {DateTime.UtcNow}");
// actual logic
. . .
Logger.LogInfo($"Finish updating order at {DateTime.UtcNow}");
}
}Ideally, you want to avoid code duplication and achieve a clear separation of concerns.
class OrderService
{
[Feature("Order.Update")] // cross-cutting concern
[Authorize("CanModifyOrderPolicy")]
[AuditPerformance]
public void DeleteOrder(DeleteOrderRequest request)
{
// actual logic
. . .
}
[Feature("Order.Update")]
[Authorize("CanModifyOrderPolicy")]
[AuditPerformance]
public void UpdateOrder(UpdateOrderRequest request)
{
// actual logic
. . .
}
}
class PerformanceAuditor
{
public void LogPerformance(Action action)
{
Logger.LogInfo($"Start {action.Name} at {DateTime.UtcNow}");
action();
Logger.LogInfo($"Finish {action.Name} at {DateTime.UtcNow}");
}
}
. . .In the code sample from above you can see:
- aspect: a class (module) that encapsulates a cross-cutting concern:
class PerformanceAuditor // aspect
{
public void LogPerformance(Action action)
{
. . .
}
}- advice: code that executes a cross-cutting concern:
class PerformanceAuditor
{
public void LogPerformance(Action action) // advice
{
Logger.LogInfo($"Start {action.Name} at {DateTime.UtcNow}");
. . .
Logger.LogInfo($"Finish {action.Name} at {DateTime.UtcNow}");
}
public void LogMemoryUsage(Action action) // advice
{
. . .
}
}- target: a class to which aspect will be applied
class OrderService // target
{
. . .
}- join point: a place where advice can be applied, such as a method call or exception thrown
class OrderService
{
public void DeleteOrder(DeleteOrderRequest request) // join point
{
. . .
}
public void UpdateOrder(UpdateOrderRequest request) // join point
{
. . .
}
}- pointcut: a rule that determines where advice should be applied
In this example, a pointcut is just a convention developers agreed on the project:
class OrderService
{
[Feature("Order.Update")] // we agreed to add to all methods in OrderService (~˘▾˘)~
[AuditPerformance] // we agreed to add to all methods
public void DeleteOrder(DeleteOrderRequest request)
{
. . .
}
[Feature("Order.Update")]
[AuditPerformance]
public void UpdateOrder(UpdateOrderRequest request)
{
. . .
}
}However, a pointcut can also be defined in the code:
class PerformanceAuditor
{
// pointcut
private string PointCut => "OrderService.*[Method]"; // apply to all methods in the OrderService
public void LogPerformance(Action action)
{
. . .
}
}❗️ Important ❗️
In Aspect-Oriented Programming, it is important to have an easy way of defining a pointcut
- weaving: the process of linking aspects with targets
I have not shown it in my pseudo-code, but you can imagine it as some kind of reflection, where we first find all methods marked with [AuditPerformance] and then apply the aspect to each of them:
var methodsToAudit = Assembly
.GetExecutingAssembly()
.GetTypes()
.SelectMany(t => t.GetMethods())
.Where(m => m.HasCustomAttributes(typeof(AuditPerformanceAttribute))
.ToList();
foreach (var methodToAudit in methodsToAudit)
{
methodToAudit.WrapWith(_auditPerformance.LogPerformance);
}Do not try to remember this terminology. It sounds cool, but also completely useless😅.
Type of Weaving
Weaving can occur at different times:
- Runtime Weaving: aspects are woven during the execution of the program.
- Compile-Time Weaving: aspects are woven into the code during compilation.
Runtime Weaving
You can achieve AOP principles with design patterns like template method, proxy, chain of responsibility, etc. But the most honored one is the decorator. Its main goal is to extend the behavior of objects without altering their core structure. Which is exactly what we want 😃.
Back to our service:
class OrderService : IOrderService
{
public void DeleteOrder(DeleteOrderRequest request)
{
. . .
}
public void UpdateOrder(UpdateOrderRequest request)
{
. . .
}
}Implementing cross-cutting concerns in the decorator will have the next look:
class PerformanceAuditorDecorator : IOrderService, /*, IUserService, ...*/
{
public PerformanceAuditorDecorator(IOrderService orderService)
{
_orderService = orderService;
}
public void DeleteOrder(DeleteOrderRequest request)
{
Logger.LogInfo($"Start {nameof(DeleteOrder)} at {DateTime.UtcNow}");
_orderService.DeleteOrder(request);
Logger.LogInfo($"Finish {nameof(DeleteOrder)} at {DateTime.UtcNow}");
}
. . .
}You can have a dedicated decorator for each service or implement all interfaces in a single decorator.
Regardless of the implementation, there are multiple problems here:
- you need to declare a decorator for each service, which may be a struggle when you have many of them
- every time you change your original service (add a method, change method signature) it will affect all decorators
- hard to define a pointcut
This is why I prefer vertical architecture with handlers over services. It is where the pattern shies the best.
I hope you are familiar with MediatR. If not, long story short. Instead of writing a service with multiple methods, you write a separate handler class for each method:
class DeleteOrderRequest : IRequest
{
public int OrderId { get; set; }
}
class DeleteOrderHandler : IRequestHandler<DeleteOrderRequest>
{
public void Handle(DeleteOrderRequest request)
{
. . .
}
}Since all handlers implement a unified interface (IRequestHandler) you just need to define one decorator for all handlers!:
class PerformanceAuditorBehaviour<TRequest, TResponse> : IPipelineBehaviour<TRequest, TResponse>
where TRequest : IPerformanceAudited // pointcut
{
public async Task<TResponse> Handle(TRequest request, RequestHandlerDelegate<TResponse> next)
{
_logger.LogInfo($"Start {typeof(TRequest).Name} at {DateTime.UtcNow}");
var response = await next();
_logger.LogInfo($"Finish {typeof(TRequest).Name} at {DateTime.UtcNow}");
return response();
}
}Notice, that pointcut is defined as a generic constraint, where IPerformanceAudited is just a marker interface:
interface IPerformanceAudited { }So, if you want your cross-cutting concern to be applied to a request, you just mark the command with this interface:
class DeleteOrderRequest : IRequest,
IPerformanceAudited, /* ICached, IIdempotent, IWithRetry , . . . */
{
public int OrderId { get; set; }
}See how much easier it is compared to services:
- you declare just one decorator
- decorator remains the same since handlers inherit the same unified interface
- a pointcut is defined as a generic constraint
- aspects are added (removed) by simply inheriting the interface
Does it look familiar to you?
Usually, different frameworks have support for this processing chain.
Get iamprovidence’s stories in your inbox
Join Medium for free to get updates from this writer.
For example, in ASP you can find a middleware pipeline and integrate custom behavior to each HTTP request:
class PerformanceAuditorMiddleware : IMiddleware
{
public async Task InvokeAsync(HttpContext context, RequestDelegate next)
{
_logger.LogInfo($"Start {HttpContext.Request.Path} at {DateTime.UtcNow}");
await next(context);
_logger.LogInfo($"Finish {HttpContext.Request.Path} at {DateTime.UtcNow}");
}
}You can tune aspects more granularly with filters that can be enabled for a specific action, a whole controller, or all methods:
class PerformanceAuditorFilter : IActionFilter
{
public void OnActionExecuting(ActionExecutingContext context)
{
_logger.LogInfo($"Start {context.ActionDescriptor.ActionName} at {DateTime.UtcNow}");
}
public void OnActionExecuted(ActionExecutedContext context)
{
_logger.LogInfo($"Finish {context.ActionDescriptor.ActionName} at {DateTime.UtcNow}");
}
}Interceptors are present in EntityFramework too:
public class PerformanceAuditorSaveChangesInterceptor : SaveChangesInterceptor
{
public override int SavingChanges(DbContextEventData eventData, InterceptionResult<int> result)
{
_logger.LogInformation($"SaveChanges started at {DateTime.UtcNow}");
return base.SavingChanges(eventData, result);
}
public override int SavedChanges(SaveChangesCompletedEventData eventData, int result)
{
_logger.LogInformation($"SaveChanges finished at {DateTime.UtcNow}");
return base.SavedChanges(eventData, result);
}
}You can define a single DelegatingHandler and enhance the behavior of any HttpClient:
public class PerformanceAuditorDelegatingHandler : DelegatingHandler
{
protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
_logger.LogInfo($"Start {request.RequestUri} at {DateTime.UtcNow}");
var response = await base.SendAsync(request, cancellationToken);
_logger.LogInfo($"Finish {request.RequestUri} at {DateTime.UtcNow}");
return response;
}
}Castle.DynamicProxy allows you to create a proxy and build an interceptor pipeline over any method:
public class PerformanceAuditorInterceptor : IInterceptor
{
public void Intercept(IInvocation invocation)
{
_logger.LogInfo($"Start {invocation.Method.Name} at {DateTime.UtcNow}");
invocation.Proceed();
_logger.LogInfo($"Finish {invocation.Method.Name} at {DateTime.UtcNow}");
}
}Do not think this is something specific to C#. You can find it in Angular and other frameworks too:
@Injectable()
export class LoggingInterceptor implements HttpInterceptor {
public intercept(request: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
console.log(`Start ${request.url} at ${new Date(Date.now()).toLocaleString()}.`);
return next
.handle(request)
.pipe(
tap(() => console.log(`Finish ${request.url} at ${new Date(Date.now()).toLocaleString()}.`),
);
}
}As you can see, there is almost always support for the middleware pattern in one way or another.
For me, as a web developer, it used to be a struggle to choose between middleware and filter, but since I tried MediatR it has been a no-brainer:
- MediatR allows to place cross-cutting concerns in the Application layer
- It can run in the console, mobile, desktop, etc, meaning no dependency on the framework
- Its declarative approach makes sure you won’t forget to enable the aspect
- Additionally, it provides a nice way to configure pointcut
On the other hand, people hate MediatR 😅. I know you won’t buy it, so I will just place some examples of experts using it for AOP:
- eShopOnContainers — sample application developed by Microsoft
- Clean Architecture — solution template by Jason Taylor
Compile-Time Weaving
All the approaches above inject aspects during execution. However, there are also libraries that will help you to enhance your code during compilation.
I have not tried them, but it is still worth to mention 🙃:
Fody
Fody defines Aspects as attributes:
[AttributeUsage(AttributeTargets.Method)]
class PerformanceAuditorAttribute : Attribute, IMethodDecorator
{
// invoked once, when the method is first called
public void Init(object instance, MethodBase method, object[] args)
{
. . .
}
public void OnEntry()
{
Logger.LogInfo($"Start {_method.Name} at {DateTime.UtcNow}");
}
public void OnExit()
{
Logger.LogInfo($"Finish {_method.Name)} at {DateTime.UtcNow}");
}
} Which are later just declared on top of the method:
class OrderService : IOrderService
{
[PerformanceAuditor]
public void DeleteOrder(DeleteOrderRequest request)
{
. . .
}
. . .
}The problems with Fody are as follows:
- it’s paid 🙃
- no DI support
- no
asyncsupport - cannot define pointcut in the code
PostSharp
Similarly to Fody, you can use PostSharp:
[PSerializable]
public class PerformanceAuditorAspect : OnMethodBoundaryAspect
{
public override void OnEntry(MethodExecutionArgs args)
{
Logger.LogInfo($"Start {args.Method.Name} at {DateTime.UtcNow}");
}
public override void OnExit(MethodExecutionArgs args)
{
Logger.LogInfo($"Finish {args.Method.Name} at {DateTime.UtcNow}");
}
. . .
}Which can be used as an attribute:
class OrderService : IOrderService
{
[PerformanceAuditorAspect]
public void DeleteOrder(DeleteOrderRequest request)
{
. . .
}
. . .
}Or applied as pointcut to all classes in the assembly:
[assembly: PerformanceAuditorAspect(AttributeTargetTypes = "regex:.*Service")]The issues are similar too:
- it’s paid 🙃
- no DI support
- no
asyncsupport
MetaLama
Another popular code transformation tool is MetaLama:
class PerformanceAuditorAttribute : OverrideMethodAspect
{
public override dynamic? OverrideMethod()
{
Logger.LogInfo($"Start {meta.Target.Method.Name} at {DateTime.UtcNow}");
meta.Proceed();
Logger.LogInfo($"Finish {meta.Target.Method.Name} at {DateTime.UtcNow}");
}
}Later aspect can be used as an attribute or you can define a pointcut to apply it to multiple classes:
class Fabric : ProjectFabric
{
public override void AmendProject(IProjectAmender amender)
{
amender
.Outbound
.SelectMany(c => c.Types)
.SelectMany(t => t.Methods)
.AddAspectIfEligible(t => new PerformanceAuditorAttribute());
}
}Again:
- it’s paid 🙃
- no DI support
So, yes, injecting aspects during compilation sounds great, but the drawbacks you have and (to be honest) questionable benefits make those libraries unusable 🤷♂️.
Not to be biased, the choice is always yours 🙃. Meanwhile, I am going to finish.
My personal thoughts 🤔
Despite how good AOP sounds, you should understand it is not a silver bullet. It offers benefits but comes with limitations.
For example, if you move logging to an aspect, you can manage common concerns (like method entry/exit logging or exception handling) in one place, reducing code duplication. Great 👍.
However, this doesn’t mean that every type of logging handling should or can be handled through AOP. There still will be scenarios where using a logger directly in your code is more appropriate for more fine-grained control.
To sum up. Use AOP when you can reuse logic, but if you need to write cross-cutting concerns directly in the code, do not worry much about it.
Conclusion
Aspect-oriented programming is a paradigm that helps to encapsulate and reuse cross-cutting concerns.
It aims to increase modularity. Aspect-oriented programming helps to deal with complexity by organizing code into separate modules.
Here are some of the main advantages:
- separation of concerns. it helps to keep the business logic separate from other concerns like logging, transaction management, security, and exception handling
- code reusability. the same aspect can be applied across different parts of your application without duplicating code
- simplified maintenance. any changes to a cross-cutting concern only need to be made in a single place (the aspect)
- enhanced readability. by removing cross-cutting concerns from the main business logic, the readability of the core logic improves significantly
I hope now Aspect-oriented programming is clear to you 🙃.
💬 Let me know in the comments what is your experience with AOP. Have you tried it? Did you like it?
👏 Clap if you learn something new
👇 Check out other articles
☕️ You can also support me with a link below
✅ Follow
😉 And keep learning









