5 minute read

Introduction

When building robust software applications, logging becomes an essential part of the process. Logs provide invaluable insights into application behavior, making it easier to debug and monitor. However, manually adding logging code throughout a project is time-consuming and can clutter the codebase. What if you could automatically inject logging, security checks, and caching into your code without explicitly writing it everywhere?

This is where Aspect-Oriented Programming (AOP) shines. In this post, we’ll explore how AOP can streamline the logging, security, and caching processes in C#, making our code more maintainable and less repetitive.

You can access the entire code from my GitHub Repo

What is Aspect-Oriented Programming (AOP)?

Aspect-Oriented Programming (AOP) is a programming paradigm that helps manage cross-cutting concerns—functionality that spans multiple points of an application but is not related to the business logic itself. Examples of cross-cutting concerns include:

  • Logging
  • Security
  • Caching
  • Exception handling

AOP allows you to encapsulate these concerns in separate modules called aspects, so you don’t need to repeat them in every function or class. This separation makes code cleaner, as business logic remains unpolluted by repetitive code for logging, security, etc.

In C#, AOP is commonly implemented through frameworks like PostSharp or Castle DynamicProxy. These frameworks allow you to inject additional behavior (such as logging) before or after method execution, without modifying the method’s core logic.

Logging: Cross-Cutting Concern

General

Logging is a textbook example of a cross-cutting concern. Whether it’s a small application or a large enterprise system, you’ll often find yourself adding logging statements across multiple methods. Manually adding Console.WriteLine() or calling logger objects throughout the codebase increases the risk of duplication and can become quite hard to maintain.

For instance, take the following code snippet, where logging is manually added:

public class PaymentService
{
    public void ProcessPayment()
    {
        Console.WriteLine("Processing payment...");
        // Payment logic
        Console.WriteLine("Payment processed successfully.");
    }
}

This approach works, but it gets cumbersome as the application grows. We need a better way—enter AOP.

Implementing AOP in C# for Logging

Let’s see how to implement AOP to handle logging in C# using PostSharp, a popular AOP library for .NET.

First, you’ll need to install the PostSharp NuGet package (I am using the free version for the demo):

Install-Package PostSharp

Then, define a custom aspect for logging:

using PostSharp.Aspects;
using System;

[PSerializable]
public class LogAspect : OnMethodBoundaryAspect
{
  public override void OnEntry(MethodExecutionArgs args)
  {
    Console.WriteLine($"Starting method {args.Method.Name} with arguments: {string.Join(", ", args.Arguments)}");
  }

  public override void OnExit(MethodExecutionArgs args)
  {
    Console.WriteLine($"Completed method {args.Method.Name}");
  }

  public override void OnException(MethodExecutionArgs args)
  {
    Console.WriteLine($"Exception in method {args.Method.Name}: {args.Exception.Message}");
  }
}

This LogAspect class uses the OnMethodBoundaryAspect from PostSharp, allowing us to inject code before (OnEntry) and after (OnExit) method execution. It also logs any exceptions encountered during execution.

Now that we have our logging aspect, let’s apply it to the OrderService and PaymentService methods:

public class OrderService
{
    [LogAspect]
    public void CreateOrder(int orderId)
    {
        // Business logic for order creation
    }

    [LogAspect]
    public void CancelOrder(int orderId)
    {
        // Business logic for order cancellation
    }
}

public class PaymentService
{
    [LogAspect]
    public void ProcessPayment(int paymentId)
    {
        // Business logic for payment processing
    }
}

When the ProcessPayment method is called, the logging behavior is automatically injected.

Example

PaymentService paymentService = new PaymentService();
paymentService.ProcessPayment();

Output:

Starting method ProcessPayment with arguments: 501
Completed method ProcessPayment

The logging logic is now centralized in the LogAspect, keeping the actual business logic in OrderService and PaymentService clean and uncluttered.

Security: Authentication and Authorization

One of the most common uses for AOP, apart from logging, is security—specifically, adding authentication and authorization checks to methods. Instead of manually verifying whether a user has the right permissions to access each method, AOP can handle this automatically.

