Skip to content

Latest commit

 

History

History
423 lines (316 loc) · 17.3 KB

README.md

File metadata and controls

423 lines (316 loc) · 17.3 KB

Castle.Core.AsyncInterceptor

NuGet Version NuGet Downloads Build status CircleCI build Status codecov Coverage Status Coveralls Coverage Status Latest release

What is AsyncInterceptor?

AsyncInterceptor is an extension to Castle DynamicProxy to simplify the development of interceptors for asynchronous methods.

Why do I want intercept methods?

The whys and wherefores of implementing interceptors is lengthy discussion, and beyond the scope of this introduction. A very common scenario is in the implementation of Aspect-oriented patterns, for which exception handling is useful use case.

An interceptor that catches exceptions and logs them could be implemented quite simply as:

// Intercept() is the single method of IInterceptor.
public void Intercept(IInvocation invocation)
{
    try
    {
        invocation.Proceed();
    }
    catch (Exception ex)
    {
        Log.Error($"Error calling {invocation.Method.Name}.", ex);
        throw;
    }
}

What's not simple about asynchronous method interception?

When implementing IInterceptor the underlying method is invoked like this:

public void Intercept(IInvocation invocation)
{
    // Step 1. Do something prior to invocation.

    invocation.Proceed();

    // Step 2. Do something after invocation.
}

For synchronous methods Proceed() returns only when the underlying method completes, but for asynchronous methods, (those that return Task or Task<TResult>) the Proceed() returns as soon as the underlying method hits an await (or ContinueWith).

Therefore with asynchronous methods Step 2 is executed before the underlying methods logically completes.

How to intercept asynchronous methods without AsyncInterceptor?

To demonstrate how AsyncInterceptor simplifies the interception of asynchronous methods, let's show how to do it without AsyncInterceptor.

Methods that return Task

To intercept methods that return a Task (Note: it must be a Task not Task<TResult>) is not overly complicated.

The invocation provides access to the return value. By checking the type of the return value it is possible to await the completion of the Task.

public void Intercept(IInvocation invocation)
{
    // Step 1. Do something prior to invocation.

    invocation.Proceed();
    Type type = invocation.ReturnValue?.GetType();
    if (type != null && type == typeof(Task))
    {
        // Given the method returns a Task, wait for it to complete before performing Step 2
        Func<Task> continuation = async () =>
        {
            await (Task) invocation.ReturnValue;

            // Step 2. Do something after invocation.
        };

        invocation.ReturnValue = continuation();
        return;
    }

    // Assume the method is synchronous.

    // Step 2. Do something after invocation.
}

Methods that return Task<TResult>

To intercept methods that return a Task<TResult> is far from simple. It's the reason why I created this library. Rather than go into the detail (the solution requires the use of reflection) the stack overflow question Intercept async method that returns generic Task<> via DynamicProxy provides great overview.

How to intercept asynchronous methods with AsyncInterceptor?

If you've got this far, then it's probably safe to assume you want to intercept asynchronous methods, and the options for doing it manually look like a lot of work.

Option 1: Implement IAsyncInterceptor interface to intercept invocations

Create a class that implements IAsyncInterceptor, then register it for interception in the same way as IInterceptor using the ProxyGenerator extension methods, e.g.

var myClass = new ClasThatImplementsIMyInterface();
var generator = new ProxyGenerator();
var interceptor = new ClasThatImplementsIAsyncInterceptor();
IMyInterface proxy = generator.CreateInterfaceProxyWithTargetInterface<IMyInterface>(myClass, interceptor)

Implementing IAsyncInterceptor is the closest to traditional interception when implementing IInterceptor

Instead of a single void Intercept(IInvocation invocation) method to implement, there are three:

void InterceptSynchronous(IInvocation invocation);
void InterceptAsynchronous(IInvocation invocation);
void InterceptAsynchronous<TResult>(IInvocation invocation);

InterceptSynchronous(IInvocation invocation)

InterceptSynchronous is effectively the same as IInterceptor.Intercept, though it is only called for synchronous methods, e.g. methods that do not return Task or Task<TResult>.

Implementing InterceptSynchronous could look something like this:

public void InterceptSynchronous(IInvocation invocation)
{
    // Step 1. Do something prior to invocation.

    invocation.Proceed();

    // Step 2. Do something after invocation.
}

InterceptAsynchronous(IInvocation invocation)

InterceptAsynchronous(IInvocation invocation) is called for methods that return Task but not the generic Task<TResult>.

