AllOverIt Reference v7 Help

Interceptors

Aspect-Oriented Programming (AOP) addresses the challenge of managing cross-cutting concerns of your application like logging, security, or error handling. AOP introduces the concept of an aspect which is a modular unit specifically designed to handle one of these cross-cutting concerns.

AllOverIt provides these aspects in the form of class-level and method-level interceptors. The implementation provided by this library is limited to intercepting methods on classes that inherit from an interface as it uses DispatchProxy under the hood. Interception is based on reflection, so it's important to consider any performance concerns. This approach can be useful, however, when you need to intercept methods on a class that you don't have access to due to it coming from a third-party library.

Consider the following interface representing a service that returns a 'secret value':

public interface ISecretService { string GetSecret(); Task<string> GetSecretAsync(); }

And here is an example implementation:

internal sealed class SecretService : ISecretService { // Assuming SecurityManager is a static provider defined elsewhere. public string GetSecret() { return SecurityManager.GetSecret(); } public Task<string> GetSecretAsync() { return SecurityManager.GetSecretAsync(); } }

There are two approaches to intercepting method calls:

  • Class level Interceptors

    Intercept all methods by implementing a class-level interceptor.

  • Method level Interceptors

    Implement one or more method-level handlers to intercept only the methods of concern.

Class-Level Interceptors

A class-level interceptor provides the ability to intercept all methods invoked on a class instance. This can be useful for handling common cross-cutting concerns such as logging the performance of all methods.

To create a class-level interceptor, begin by creating a class that inherits from the abstract class, InterceptorBase<TService>. This class contains virtual methods that can be overriden to invoke custom code before, or after, each method on the interface, or if a method faults (throws an Exception). The available virtual methods are shown in the sample implementation below.

internal class TimedInterceptor : InterceptorBase<ISecretService> { protected override InterceptorState BeforeInvoke( MethodInfo targetMethod, ref object[] args) { // Custom code here return new InterceptorState(); } protected override void AfterInvoke(MethodInfo targetMethod, object[] args, InterceptorState state) { // Custom code here } protected override void Faulted(MethodInfo targetMethod, object[] args, InterceptorState state, Exception exception) { // Custom code here } }

Sample Interceptor

As an example, the above TimedInterceptor is updated below to determine how long each method call takes to execute.

internal class TimedInterceptor : InterceptorBase<ISecretService> { private sealed class TimedState : InterceptorState { public Stopwatch Stopwatch { get; } = Stopwatch.StartNew(); } protected override InterceptorState BeforeInvoke( MethodInfo targetMethod, ref object[] args) { return new TimedState(); } protected override void AfterInvoke(MethodInfo targetMethod, object[] args, InterceptorState state) { var timedState = state as TimedState; var elapsed = timedState.Stopwatch.ElapsedMilliseconds; // Do something here... } }

In this example, a new TimedState instance is created for each intercepted method, and it is provided to AfterInvoke() when the decorated instance method returns.

Method-Level Interceptors

Method-level interceptors provide the following benefits over class-level interceptors:

  • Implementing a handler per method to be intercepted applies the "Single Responsibility Principle".

  • Individual handlers are testable.

