【问题标题】:Making Ninject Interceptors work with async methods使 Ninject 拦截器与异步方法一起工作
【发布时间】:2023-12-10 13:13:02
【问题描述】:

我开始使用 ninject 拦截器来包装我的一些具有各种行为的异步代码,并且在让一切正常工作时遇到了一些麻烦。

这是我正在使用的拦截器:

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;
        }
    }

这似乎在大多数正常情况下都能正常运行。我不确定这是否会达到我的预期。尽管它似乎异步地将控制流返回给调用者,但我仍然有点担心代理无意中阻塞线程或其他东西的可能性。

除此之外,我无法让异常处理正常工作。对于这个测试用例:

[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)
    {

    }
}

我可以在拦截器中看到 catch 块被击中,但我的测试中的 catch 块从未被 rethrow 击中。我更困惑,因为这里没有代理或任何东西,只有非常简单的模拟和对象。我还在测试中尝试了Task.Run(() =&gt; interceptor.Intercept(invocation.Object)).Wait(); 之类的东西,但仍然没有变化。测试顺利通过,但 nUnit 输出确实有异常消息。

我想我把事情搞砸了,我并不像我想的那样完全理解发生了什么。有没有更好的方法来拦截异步方法?我在异常处理方面做错了什么?

【问题讨论】:

  • 我认为这可能与IInterceptor.Intercept方法无效有关。我可以让它异步,但也许我在测试中没有正确地等待它或其他东西。不过这对我来说没有意义,因为在这条路径中甚至还没有发生任何异步事件。
  • 在深入挖掘时,在我看来,异常是在与主测试线程具有不同同步上下文的线程上引发的。由于它是一个 void 我无法真正同步等待它或捕获它的异常。除了重写拦截插件,我能做些什么吗?
  • async void 方法几乎总是一个坏主意,除非您正在编写事件处理程序。我认为要做到这一点,Ninject 必须支持async

标签: c# ninject async-await ninject-interception


【解决方案1】:

如果您还没有阅读我的async/await intro,我建议您阅读。您需要非常了解 async 方法与其返回的 Task 的关系,以便拦截它们。

考虑您当前的Intercept 实施。正如 svick 所说,最好避免使用async void。一个原因是错误处理异常:async void 方法的任何异常都会直接在当前SynchronizationContext 上引发。

在您的情况下,如果 Proceed 方法引发异常(就像您的模拟一样),那么您的 async void Intercept 实现将引发异常,该异常将直接发送到 SynchronizationContextwhich is a default - or thread pool - SynchronizationContext since this is a unit test,如我在我的博客上解释)。因此,您会在某个随机线程池线程上看到该异常,而不是在单元测试的上下文中。

要解决此问题,您必须重新考虑Intercept。常规拦截只允许你拦截async方法的first部分;要响应async 方法的结果,您需要在返回的Task 完成时响应。

这是一个简单的例子,它只捕获返回的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;
        }
    }
}

您可能还想运行NUnit 2.6.2 or later, which added support for async unit tests。这将使您能够await 您的MyInterceptor.Result(这将在单元测试上下文中正确引发异常)。

如果你想要更复杂的异步拦截,你可以使用async——而不是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);
    }
}

不幸的是,拦截必须同步进行,所以不可能有异步预执行(除非你同步等待它完成,或者使用IChangeProxyTarget)。不过,即使有这个限制,您也应该能够使用上述技术做几乎任何您需要的事情。

【讨论】:

  • 这是一个了不起的答案,似乎解决了我的问题。我增加了一些复杂性来检查它是否返回一个任务,所以我的拦截器可以是同步的或异步的,但这很好用。我没有想过使用返回值来传播任务而不是拦截器链本身。谢谢。
最近更新 更多