Implementing InterceptAsynchronous(IInvocation invocation) could look something like this:

public void InterceptAsynchronous(IInvocation invocation)
{
    invocation.ReturnValue = InternalInterceptAsynchronous(invocation);
}

private async Task InternalInterceptAsynchronous(IInvocation invocation)
{
    // Step 1. Do something prior to invocation.

    invocation.Proceed();
    var task = (Task)invocation.ReturnValue;
    await task;

    // Step 2. Do something after invocation.
}

InterceptAsynchronous<TResult>(IInvocation invocation)

InterceptAsynchronous<TResult>(IInvocation invocation) is called for methods that return the generic Task<TResult>.

Implementing InterceptAsynchronous<TResult>(IInvocation invocation) could look something like this:

public void InterceptAsynchronous<TResult>(IInvocation invocation)
{
    invocation.ReturnValue = InternalInterceptAsynchronous<TResult>(invocation);
}

private async Task<TResult> InternalInterceptAsynchronous<TResult>(IInvocation invocation)
{
    // Step 1. Do something prior to invocation.

    invocation.Proceed();
    var task = (Task<TResult>)invocation.ReturnValue;
    TResult result = await task;

    // Step 2. Do something after invocation.

    return result;
}

Option 2: Extend AsyncInterceptorBase class to intercept invocations

⚠️ PROCEED WITH CAUTION ⚠️ This option come with a major caveat.

If you need to perform asynchronous operations before calling proceed, then you should implement option 1, otherwise you risk thread starvation and deadlocking.

Create a class that extends the abstract base class AsyncInterceptorBase, then register it for interception in the same way as IInterceptor using the ProxyGenerator extension methods, e.g.

var myClass = new ClasThatImplementsIMyInterface();
var generator = new ProxyGenerator();
var interceptor = new ClasThatExtendsAsyncInterceptorBase();
IMyInterface proxy = generator.CreateInterfaceProxyWithTargetInterface<IMyInterface>(myClass, interceptor)

Extending AsyncInterceptorBase provides a simple mechanism to intercept methods using the async/await pattern. There are two abstract methods that must be implemented.

Task InterceptAsync(IInvocation invocation, Func<IInvocation, Task> proceed);
Task<T> InterceptAsync<T>(IInvocation invocation, Func<IInvocation, Task<T>> proceed);

Each method takes two parameters. The IInvocation provided by DaynamicProxy and a proceed function to execute the invocation returning an awaitable task.

The first method in called when intercepting void methods or methods that return Task. The second method is called when intercepting any method that returns a value, including Task<TResult>.

A possible extension of AsyncInterceptorBase for exception handling could be implemented as follows:

public class ExceptionHandlingInterceptor : AsyncInterceptorBase
{
    protected override async Task InterceptAsync(IInvocation invocation, Func<IInvocation, Task> proceed)
    {
        try
        {
            // Cannot simply return the the task, as any exceptions would not be caught below.
            await proceed(invocation).ConfigureAwait(false);
        }
        catch (Exception ex)
        {
            Log.Error($"Error calling {invocation.Method.Name}.", ex);
            throw;
        }
    }

    protected override async Task<T> InterceptAsync<T>(IInvocation invocation, Func<IInvocation, Task<T>> proceed)
    {
        try
        {
            // Cannot simply return the the task, as any exceptions would not be caught below.
            return await proceed(invocation).ConfigureAwait(false);
        }
        catch (Exception ex)
        {
            Log.Error($"Error calling {invocation.Method.Name}.", ex);
            throw;
        }
    }
}

Option 3: Extend ProcessingAsyncInterceptor<TState> class to intercept invocations

Create a class that extends the abstract base class ProcessingAsyncInterceptor<TState>, then register it for interception in the same was as IInterceptor using the ProxyGenerator extension methods, e.g.

var myClass = new ClasThatImplementsIMyInterface();
var generator = new ProxyGenerator();
var interceptor = new ClasThatExtendsProcessingAsyncInterceptor();
IMyInterface proxy = generator.CreateInterfaceProxyWithTargetInterface<IMyInterface>(myClass, interceptor)

Extending ProcessingAsyncInterceptor<TState>, provides a simplified mechanism of intercepting method invocations without having to implement the three methods of IAsyncInterceptor.

ProcessingAsyncInterceptor<TState> defines two virtual methods, one that is invoked before to the method invocation, the second after.

