9

We basically have a class that looks like this below that is using the Castle.DynamicProxy for Interception.

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

namespace SaaS.Core.IoC
{
    public abstract class AsyncInterceptor : IInterceptor
    {
        private readonly ILog _logger;

        private readonly ConcurrentDictionary<Type, Func<Task, IInvocation, Task>> wrapperCreators =
            new ConcurrentDictionary<Type, Func<Task, IInvocation, Task>>();

        protected AsyncInterceptor(ILog logger)
        {
            _logger = logger;
        }

        void IInterceptor.Intercept(IInvocation invocation)
        {
            if (!typeof(Task).IsAssignableFrom(invocation.Method.ReturnType))
            {
                InterceptSync(invocation);
                return;
            }

            try
            {
                CheckCurrentSyncronizationContext();
                var method = invocation.Method;

                if ((method != null) && typeof(Task).IsAssignableFrom(method.ReturnType))
                {
                    var taskWrapper = GetWrapperCreator(method.ReturnType);
                    Task.Factory.StartNew(
                        async () => { await InterceptAsync(invocation, taskWrapper).ConfigureAwait(true); }
                        , // this will use current synchronization context
                        CancellationToken.None,
                        TaskCreationOptions.AttachedToParent,
                        TaskScheduler.FromCurrentSynchronizationContext()).Wait();
                }
            }
            catch (Exception ex)
            {
                //this is not really burring the exception
                //excepiton is going back in the invocation.ReturnValue which 
                //is a Task that failed. with the same excpetion 
                //as ex.
            }
        }
....

Initially this code was:

Task.Run(async () => { await InterceptAsync(invocation, taskWrapper)).Wait()

But we were losing HttpContext after any call to this, so we had to switch it to:

Task.Factory.StartNew 

So we could pass in the TaskScheduler.FromCurrentSynchronizationContext()

All of this is bad because we are really just swapping one thread for another thread. I would really love to change the signature of

void IInterceptor.Intercept(IInvocation invocation)

to

async Task IInterceptor.Intercept(IInvocation invocation)

And get rid of the Task.Run or Task.Factory and just make it:

await InterceptAsync(invocation, taskWrapper);

The problem is Castle.DynamicProxy IInterecptor won't allow this. I really want do an await in the Intercept. I could do .Result but then what is the point of the async call I am calling? Without being able to do the await I lose out of the benefit of it being able to yield this threads execution. I am not stuck with Castle Windsor for their DynamicProxy so I am looking for another way to do this. We have looked into Unity, but I don't want to replace our entire AutoFac implementation.

Any help would be appreciated.

Travis Illig
  • 23,195
  • 2
  • 62
  • 85
Eric Renken
  • 91
  • 1
  • 3

1 Answers1

9

All of this is bad because we are really just swapping one thread for another thread.

True. Also because the StartNew version isn't actually waiting for the method to complete; it will only wait until the first await. But if you add an Unwrap() to make it wait for the complete method, then I strongly suspect you'll end up with a deadlock.

The problem is Castle.DynamicProxy IInterecptor won't allow this.

IInterceptor does have a design limitation that it must proceed synchronously. So this limits your interception capabilities: you can inject synchronous code before or after the asynchronous method, and asynchronous code after the asynchronous method. There's no way to inject asynchronous code before the asynchronous method. It's just a limitation of DynamicProxy, one that would be extremely painful to correct (as in, break all existing user code).

To do the kinds of injection that is supported, you have to change your thinking a bit. One of the valid mental models of async is that a Task returned from a method represents the execution of that method. So, to append code to that method, you would call the method directly and then replace the task return value with an augmented one.

So, something like this (for return types of Task):

protected abstract void PreIntercept(); // must be sync
protected abstract Task PostInterceptAsync(); // may be sync or async

// This method will complete when PostInterceptAsync completes.
private async Task InterceptAsync(Task originalTask)
{
  // Asynchronously wait for the original task to complete
  await originalTask;

  // Asynchronous post-execution
  await PostInterceptAsync();
}

public void Intercept(IInvocation invocation)
{
  // Run the pre-interception code.
  PreIntercept();

  // *Start* the intercepted asynchronous method.
  invocation.Proceed();

  // Replace the return value so that it only completes when the post-interception code is complete.
  invocation.ReturnValue = InterceptAsync((Task)invocation.ReturnValue);
}

Note that the PreIntercept, the intercepted method, and PostInterceptAsync are all run in the original (ASP.NET) context.

P.S. A quick Google search for async DynamicProxy resulted in this. I don't have any idea how stable it is, though.

Stephen Cleary
  • 437,863
  • 77
  • 675
  • 810
  • 1
    Thanks for the link I will check it out and I will give your example a go. I have been searching for answers to this for days. – Eric Renken Sep 20 '16 at 15:29
  • I just looked into *why* DynamicProxy doesn't allow async code before `Proceed`, and the reason is that the invocation passed to the interceptor resets its state as soon as the interceptor returns. That is, if the interceptor sets as the `ReturnValue` a `Task` that first awaits some async stuff and _then_ calls `Proceed`, the invocation will have forgotten what to do next by the time `Proceed` is called by the `Task`. And this causes the whole invocation to start again from scratch, resulting in an endless loop. A pity - if the invocation used an immutable data structure, it would work. – Fabian Schmied Apr 26 '20 at 10:50