  • Methods with a non-void return type use a strongly-type InterceptorState<TResult>, in place of the non-typed InterceptorState.

Method intercept handlers are implemented by deriving from a base handler that is suited to the return type of the method signature, as described in the following table.

Method return type

Example

Handler base class

void

void Initialize()

InterceptorMethodHandlerBase

TResult

string GetSecret()

InterceptorMethodHandlerBase<string>

Task

Task InitializeAsync()

InterceptorMethodHandlerAsyncBase

Task<TResult>

Task<string> GetSecretAsync()

InterceptorMethodHandlerAsyncBase<TResult>

Base Handlers

Since method-level interceptors are designed to provide greater-flexibility and type safety than class-level interceptors, the signature of the virtual methods in the above-mentioned base classes is slightly different.

InterceptorMethodHandlerBase

InterceptorState BeforeMethodInvoke(MethodInfo targetMethod, ref object[] args); void AfterMethodInvoke(MethodInfo targetMethod, object[] args, InterceptorState state); void OnMethodFaulted(MethodInfo targetMethod, object[] args, InterceptorState state, Exception exception);

InterceptorState is the base class for all state classes. Since InterceptorMethodHandlerBase is used to intercept methods with a void return type, any result passed to SetResult() will be ignored beyond the state class.

InterceptorMethodHandlerBase<TResult>

InterceptorState<TResult> BeforeMethodInvoke(MethodInfo targetMethod, ref object[] args); void AfterMethodInvoke(MethodInfo targetMethod, object[] args, InterceptorState<TResult> state); void OnMethodFaulted(MethodInfo targetMethod, object[] args, InterceptorState<TResult> state, Exception exception);

InterceptorState<TResult> is typed to the return type of the method being intercepted.

Refer to Interceptor State for more information on how to use it to mutate the final result returned to the caller.

InterceptorMethodHandlerAsyncBase

InterceptorState<Task> BeforeMethodInvoke(MethodInfo targetMethod, ref object[] args); void AfterMethodInvoke(MethodInfo targetMethod, object[] args, InterceptorState<Task> state); void OnMethodFaulted(MethodInfo targetMethod, object[] args, InterceptorState<Task> state, Exception exception);

Since InterceptorMethodHandlerAsyncBase is used to intercept methods with a Task return type, any result passed to SetResult() will be ignored beyond the state class.

InterceptorMethodHandlerAsyncBase<TResult>

InterceptorState<Task<TResult>> BeforeMethodInvoke(MethodInfo targetMethod, ref object[] args); void AfterMethodInvoke(MethodInfo targetMethod, object[] args, InterceptorState<Task<TResult>> state); void OnMethodFaulted(MethodInfo targetMethod, object[] args, InterceptorState<Task<TResult>> state, Exception exception);

InterceptorState<Task<TResult>> is typed to the return type of the method being intercepted.

Refer to Interceptor State for more information on how to use it to mutate the final result returned to the caller.

Sample handlers

Consider the scenario where you want to register a handler for each method on this ISecretService interface:

public interface ISecretService { string GetSecret(string accessKey); Task<string> GetSecretAsync(string accessKey); }

The handler for GetSecret() may look something like this:

internal sealed class GetSecretHandler : InterceptorMethodHandlerBase<string> { // An example cache for demonstration only private readonly ICache _cache; // This handler will be invoked when // ISecretService.GetSecret() is called. public override MethodInfo[] TargetMethods { get; } = [typeof(ISecretService) .GetMethod(nameof(ISecretService.GetSecret))]; public GetSecretHandler(ICache cache) { _cache = cache; } protected override InterceptorState<string> BeforeMethodInvoke( MethodInfo targetMethod, ref object[] args) { var accessKey = (string) args[0]; // Use _cache to see if there is a suitable // value to set as the result. if (_cache.TryGetValue(accessKey, out string value)) { return new InterceptorState<string> { IsHandled = true, Result = value }; } // All handlers must return a state. In this case, // returning a default constructed state indicates // the intercept is not yet handled so the decorated // instance method will be called. return new InterceptorState<string>(); } protected override void AfterMethodInvoke(MethodInfo targetMethod, object[] args, InterceptorState<string> state) { var accessKey = (string) args[0]; var result = state.Result; // Do something with the input // argument(s) or final result. } }

For comparison, the handler for GetSecretAsync() may look something like this:

internal sealed class GetSecretAsyncHandler : InterceptorMethodHandlerAsyncBase<string> { private readonly ICache _cache; // This handler will be invoked when // ISecretService.GetSecretAsync() is called. public override MethodInfo[] TargetMethods { get; } = [typeof(ISecretService).GetMethod( nameof(ISecretService.GetSecretAsync))]; public GetSecretAsyncHandler(ICache cache) { _cache = cache; } protected override InterceptorState<Task<string>> BeforeMethodInvoke(MethodInfo targetMethod, ref object[] args) { var accessKey = (string) args[0]; // Use _cache to see if there is a suitable // value to set as the result. if (_cache.TryGetValue(accessKey, out string value)) { return new InterceptorState<string> { IsHandled = true, Result = Task.FromResult(value) }; } // All handlers must return a state. In this case, // returning a default constructed state indicates // the intercept is not yet handled so the decorated // instance method will be called. return new InterceptorState<Task<string>>(); } protected override void AfterMethodInvoke( MethodInfo targetMethod, object[] args, InterceptorState<Task<TResult>> state) { var accessKey = (string) args[0]; // The state's 'Result' is of type Task<string> var value = state.Result; // Noting that 'value' is of type Task<string>, // then referencing the Task's 'Result' property will // return the underlying 'string' result. Referencing // value's 'Result' in this instance is completely // safe because AfterMethodInvoke() is only called if the // Task has successfully Completed. string finalResult = value.Result; // Do something with the input argument(s) or final result. // ... } }
Last modified: 31 March 2024