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

BFr*_*ree 77 c# asp.net-mvc dependency-injection actionfilterattribute asp.net-mvc-3

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

public interface IMyService
{
   void DoSomething();
}

public class MyService : IMyService
{
   public void DoSomething(){}
}
Run Code Online (Sandbox Code Playgroud)

然后我有一个需要该服务实例的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);
   }
}
Run Code Online (Sandbox Code Playgroud)

在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);
   }
}
Run Code Online (Sandbox Code Playgroud)

这里我们使用构造函数注入和重载构造函数来使用容器并注入服务.我确实同意将容器与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);
   }
}
Run Code Online (Sandbox Code Playgroud)

现在我知道一些纯粹主义者可能会嘲笑这一点,但严重的是,这将是什么缺点?它仍然是可测试的,因为你可以使用在测试时采用IMyService并以这种方式注入模拟服务的构造函数.由于您使用的是DependencyResolver,因此您没有被限制在DI容器的任何实现中,因此这种方法有任何缺点吗?

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

Mar*_*ann 91

是的,有一些缺点,因为IDependencyResolver本身存在很多问题,对于那些你可以添加使用Singleton Service Locator以及Bastard Injection的问题.

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

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
    }
}
Run Code Online (Sandbox Code Playgroud)

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

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

public class MyActionFilterAttribute : Attribute { }
Run Code Online (Sandbox Code Playgroud)

但是,现在该属性完全是惰性的.

过滤器可以使用所需的依赖项组成,并添加到global.asax中的全局过滤器中:

GlobalFilters.Filters.Add(new MyActionFilter(new MyService()));
Run Code Online (Sandbox Code Playgroud)

有关此技术的更详细示例,尽管应用于ASP.NET Web API而不是MVC,请参阅此文章:http://blog.ploeh.dk/2014/06/13/passive-attributes

  • 您询问了缺点是什么:缺点是代码库的可维护性降低,但这几乎感觉不到*具体*.这是你的事情.我不能说如果你按照你的意愿行事,你就会遇到竞争状态,或者CPU可能会过热,或者小猫会死.这不会发生,但是如果你没有遵循正确的设计模式并避免反模式,那么你的代码就会腐烂,从现在起四年你就会想要从头开始重写应用程序(但你的利益相关者不会让你). (22认同)
  • @BFree:顺便说一下,R​​emo Gloor为Ninject的MVC3扩展做了一些很棒的事情.https://github.com/ninject/ninject.web.mvc/wiki/Conditional-bindings-for-filters描述了如何使用Ninject绑定来定义应用于控制器或具有特定属性的操作的操作过滤器而不是必须全局注册过滤器.这会将更多控制权转移到您的Ninject绑定中,这是IoC的重点. (5认同)
  • +1表示如何将动作过滤器代码与属性代码分开.我更喜欢这种方法只是为了分离关注点.我很欣赏OP对这个问题的"这个问题有什么不对"的含糊不清感到沮丧.调用反模式很容易,但是当他的特定代码解决了反模式的大多数参数(单元可测试性,通过配置绑定等)时,很高兴知道*为什么*这个模式使得代码腐烂比"纯"代码更快.不是说我不同意你的看法.我很喜欢你的书,BTW. (4认同)

Str*_*ior 30

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

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

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

    public override void OnActionExecuting(ActionExecutingContext filterContext)
    {
        MyService.DoSomething();
        base.OnActionExecuting(filterContext);
    }
}
Run Code Online (Sandbox Code Playgroud)

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

更新

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

更新2

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

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

  • 动作过滤器在MVC 3中的请求之间共享.这是高度线程不安全的. (3认同)
  • 好的,我已经删除了downvote.这不合适.我最终会删除这些评论.在我看来,MVC3的改变使得过滤单体是没有正面价值而且非常危险的.我的意图是当他们在生产中发现这一点时为他人省去一些麻烦. (3认同)

Jak*_*kob 6

Mark Seemann建议的解决方案似乎很优雅.然而,对于一个简单的问 通过实现AuthorizeAttribute来使用框架感觉更自然.

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

在global.asax中:

MyAuthorizeAttribute.AuthorizeServiceFactory = () => Container.Resolve<IAuthorizeService>();
Run Code Online (Sandbox Code Playgroud)

我的自定义属性类:

[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);
    }
}
Run Code Online (Sandbox Code Playgroud)