IT-Swarm.Net

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

这是设置。假设我有一些需要服务实例的动作过滤器:

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/ 这种解决方法背后的主要动机是因为以下方法被认为是与容器的邋and和紧密耦合:

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容器的任何实现中,因此这种方法有任何缺点吗?

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

76
BFree

我不是肯定的,但我相信你可以使用一个空构造函数(对于attribute part)然后有一个实际注入值的构造函数(对于filter part) *。

编辑 :稍微阅读后,似乎可接受的方法是通过属性注入:

public class MyActionFilter : ActionFilterAttribute
{
    [Injected]
    public IMyService MyService {get;set;}

    public override void OnActionExecuting(ActionExecutingContext filterContext)
    {
        MyService.DoSomething();
        base.OnActionExecuting(filterContext);
    }
}

关于为什么不使用服务定位器问题:它主要只是降低了依赖注入的灵活性。例如,如果您正在注入日志记录服务,并且您希望自动为日志记录服务提供正在注入的类的名称,该怎么办?如果你使用构造函数注入,那将是很好的。如果您使用的是依赖性解析器/服务定位器,那么您将失去运气。

更新

由于这被接受为答案,我想记录下来说我更喜欢 Mark Seeman的方法 因为它将Action Filter责任与Attribute分开。此外,Ninject的MVC3扩展还有一些非常强大的方法可以通过绑定配置动作过滤器。有关更多详细信息,请参阅以下参考

更新2

正如@usr在下面的注释中指出的那样,ActionFilterAttributes在加载类时被实例化,并且它们持续应用程序的整个生命周期。如果IMyService接口不应该是Singleton,那么它最终成为 Captive Dependency 。如果它的实现不是线程安全的,那么你可能会遇到很多痛苦。

每当你的生命周期短于你班级的预期寿命时,最好注入一个工厂来按需生成依赖,而不是直接注入。

30
StriplingWarrior

是的,有缺点,因为有 很多问题与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

91
Mark Seemann

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);
    }
}
6
Jakob