Making Ninject Interceptors work with async methods
Asked Answered
T

1

5

I am starting to work with ninject interceptors to wrap some of my async code with various behaviors and am having some trouble getting everything working.

Here is an interceptor I am working with:

public class MyInterceptor : IInterceptor
{
    public async void Intercept(IInvocation invocation)
    {
        try
        {
            invocation.Proceed();
            //check that method indeed returns Task
            await (Task) invocation.ReturnValue;
            RecordSuccess();
        }
        catch (Exception)
        {
            RecordError();
            invocation.ReturnValue = _defaultValue;
            throw;
        }
    }

This appears to run properly in most normal cases. I am not sure if this will do what I expect. Although it appears to return control flow to the caller asynchronously, I am still a bit worried about the possibility that the proxy is unintentionally blocking a thread or something.

That aside, I cannot get the exception handling working. For this test case:

[Test]
public void ExceptionThrown()
{
    try
    {
        var interceptor = new MyInterceptor(DefaultValue);
        var invocation = new Mock<IInvocation>();
        invocation.Setup(x => x.Proceed()).Throws<InvalidOperationException>();
        interceptor.Intercept(invocation.Object);
    }
    catch (Exception e)
    {

    }
}

I can see in the interceptor that the catch block is hit, but the catch block in my test is never hit from the rethrow. I am more confused because there is no proxy or anything here, just pretty simple mocks and objects. I also tried something like Task.Run(() => interceptor.Intercept(invocation.Object)).Wait(); in my test, and still no change. The test passes happily, but the nUnit output does have the exception message.

I imagine I am messing something up, and I don't quite understand what is going on as much as I think I do. Is there a better way to intercept an async method? What am I doing wrong with regards to exception handling?

Tessler answered 29/11, 2012 at 16:44 Comment(3)
I think this may be related to the fact that the IInterceptor.Intercept method is void. I can make it async, but maybe I am not properly waiting for it in the test or something. That doesn't make sense to me though, because in this path nothing asynchronous has even happened yet.Tessler
On digging deeper it appears to me that the exception is being thrown on a thread with a different synchronization context than the main test thread. Since it is a void I have no way to really wait synchronously on it or to catch its exceptions. Is there anything I can do of this, short of rewriting the interception plugin?Tessler
async void method are almost always a bad idea, unless you're writing an event handler. And I think to do this, Ninject would have to support async.Kilbride
S
10

I recommend you read my async/await intro, if you haven't already done so. You need a really good grasp of how async methods relate to their returned Task in order to intercept them.

Consider your current Intercept implementation. As svick commented, it's best to avoid async void. One reason is the error handling is unusual: any exceptions from async void methods are raised on the current SynchronizationContext directly.

In your case, if the Proceed method raises an exception (like your mock will), then your async void Intercept implementation will raise the exception, which will get sent directly to the SynchronizationContext (which is a default - or thread pool - SynchronizationContext since this is a unit test, as I explain on my blog). So you will see that exception raised on some random thread pool thread, not in the context of your unit test.

To fix this, you must rethink Intercept. Regular interception only allows you to intercept the first part of an async method; to respond to the result of an async method, you'll need to respond when the returned Task completes.

Here's a simple example that just captures the returned Task:

public class MyInterceptor : IInterceptor
{
    public Task Result { get; private set; }

    public void Intercept(IInvocation invocation)
    {
        try
        {
            invocation.Proceed();
            Result = (Task)invocation.ReturnValue;
        }
        catch (Exception ex)
        {
            var tcs = new TaskCompletionSource<object>();
            tcs.SetException(ex);
            Result = tcs.Task;
            throw;
        }
    }
}

You also probably want to be running NUnit 2.6.2 or later, which added support for async unit tests. This will enable you to await your MyInterceptor.Result (which will properly raise the exception in the unit test context).

If you want more complex asynchronous interception, you can use async - just not async void. ;)

// Assumes the method returns a plain Task
public class MyInterceptor : IInterceptor
{
    private static async Task InterceptAsync(Task originalTask)
    {
        // Await for the original task to complete
        await originalTask;

        // asynchronous post-execution
        await Task.Delay(100);
    }

    public void Intercept(IInvocation invocation)
    {
        // synchronous pre-execution can go here
        invocation.Proceed();
        invocation.ReturnValue = InterceptAsync((Task)invocation.ReturnValue);
    }
}

Unfortunately, interception must Proceed synchronously, so it's not possible to have asynchronous pre-execution (unless you synchronously wait for it to complete, or use IChangeProxyTarget). Even with that limitation, though, you should be able to do pretty much anything you need using the techniques above.

Scout answered 30/11, 2012 at 4:36 Comment(1)
This is an amazing answer and seems to solve my problem. I add some complexity to check if it returns a Task or not, so my interceptors can be synchronous or asynchronous, but this works well. I hadn't thought to use the return value to propigate the task rather than the interceptor chain itself. Thank you.Tessler

© 2022 - 2024 — McMap. All rights reserved.