将依赖项注入ASP.. NET MVC 3动作过滤器.这种方法有什么问题?

本文关键字:过滤器 方法 问题 什么 注入 依赖 ASP NET MVC | 更新日期: 2023-09-27 18:06:34

设置如下。假设我有一个需要服务实例的动作过滤器:

public interface IMyService
{
   void DoSomething();
}
public class MyService : IMyService
{
   public void DoSomething(){}
}

然后我有一个ActionFilter,它需要该服务的一个实例:

public class MyActionFilter : ActionFilterAttribute
{
   private IMyService _myService; // <--- How do we get this injected
   public override void OnActionExecuting(ActionExecutingContext filterContext)
   {
       _myService.DoSomething();
       base.OnActionExecuting(filterContext);
   }
}

在MVC 1/2中,将依赖项注入到动作过滤器中有点麻烦。最常见的方法是使用自定义动作调用程序,如下所示:http://www.jeremyskinner.co.uk/2008/11/08/dependency-injection-with-aspnet-mvc-action-filters/这种解决方法背后的主要动机是,这种方法被认为是粗心的,并且与容器紧密耦合:

public class MyActionFilter : ActionFilterAttribute
{
   private IMyService _myService;
   public MyActionFilter()
      :this(MyStaticKernel.Get<IMyService>()) //using Ninject, but would apply to any container
   {
   }
   public MyActionFilter(IMyService myService)
   {
      _myService = myService;
   }
   public override void OnActionExecuting(ActionExecutingContext filterContext)
   {
       _myService.DoSomething();
       base.OnActionExecuting(filterContext);
   }
}

这里我们使用构造函数注入并重载构造函数来使用容器并注入服务。我同意这确实紧密耦合容器与ActionFilter。

我的问题是:现在在ASP。. NET MVC 3,在那里我们有一个正在使用的容器的抽象(通过DependencyResolver)所有这些箍仍然是必要的吗?请允许我演示一下:

public class MyActionFilter : ActionFilterAttribute
{
   private IMyService _myService;
   public MyActionFilter()
      :this(DependencyResolver.Current.GetService(typeof(IMyService)) as IMyService)
   {
   }
   public MyActionFilter(IMyService myService)
   {
      _myService = myService;
   }
   public override void OnActionExecuting(ActionExecutingContext filterContext)
   {
       _myService.DoSomething();
       base.OnActionExecuting(filterContext);
   }
}

现在我知道一些纯粹主义者可能会嘲笑这一点,但说真的,会有什么缺点呢?它仍然是可测试的,因为您可以使用在测试时接受IMyService的构造函数,并以这种方式注入模拟服务。由于您使用的是DependencyResolver,因此您没有被束缚于任何DI容器的实现,那么这种方法有什么缺点吗?

顺便说一下,这里有另一种很好的方法在MVC3中使用新的IFilterProvider接口:http://www.thecodinghumanist.com/blog/archives/2011/1/27/structuremap-action-filters-and-dependency-injection-in-asp-net-mvc-3

将依赖项注入ASP.. NET MVC 3动作过滤器.这种方法有什么问题?

是的,有缺点,因为有很多问题与IDependencyResolver本身,和那些你可以添加使用单例服务定位器,以及Bastard注入。

一个更好的选择是将过滤器实现为一个普通类,你可以向其中注入任何你想要的服务:

public class MyActionFilter : IActionFilter
{
    private readonly IMyService myService;
    public MyActionFilter(IMyService myService)
    {
        this.myService = myService;
    }
    public void OnActionExecuting(ActionExecutingContext filterContext)
    {
        if(this.ApplyBehavior(filterContext))
            this.myService.DoSomething();
    }
    public void OnActionExecuted(ActionExecutedContext filterContext)
    {
        if(this.ApplyBehavior(filterContext))
            this.myService.DoSomething();
    }
    private bool ApplyBehavior(ActionExecutingContext filterContext)
    {
        // Look for a marker attribute in the filterContext or use some other rule
        // to determine whether or not to apply the behavior.
    }
    private bool ApplyBehavior(ActionExecutedContext filterContext)
    {
        // Same as above
    }
}

请注意过滤器是如何检查filterContext以确定是否应该应用该行为的。

这意味着您仍然可以使用属性来控制是否应该应用过滤器:

public class MyActionFilterAttribute : Attribute { }

但是,现在这个属性是完全惰性的。

过滤器可以由所需的依赖项组成,并添加到global.asax:

中的全局过滤器中。
GlobalFilters.Filters.Add(new MyActionFilter(new MyService()));

对于该技术的更详细的示例,虽然应用于ASP。. NET Web API代替MVC,参见这篇文章:http://blog.ploeh.dk/2014/06/13/passive-attributes

我不是很肯定,但我相信你可以只使用一个空的构造函数(对于属性部分),然后有一个构造函数实际注入值(对于过滤器部分)。

编辑:经过一番阅读,似乎可以接受的方式是通过属性注入:

public class MyActionFilter : ActionFilterAttribute
{
    [Injected]
    public IMyService MyService {get;set;}
    
    public override void OnActionExecuting(ActionExecutingContext filterContext)
    {
        MyService.DoSomething();
        base.OnActionExecuting(filterContext);
    }
}
关于为什么不使用服务定位器的问题:它只会降低依赖注入的灵活性。例如,如果您正在注入一个日志服务,并且希望自动为日志服务提供被注入的类的名称,该怎么办?如果使用构造函数注入,效果会很好。如果你使用的是依赖解析器/服务定位器,那你就不走运了。

更新

既然这个答案被接受了,我想公开地说,我更喜欢Mark Seeman的方法,因为它将动作过滤器的职责从属性中分离出来。此外,Ninject的MVC3扩展有一些非常强大的方法来通过绑定配置动作过滤器。有关详细信息,请参阅以下参考资料:

  • https://github.com/ninject/ninject.web.mvc/wiki/Dependency-injection-for-filters
  • https://github.com/ninject/ninject.web.mvc/wiki/Conditional-bindings-for-filters
  • https://github.com/ninject/ninject.web.mvc/wiki/Filter-configurations

更新2

正如@usr在下面的评论中指出的那样,ActionFilterAttribute在类被加载时被实例化,并且它们将持续整个应用程序的生命周期。如果IMyService接口不应该是单例的,那么它最终会成为一个强制依赖。如果它的实现不是线程安全的,你可能会很痛苦。

当你有一个依赖的寿命比你的类的预期寿命短的时候,明智的做法是注入一个工厂来按需生产这个依赖,而不是直接注入。

Mark Seemann提出的解决方案似乎很优雅。然而,对于一个简单的问题来说,这是相当复杂的。通过实现AuthorizeAttribute来使用框架感觉更自然。

我的解决方案是创建一个AuthorizeAttribute和一个静态委托工厂到一个注册在global.asax中的服务。它适用于任何DI容器,感觉比服务定位器好一点。

在global.asax:

MyAuthorizeAttribute.AuthorizeServiceFactory = () => Container.Resolve<IAuthorizeService>();

我的自定义属性类:

[AttributeUsage(AttributeTargets.Method | AttributeTargets.Class)]
public class MyAuthorizeAttribute : AuthorizeAttribute
{
    public static Func<IAuthorizeService> AuthorizeServiceFactory { get; set; } 
    protected override bool AuthorizeCore(HttpContextBase httpContext)
    {
        return AuthorizeServiceFactory().AuthorizeCore(httpContext);
    }
}