如何使用Ninject拦截器记录所有ASP.NET WebApi控制器操作方法的调用?

20

我们公司需要在每次调用ASP.NET WebApi控制器的动作方法时记录某些信息。由于我们目前使用Ninject进行DI,因此我们希望也能将其用于此目的。这是我迄今为止尝试过的。

我已经通过NuGet安装了Ninject、Ninject.Extensions.Interception和Ninject.Extensions.Interception.DynamicProxy,并拥有以下模块。

public class InterceptAllModule : InterceptionModule
{
    public override void Load()
    {
        Kernel.Intercept(p => p.Request.Service.Name.EndsWith("Controller")).With(new TimingInterceptor());
    }
}

TimingInterceptor 的作用是

public class TimingInterceptor : SimpleInterceptor
{
    readonly Stopwatch _stopwatch = new Stopwatch();
    protected override void BeforeInvoke(IInvocation invocation)
    {
        _stopwatch.Start();
    }

    protected override void AfterInvoke(IInvocation invocation)
    {
        _stopwatch.Stop();
        string message = string.Format("[Execution of {0} took {1}.]",invocation.Request.Method,_stopwatch.Elapsed);
        Log.Info(message + "\n");
        _stopwatch.Reset();
    }
}

现在,当我尝试将模块与Ninject内核连接并运行我的网站时

var kernel = new StandardKernel(new InterceptAllModule());

然而,每当有一个呼叫到达其中一个操作方法时,它会抛出一个错误,内容为:

Cannot instantiate proxy of class: MyApiController.

有经验的人能否指出我做错了什么,谢谢。


也许这个建议对设计来说是一个太大的转变,但是当你将所有业务操作抽象到通用的抽象层后,并将其注入控制器中,你就可以轻松地装饰或拦截这些抽象层,以添加横切关注点,比如时间方面。看一下命令/处理程序模式,了解我在谈论什么。 - Steven
1
你是否有一个额外的无参构造函数,并且所有的操作方法都是虚拟的? - Remo Gloor
1
谢谢Remo,我已经将virtual添加到所有的操作方法中,但是对于无参数构造函数,我有一个问题,因为我们的服务对象被注入到那里并在控制器中使用。使用无参数构造函数时,服务对象为空。你如何解决这个问题?非常感谢! - Ray
2个回答

30

更新

所以,使用您的代码和Remo提出的很好的观点,需要将操作方法设置为虚拟的,并放置一个空的默认构造函数(只是为了平息动态代理,保留您的其他构造函数),我已经让操作过滤器和拦截器方法均可正常工作。

我认为,目前为止,您的代码将拦截ApiController上可能不需要的方法,因此您可能还需要编写一些代码来过滤这些方法,例如ExecuteAsync和Dispose。

我的另一个建议是性能方面。 非常重要的免责声明 这些仅是非常基本的测试(每次使用操作过滤器方法记录统计信息),我邀请您进行自己的测试!...但是使用DynamicProxy拦截器时,我得到了大约每个GET请求4毫秒的时间。

[Execution of Get took 00:00:00.0046615.]
[Execution of Get took 00:00:00.0041988.]
[Execution of Get took 00:00:00.0039383.]

将拦截代码注释掉并使用操作过滤器,我获得了亚毫秒级的性能表现:

[Execution of Get took 00:00:00.0001146.]
[Execution of Get took 00:00:00.0001116.]
[Execution of Get took 00:00:00.0001364.]

这是否是一个问题或关注点完全取决于您,但我想指出这一点。

之前的回复

您是否考虑过使用ActionFilters?这是在MVC操作上进行AOP的自然扩展点。

如果您对除控制器上的实际操作以外的方法感兴趣,那么我可以理解,但我还是想提供一个建议。

灵感来自Are ActionFilterAttributes reused across threads? How does that work?Measure Time Invoking ASP.NET MVC Controller Actions

更新以显示标记方法时计时器的排除。灵感来自核心WebApi框架,特别是AllowAnonymousAttributeAuthorizeAttribute

全局注册此功能,以便所有操作都受到监视:

GlobalConfiguration.Configuration.Filters.Add(new TimingActionFilter());

那么:

public class TimingActionFilter : ActionFilterAttribute
{
    private const string Key = "__action_duration__";

    public override void OnActionExecuting(HttpActionContext actionContext)
    {
        if (SkipLogging(actionContext))
        {
            return;
        }

        var stopWatch = new Stopwatch();
        actionContext.Request.Properties[Key] = stopWatch;
        stopWatch.Start();
    }

    public override void OnActionExecuted(HttpActionExecutedContext actionExecutedContext)
    {
        if (!actionExecutedContext.Request.Properties.ContainsKey(Key))
        {
            return;
        }

        var stopWatch = actionExecutedContext.Request.Properties[Key] as Stopwatch;
        if(stopWatch != null)
        {
            stopWatch.Stop();
            var actionName = actionExecutedContext.ActionContext.ActionDescriptor.ActionName;
            Debug.Print(string.Format("[Execution of {0} took {1}.]", actionName, stopWatch.Elapsed));
        }

    }

    private static bool SkipLogging(HttpActionContext actionContext)
    {
        return actionContext.ActionDescriptor.GetCustomAttributes<NoLogAttribute>().Any() ||
                actionContext.ControllerContext.ControllerDescriptor.GetCustomAttributes<NoLogAttribute>().Any();
    }
}

[AttributeUsage(AttributeTargets.Method | AttributeTargets.Class, Inherited = true)]
public class NoLogAttribute : Attribute
{

}

现在,您可以使用以下方法排除全局过滤器:

public class ExampleController : ApiController
{
    // GET api/example
    [NoLog]
    public Example Get()
    {
       //
    }
}

+1 嗨,马克,感谢你的回答。但是我的经理不想使用操作过滤器属性。他希望所有的操作方法都被记录,除了那些我们标记为 [nolog] 的属性。 - Ray
1
@ray247,感谢您让我提出建议。既然已经开始了,那就彻底做好吧。我已经更新了代码,使用这种方法来展示NoLog的能力。我还会很快更新答案,加入一些Ninject的例子。 - Mark Jones
2
非常感谢你,马克。我还没有尝试过你的代码,但我绝对同意你上面展示的代码。GlobalConfiguration.Configuration.Filters.Add将使其适用于每个操作,而NoLog属性将过滤掉我们不想要的操作。我的同事提出了一个使用HttpModule拦截每个请求的解决方案。但是你的更适合MVC项目。谢谢你,祝你新年快乐。标记为答案! - Ray
@MarkJones,请问您能否帮我回答这个问题:https://dev59.com/aIDba4cB1Zd3GeqPHKpp#24235652 - eddy
如果你可以走这条路线,那么OWIN中间件似乎是一个不错的选择。 - jpierson

2

如果还有人在潜水,我想使用Ninject的原因是我想将一个记录器(或任何其他东西)注入到拦截器中,但我希望拦截所有操作。

Mark的答案很完美,但是不要使用全局注册,可以使用

GlobalConfiguration.Configuration.Filters.Add(new TimingActionFilter());

使用Ninject绑定您的过滤器。
Kernal.BindHttpFilter<TimingActionFilter>(FilterScope.Action).

您需要在TimingActionFilter类中创建一个适当的构造函数。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接