Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Make exception handling simpler #20

Merged
merged 7 commits into from
Sep 26, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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