protected virtual TState StartingInvocation(IInvocation invocation);
protected virtual void CompletedInvocation(IInvocation invocation, TState state);

State can be maintained between the two method through the generic class parameter TState. StartingInvocation is called before method invocation. The return value of type TState is then passed to the CompletedInvocation which is called after method invocation.

A possible extension of ProcessingAsyncInterceptor<TState> could be as follows:

public class MyProcessingAsyncInterceptor : ProcessingAsyncInterceptor<string>
{
    protected override string StartingInvocation(IInvocation invocation)
    {
        return $"{invocation.Method.Name}:StartingInvocation:{DateTime.UtcNow:O}";
    }

    protected override void CompletedInvocation(IInvocation invocation, string state)
    {
        Trace.WriteLine(state);
        Trace.WriteLine($"{invocation.Method.Name}:CompletedInvocation:{DateTime.UtcNow:O}");
    }
}

The state of type TState returned from StartingInvocation can be null. Neither StartingInvocation nor CompletedInvocation are require to be overridden in the class that derives from ProcessingAsyncInterceptor<TState>. The default implementation of StartingInvocation simply returns null. If all you require is to intercept methods after they are invoked, then just implement CompletedInvocation and ignore the state parameter which will be null. In that situation your class can be defined as:

public class MyProcessingAsyncInterceptor : ProcessingAsyncInterceptor<object>
{
    protected override void CompletedInvocation(IInvocation invocation, object state)
    {
        Trace.WriteLine($"{invocation.Method.Name}:CompletedInvocation:{DateTime.UtcNow:O}");
    }
}

Using AsyncInterceptor with non-overloaded ProxyGenerator methods

While AsyncInterceptor offers convenient overloads for the most common ProxyGenerator methods, some methods do not (yet) have such overloads. In this case, the extension method IAsyncInterceptor.ToInterceptor() can be used to obtain a regular IInterceptor implementation.

var generator = new ProxyGenerator();
var interceptor = new MyInterceptorWithoutTarget<T>();
generator.CreateInterfaceProxyWithoutTarget(typeof(T), interceptor.ToInterceptor());

Method invocation timing using AsyncTimingInterceptor

A common use-case for method invocation interception is to time how long a method takes to execute. For this reason AsyncTimingInterceptor is provided.

AsyncTimingInterceptor is a specialised implementation of ProcessingAsyncInterceptor<TState> that uses a Stopwatch as the TState.

AsyncTimingInterceptor defines two abstract methods, one that is invoked before method invocation and before the Stopwatch has started. The second after method invocation and the Stopwatch has stopped

protected abstract void StartingTiming(IInvocation invocation);
protected abstract void CompletedTiming(IInvocation invocation, Stopwatch stopwatch);

A possible extension of AsyncTimingInterceptor could be as follows:

public class TestAsyncTimingInterceptor : AsyncTimingInterceptor
{
    protected override void StartingTiming(IInvocation invocation)
    {
        Trace.WriteLine($"{invocation.Method.Name}:StartingTiming");
    }

    protected override void CompletedTiming(IInvocation invocation, Stopwatch stopwatch)
    {
        Trace.WriteLine($"{invocation.Method.Name}:CompletedTiming:{stopwatch.Elapsed:g}");
    }
}

Testing

This library maintains a high level of code coverage with extensive unit tests.

Running the tests

There are several ways to run the tests, the most convenient is through Visual Studio, via Test Explorer or ReSharper.

The tests can also be executed from the command line like this:

dotnet test test/Castle.Core.AsyncInterceptor.Tests/Castle.Core.AsyncInterceptor.Tests.csproj

On Windows the above command will execute the tests on all 3 runtimes, .NETFramework,Version=v4.7, .NETCoreApp,Version=v1.1, and .NETCoreApp,Version=v2.1.

To run the tests targeting a specific runtime (which may be necessary if you don't have all them all installed) run the following command:

dotnet test -f netcoreapp3.1 test/Castle.Core.AsyncInterceptor.Tests/Castle.Core.AsyncInterceptor.Tests.csproj

A docker compose file is provided to run the tests on a linux container. To execute the tests in a container run the following command:

docker build --target test --progress=plain .

Executing the code coverage tests

Code coverage uses the excellent and free OpenCover.

To execute the tests with code coverage (Windows Only) run the following command:

coverage.cmd

Code coverage reports are produced using ReportGenerator and can be viewed in the test/TestResults/Report folder.