Implementing AOP in C# for Security

Let’s define an aspect that checks if the user is authenticated and authorized to execute a method. We’ll simulate this with a User class and a SecurityAspect.

using PostSharp.Aspects;
using System;

public class User
{
    public string Username { get; set; }
    public string Role { get; set; }
}

public static class SecurityContext
{
    public static User CurrentUser { get; set; }
}

[PSerializable]
public class SecurityAspect : OnMethodBoundaryAspect
{
  [PNonSerialized]
  private readonly string _requiredRole;

  public SecurityAspect(string requiredRole)
  {
    _requiredRole = requiredRole;
  }

  public override void OnEntry(MethodExecutionArgs args)
  {
    var user = SecurityContext.CurrentUser;
    
    if (user == null)
    {
      throw new UnauthorizedAccessException("User is not authenticated.");
    }

    if (user.Role != _requiredRole)
    {
      throw new UnauthorizedAccessException($"User {user.Username} does not have permission to access this method");
    }

    Console.WriteLine($"User {user.Username} is authorized to access {args.Method.Name}.");
  }
}

This SecurityAspect will throw an exception if the user is not authenticated or does not have the required role. Now, let’s apply the above aspect to the OrderService and CancelOrder method in the following use case.

public class OrderService
{
    [SecurityAspect("Admin")]
    public void CancelOrder(int orderId)
    {
        // Business logic for order cancellation
        Console.WriteLine($"Order {orderId} cancelled successfully.");
    }
}

Example

SecurityContext.CurrentUser = new User { Username = "JohnDoe", Role = "User" };
OrderService orderService = new OrderService();

try
{
  orderService.CancelOrder(101);
}
catch (UnauthorizedAccessException ex)
{
  Console.WriteLine(ex.Message);
}

Output:

User JohnDoe does not have permission to access this method.

Change the role to Admin and you’ll see the user is authorized:

User JaneAdmin is authorized to access CancelOrder.
Order 101 cancelled successfully.

Caching: Performance Optimization

AOP can also be used for caching, improving performance by storing method results and returning them from a cache for subsequent requests with the same inputs.

Implementing AOP in C# for Caching

Let’s define an aspect that checks if the key is cached then return the valur from the cache else get the same.

using PostSharp.Aspects;
using System;
using System.Collections.Generic;

[PSerializable]
public class CachingAspect : MethodInterceptionAspect
{
  private static readonly Dictionary<string, object> Cache = new Dictionary<string, object>();

  public override void OnInvoke(MethodInterceptionArgs args)
  {
      string key = $"{args.Method.Name}_{string.Join("_", args.Arguments)}";

      if (Cache.ContainsKey(key))
      {
          Console.WriteLine($"Returning cached result for {args.Method.Name}");
          args.ReturnValue = Cache[key];
      }
      else
      {
          base.OnInvoke(args);
          Cache[key] = args.ReturnValue;
          Console.WriteLine($"Caching result for {args.Method.Name}");
      }
  }
}

Further, let’s define a ProductService showing this cache in action:

public class ProductService
{
    [CachingAspect]
    public string GetProductDetails(int productId)
    {
        // Simulate a slow operation
        Console.WriteLine("Fetching product details from database...");
        System.Threading.Thread.Sleep(2000);
        return $"Product {productId} details";
    }
}

Example

ProductService productService = new ProductService();

string result1 = productService.GetProductDetails(101);
Console.WriteLine(result1);

string result2 = productService.GetProductDetails(101);
Console.WriteLine(result2);

Output:

Fetching product details from database...
Caching result for GetProductDetails
Product 101 details
Returning cached result for GetProductDetails
Product 101 details

Conclusion

Aspect-Oriented Programming is a powerful way to manage cross-cutting concerns like logging, security, and caching in C#. Using frameworks like PostSharp, developers can centralize and automate repetitive tasks, keeping business logic clean and easier to maintain. By implementing AOP for logging, security checks, and caching, you can improve your code’s maintainability, scalability, and performance.

Comments