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


78

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

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容器的实现,所以这种方法有不利之处吗?

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


感谢您链接到我的帖子:)。我认为这会很好。尽管我今年早些时候发表了博客文章,但实际上我并不是MVC 3中包含的DI的忠实拥护者,并且最近一直没有使用它。它似乎有效,但有时会感到尴尬。
Mallioch 2011年

如果您使用Ninject,这可能是一个可行的方法:stackoverflow.com/questions/6193414/...
罗宾的van der Knaap

+1,尽管服务定位器被许多人视为反模式,但我认为我更喜欢您的方法而不是Marks,因为它简单易行,而且事实是在一个地方(IOC容器)解决了依赖性,而在Mark的示例中,您会必须在引导程序和注册全局过滤器的两个地方进行解析,这是错误的。
magritte 2011年

您仍然可以随时使用“ DependencyResolver.Current.GetService(Type)。”
Mert Susur 2014年

Answers:


31

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

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

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。如果其实现不是线程安全的,那么您可能会很痛苦。

每当您的依赖项的寿命比班级的预期寿命短时,明智的方法是注入工厂以按需生成该依赖项,而不是直接注入它。


关于缺乏灵活性的评论:在DependencyResolver后面是一个驱动它的实际IOC容器,因此您可以在构建对象时在其中添加任何想要的自定义逻辑。不确定我是否会遵循您的意思.....
BFree

@BFree:调用时DependencyResolver.GetService,绑定方法不知道此依赖项将注入哪个类。如果您想IMyService为某些类型的操作过滤器创建不同的内容怎么办?或者,就像我在回答中所说的那样,如果您想为实现提供一个特殊的参数MyService以告诉它注入了什么类(这对记录器很有用),该怎么办?
StriplingWarrior

好吧,我做了一些弄乱,您是100%正确的,没有办法获得当前解决方案正在发生的“上下文”,所以是不利的。好点子。不过,我会说,添加Inject属性也很丑陋,因为这会将您的服务也绑定到特定容器的实现上,而我的DependencyResolver方法却没有。我让这个问题待一会儿,我很好奇听到更多意见。谢谢!
BFree

3
操作筛选器在MVC 3中的请求之间共享。这是高度线程安全的。
2014年

3
好的,我已经删除了downvote。这是不合适的。我最终将删除这些评论。在我看来,将MVC3更改为使滤镜成为单例的做法没有积极价值,而且非常危险。我的目的是在别人发现生产中的麻烦时为他们省些麻烦。
usr 2014年

92

是的,还有缺点,因为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
    }
}

请注意,过滤器如何检查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


22
你问到什么缺点是:该缺点是你的代码库的可维护性下降,但几乎感觉不到混凝土。这是你身上蔓延的东西。我不能说,如果您按照自己的意愿去做,就会出现竞争状况,或者CPU可能过热,或者小猫会死。那不会发生,但是如果您不遵循正确的设计模式并且避免使用反模式,那么您的代码将会烂掉,并且从现在开始四年后,您将希望从头开始重写应用程序(但是您的涉众不会允许您)。
马克·西曼

4
+1,用于显示如何将动作过滤器代码与属性代码分开。我更希望仅出于分离关注点的目的使用此方法。我确实感谢OP对问题“这是怎么回事”部分含糊不清的无奈之举。称其为反模式很容易,但是当他的特定代码处理了反对反模式的大多数参数(单元可测试性,通过配置绑定等)时,很高兴知道为什么这种模式会使代码腐烂比“更纯净”的代码更快。并不是说我不同意你的看法。我喜欢你的书,顺便说一句。
StriplingWarrior

5
@BFree:顺便说一句,Remo Gloor通过Ninject的MVC3扩展做了一些很棒的事情。github.com/ninject/ninject.web.mvc/wiki/…描述了如何使用Ninject绑定来定义动作过滤器,该动作过滤器将应用于控制器或具有特定属性的动作,而不必全局注册过滤器。这将更多的控制权转移到您的Ninject绑定中,这就是IoC的重点。
StriplingWarrior

1
如何实现方法-草图:filterContext的一部分的ActionDescriptor实现了ICustomAttributeProvider,因此您可以从那里拉出marker属性。
Mark Seemann

1
@Mark:四年后,您将要从头开始重写应用程序(但您的涉众不会让您) -否则他们因为TTM时间太长而让他和产品死亡。
2011年

7

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

我喜欢这段代码,因为您没有将Service Locator与MyAuthorizeAttribute高耦合。
山本彰(Akira Yamamoto)
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.