Включение зависимостей в фильтры действия 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 инъекции зависимостей в фильтры действия были немного болью в заднице. Наиболее распространенный подход заключался в использовании пользовательского action invoker, который можно увидеть здесь: 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 во время тестирования и таким образом вводит макет службы. Вы не привязаны к какой-либо реализации контейнера DI, так как вы используете DependencyResolver, так есть ли какие-либо недостатки этого подхода?

Кстати, вот еще один хороший подход для этого в MVC3 с использованием нового интерфейса IFilterProvider: http://www.thecodinghumanist.com/blog/archives/2011/1/27/structuremap-action-filters-and-dependency-injection-in-asp-net-mvc-3

Ответ 1

Я не уверен, но я считаю, что вы можете просто использовать пустой конструктор (для части атрибута), а затем иметь конструктор, который фактически вводит значение (для части фильтра). *

Изменить. После небольшого считывания, кажется, что принятый способ сделать это - через инъекцию свойств:

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

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

Относительно того, почему не следует использовать вопрос "Локатор сервисов": это в основном снижает гибкость вашей инъекции зависимостей. Например, что, если вы вводили службу ведения журнала, и вы хотели автоматически предоставить службе ведения журнала имя вводимого класса? Если вы используете инъекцию конструктора, это будет работать отлично. Если вы используете Resolver Dependency/Service Locator, вам не повезло.

Update

Поскольку это было принято в качестве ответа, я хотел бы перейти к записи, чтобы сказать, что предпочитаю подход Mark Seeman, поскольку он отделяет ответственность за фильтр действий из атрибута. Кроме того, расширение Ninject MVC3 имеет очень мощные способы настройки фильтров действий с помощью привязок. Для получения дополнительной информации см. Следующие ссылки:

Обновление 2

Как отмечалось в примечаниях к @usr в комментариях ниже, ActionFilterAttribute создаются при загрузке класса и сохраняются на протяжении всего времени работы приложения. Если интерфейс IMyService не должен быть Singleton, он становится Captive Dependency. Если его реализация не является потокобезопасной, вы можете испытывать большую боль.

Всякий раз, когда у вас есть зависимость с более короткой продолжительностью жизни, чем ожидаемый срок службы вашего класса, целесообразно ввести factory для создания этой зависимости по требованию, а не для ее непосредственного ввода.

Ответ 2

Да, есть недостатки, так как есть множество проблем с IDependencyResolver, и тем, кто может добавить использование Singleton 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

Ответ 3

Решение, предложенное Марк Семанн, кажется элегантным. Однако довольно сложно для простой проблемы. Использование рамки с помощью AuthorizeAttribute кажется более естественным.

Моим решением было создать AuthorizeAttribute со статическим делегатом factory для службы, зарегистрированной в 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);
    }
}