Skip to content

Commit

Permalink
Merge pull request #20 from JSkimming/simplify-exception-handling
Browse files Browse the repository at this point in the history
Make exception handling simpler
  • Loading branch information
JSkimming authored Sep 26, 2017
2 parents ac7996e + b8c6a4b commit 24d79c6
Show file tree
Hide file tree
Showing 10 changed files with 957 additions and 32 deletions.
89 changes: 73 additions & 16 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ useful use case.

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

```c#
```csharp
// Intercept() is the single method of IInterceptor.
public void Intercept(IInvocation invocation)
{
Expand All @@ -41,7 +41,7 @@ public void Intercept(IInvocation invocation)

When implementing `IInterceptor` the underlying the method is invoked like this:

```c#
```csharp
public void Intercept(IInvocation invocation)
{
// Step 1. Do something prior to invocation.
Expand Down Expand Up @@ -73,7 +73,7 @@ To intercept methods that return a [`Task`](https://msdn.microsoft.com/en-us/lib
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`](https://msdn.microsoft.com/en-us/library/system.threading.tasks.task.aspx).

```c#
```csharp
public void Intercept(IInvocation invocation)
{
// Step 1. Do something prior to invocation.
Expand Down Expand Up @@ -112,12 +112,69 @@ DynamicProxy](http://stackoverflow.com/a/28374134) provides great overview.
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
### Option 1: Extend `AsyncInterceptorBase` class to intercept invocations

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

```csharp
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.

```csharp
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:

```csharp
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 2: Implement `IAsyncInterceptor` interface to intercept invocations

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

```c#
```csharp
var myClass = new ClasThatImplementsIMyInterface();
var generator = new ProxyGenerator();
var interceptor = new ClasThatImplementsIAsyncInterceptor();
Expand All @@ -131,7 +188,7 @@ is the closest to traditional interception when implementing
Instead of a single `void Intercept(IInvocation invocation)` method to implement, there are three:

```c#
```csharp
void InterceptSynchronous(IInvocation invocation);
void InterceptAsynchronous(IInvocation invocation);
void InterceptAsynchronous<TResult>(IInvocation invocation);
Expand All @@ -144,7 +201,7 @@ methods, e.g. methods that do not return `Task` or `Task<TResult>`.

Implementing `InterceptSynchronous` could look something like this:

```c#
```csharp
public void InterceptSynchronous(IInvocation invocation)
{
// Step 1. Do something prior to invocation.
Expand All @@ -162,7 +219,7 @@ public void InterceptSynchronous(IInvocation invocation)

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

```c#
```csharp
public void InterceptAsynchronous(IInvocation invocation)
{
invocation.ReturnValue = InternalInterceptAsynchronous(invocation);
Expand All @@ -186,7 +243,7 @@ private async Task InternalInterceptAsynchronous(IInvocation invocation)

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

```c#
```csharp
public void InterceptAsynchronous<TResult>(IInvocation invocation)
{
invocation.ReturnValue = InternalInterceptAsynchronous<TResult>(invocation);
Expand All @@ -206,12 +263,12 @@ private async Task<TResult> InternalInterceptAsynchronous<TResult>(IInvocation i
}
```

### Option 2: Extend `ProcessingAsyncInterceptor<TState>` class to intercept invocations
### 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.

```c#
```csharp
var myClass = new ClasThatImplementsIMyInterface();
var generator = new ProxyGenerator();
var interceptor = new ClasThatExtendsProcessingAsyncInterceptor();
Expand All @@ -226,7 +283,7 @@ provides a simplified mechanism of intercepting method invocations without havin
`ProcessingAsyncInterceptor<TState>` defines two virtual methods, one that is invoked before to the method invocation,
the second after.

```c#
```csharp
protected virtual TState StartingInvocation(IInvocation invocation);
protected virtual void CompletedInvocation(IInvocation invocation, TState state);
```
Expand All @@ -237,7 +294,7 @@ called after method invocation.

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

```c#
```csharp
public class MyProcessingAsyncInterceptor : ProcessingAsyncInterceptor<string>
{
protected override string StartingInvocation(IInvocation invocation)
Expand All @@ -260,7 +317,7 @@ after they are invoked, then just implement `CompletedInvocation` and ignore the
null. In that situation your class can be defined as:


```c#
```csharp
public class MyProcessingAsyncInterceptor : ProcessingAsyncInterceptor<object>
{
protected override void CompletedInvocation(IInvocation invocation, object state)
Expand All @@ -282,14 +339,14 @@ is provided.
`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

```c#
```csharp
protected abstract void StartingTiming(IInvocation invocation);
protected abstract void CompletedTiming(IInvocation invocation, Stopwatch stopwatch);
```

A possible extension of `AsyncTimingInterceptor` could be as follows:

```c#
```csharp
public class TestAsyncTimingInterceptor : AsyncTimingInterceptor
{
protected override void StartingTiming(IInvocation invocation)
Expand Down
186 changes: 186 additions & 0 deletions src/Castle.Core.AsyncInterceptor/AsyncInterceptorBase.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,186 @@
// Copyright (c) 2016 James Skimming. All rights reserved.
// Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information.

namespace Castle.DynamicProxy
{
using System;
using System.Collections.Concurrent;
using System.Reflection;
using System.Threading.Tasks;

/// <summary>
/// A base type for an <see cref="IAsyncInterceptor"/> to provided a simplified solution of method
/// <see cref="IInvocation"/> by enforcing only two types of interception, both asynchronous.
/// </summary>
public abstract class AsyncInterceptorBase : IAsyncInterceptor
{
#if !NETSTANDARD2_0
/// <summary>
/// A completed <see cref="Task"/>.
/// </summary>
private static readonly Task CompletedTask = Task.FromResult(0);
#endif

private static readonly MethodInfo InterceptSynchronousMethodInfo =
typeof(AsyncInterceptorBase)
.GetMethod(nameof(InterceptSynchronousResult), BindingFlags.Static | BindingFlags.NonPublic);

private static readonly ConcurrentDictionary<Type, GenericSynchronousHandler> GenericSynchronousHandlers =
new ConcurrentDictionary<Type, GenericSynchronousHandler>
{
[typeof(void)] = InterceptSynchronousVoid,
};

private delegate void GenericSynchronousHandler(AsyncInterceptorBase me, IInvocation invocation);

/// <summary>
/// Intercepts a synchronous method <paramref name="invocation"/>.
/// </summary>
/// <param name="invocation">The method invocation.</param>
void IAsyncInterceptor.InterceptSynchronous(IInvocation invocation)
{
Type returnType = invocation.Method.ReturnType;
GenericSynchronousHandler handler = GenericSynchronousHandlers.GetOrAdd(returnType, CreateHandler);
handler(this, invocation);
}

/// <summary>
/// Intercepts an asynchronous method <paramref name="invocation"/> with return type of <see cref="Task"/>.
/// </summary>
/// <param name="invocation">The method invocation.</param>
void IAsyncInterceptor.InterceptAsynchronous(IInvocation invocation)
{
invocation.ReturnValue = InterceptAsync(invocation, ProceedAsynchronous);
}

/// <summary>
/// Intercepts an asynchronous method <paramref name="invocation"/> with return type of <see cref="Task{T}"/>.
/// </summary>
/// <typeparam name="TResult">The type of the <see cref="Task{T}"/> <see cref="Task{T}.Result"/>.</typeparam>
/// <param name="invocation">The method invocation.</param>
void IAsyncInterceptor.InterceptAsynchronous<TResult>(IInvocation invocation)
{
invocation.ReturnValue = InterceptAsync(invocation, ProceedAsynchronous<TResult>);
}

/// <summary>
/// Override in derived classes to intercept method invocations.
/// </summary>
/// <param name="invocation">The method invocation.</param>
/// <param name="proceed">The function to proceed the <paramref name="invocation"/>.</param>
/// <returns>A <see cref="Task" /> object that represents the asynchronous operation.</returns>
protected abstract Task InterceptAsync(IInvocation invocation, Func<IInvocation, Task> proceed);

/// <summary>
/// Override in derived classes to intercept method invocations.
/// </summary>
/// <typeparam name="TResult">The type of the <see cref="Task{T}"/> <see cref="Task{T}.Result"/>.</typeparam>
/// <param name="invocation">The method invocation.</param>
/// <param name="proceed">The function to proceed the <paramref name="invocation"/>.</param>
/// <returns>A <see cref="Task" /> object that represents the asynchronous operation.</returns>
protected abstract Task<TResult> InterceptAsync<TResult>(
IInvocation invocation,
Func<IInvocation, Task<TResult>> proceed);

private static GenericSynchronousHandler CreateHandler(Type returnType)
{
MethodInfo method = InterceptSynchronousMethodInfo.MakeGenericMethod(returnType);
return (GenericSynchronousHandler)method.CreateDelegate(typeof(GenericSynchronousHandler));
}

private static void InterceptSynchronousVoid(AsyncInterceptorBase me, IInvocation invocation)
{
Task task = me.InterceptAsync(invocation, ProceedSynchronous);

// If the intercept task has yet to complete, wait for it.
if (!task.IsCompleted)
{
Task.Run(() => task).Wait();
}

if (task.IsFaulted)
{
throw task.Exception.InnerException;
}
}

private static void InterceptSynchronousResult<TResult>(AsyncInterceptorBase me, IInvocation invocation)
{
Task task = me.InterceptAsync(invocation, ProceedSynchronous<TResult>);

// If the intercept task has yet to complete, wait for it.
if (!task.IsCompleted)
{
Task.Run(() => task).Wait();
}

if (task.IsFaulted)
{
throw task.Exception.InnerException;
}
}

private static Task ProceedSynchronous(IInvocation invocation)
{
try
{
invocation.Proceed();
#if NETSTANDARD2_0
return Task.CompletedTask;
#else
return CompletedTask;
#endif
}
catch (Exception e)
{
#if NETSTANDARD2_0
return Task.FromException(e);
#else
var tcs = new TaskCompletionSource<int>();
tcs.SetException(e);
return tcs.Task;
#endif
}
}

private static Task<TResult> ProceedSynchronous<TResult>(IInvocation invocation)
{
try
{
invocation.Proceed();
return Task.FromResult((TResult)invocation.ReturnValue);
}
catch (Exception e)
{
#if NETSTANDARD2_0
return Task.FromException<TResult>(e);
#else
var tcs = new TaskCompletionSource<TResult>();
tcs.SetException(e);
return tcs.Task;
#endif
}
}

private static async Task ProceedAsynchronous(IInvocation invocation)
{
invocation.Proceed();

// Get the task to await.
var originalReturnValue = (Task)invocation.ReturnValue;

await originalReturnValue.ConfigureAwait(false);
}

private static async Task<TResult> ProceedAsynchronous<TResult>(IInvocation invocation)
{
invocation.Proceed();

// Get the task to await.
var originalReturnValue = (Task<TResult>)invocation.ReturnValue;

TResult result = await originalReturnValue.ConfigureAwait(false);
return result;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@ namespace Castle.DynamicProxy
using System.Threading.Tasks;

/// <summary>
/// A base type for an <see cref="IAsyncInterceptor"/> which only does some some small processing when
/// intercepting a method <see cref="IInvocation"/>
/// A base type for an <see cref="IAsyncInterceptor"/> which executes only minimal processing when intercepting a
/// method <see cref="IInvocation"/>
/// </summary>
/// <typeparam name="TState">
/// The type of the custom object used to maintain state between <see cref="StartingInvocation"/> and
Expand Down
Loading

0 comments on commit 24d79c6

Please sign in to comment.