Внедрение зависимостей в фильтры действий 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/ Основная мотивация Причина этого обходного пути заключалась в том, что следующий подход считался небрежным и жестким взаимодействием с контейнером:

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


person BFree    schedule 25.08.2011    source источник
comment
Спасибо за ссылку на мой пост :). Я думаю, это было бы хорошо. Несмотря на мои сообщения в блоге, сделанные ранее в этом году, я на самом деле не большой поклонник DI, который они включили в MVC 3, и не использую его в последнее время. Кажется, это работает, но временами немного неловко.   -  person Mallioch    schedule 25.08.2011
comment
Если вы используете Ninject, это может быть возможным подходом: stackoverflow.com/questions/6193414/   -  person Robin van der Knaap    schedule 25.08.2011
comment
+1, хотя многие считают локатор сервисов анти-шаблоном, я думаю, что предпочитаю ваш подход меткам из-за его простоты, а также того факта, что зависимость разрешается в одном месте, контейнере IOC, тогда как в примере Марка вы бы приходится разрешать в двух местах, в загрузчике и при регистрации глобальных фильтров, что кажется неправильным.   -  person magritte    schedule 07.10.2011
comment
вы по-прежнему можете использовать DependencyResolver.Current.GetService (Type) в любое время, когда захотите.   -  person Mert Susur    schedule 19.09.2014


Ответы (3)


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

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

public class MyActionFilter : ActionFilterAttribute
{
    [Injected]
    public IMyService MyService {get;set;}
    
    public override void OnActionExecuting(ActionExecutingContext filterContext)
    {
        MyService.DoSomething();
        base.OnActionExecuting(filterContext);
    }
}

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

Обновлять

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

Обновление 2

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

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

person StriplingWarrior    schedule 25.08.2011
comment
Повторите свой комментарий об отсутствии гибкости: за DependencyResolver стоит реальный контейнер IOC, управляющий им, поэтому вы можете добавить любую настраиваемую логику прямо там, при создании объекта. Не уверен, что понимаю вашу точку зрения ... - person BFree; 25.08.2011
comment
@BFree: при вызове DependencyResolver.GetService метод привязки не знает, в какой класс внедряется эта зависимость. Что, если вы хотите создать другой IMyService для определенных типов фильтров действий? Или, как я сказал в своем ответе, что, если вы хотите предоставить специальный аргумент для реализации MyService, чтобы сообщить ей, в какой класс она была введена (что полезно для регистраторов)? - person StriplingWarrior; 25.08.2011
comment
Хорошо, я немного повозился, и вы на 100% правы, нет способа узнать контекст, в котором происходит текущее разрешение, так что да, это обратная сторона. Хорошая точка зрения. Тем не менее, я бы сказал, что добавление атрибута Inject также уродливо, поскольку это также связывает вашу службу с реализацией конкретного контейнера, в отличие от моего подхода DependencyResolver. Я ненадолго оставлю этот вопрос открытым, мне просто интересно услышать больше мнений. Спасибо! - person BFree; 25.08.2011
comment
@BFree: Я согласен с тем, что внедрение свойств далеко не идеально. Не стесняйтесь оставлять вопрос открытым столько, сколько хотите. Мне интересно услышать и другие решения. Что касается атрибута Injected, вы можете указать свой собственный атрибут через свойство NinjectSettings.InjectAttribute. Это упрощает сохранение ваших ссылок Ninject, содержащихся в вашем установочном коде DI. - person StriplingWarrior; 25.08.2011
comment
Я ненавижу публиковать комментарии по старым вопросам, но что касается вопроса «Что, если вы хотите предоставить особый аргумент для реализации (например, регистраторы)», по моему опыту, вы все еще можете использовать шаблон локатора сервисов таким образом. Просто попросите сервисный локатор вернуть фабрику регистратора. Затем вы можете вызвать LoggerFactory.Create (someParameter), чтобы ввести нужные вам параметры. Я считаю, что это обычно называют абстрактным фабричным шаблоном. - person Chris; 04.01.2012
comment
@Chris: Спасибо за комментарий. Идея с вопросом о специальном аргументе заключалась в том, что ваш специальный аргумент может быть автоматически предоставлен через разрешение зависимостей. Примером в вопросе был регистратор, который часто принимает в качестве параметра класс, который его использует. Как вы говорите, вы можете заставить каждый класс создать свой собственный регистратор, вызвав ServiceLocator.Get<LoggerFactory>().Create(typeof(Foo)), но это открывает перед вами высокую вероятность ошибок копирования и вставки. Если вы не измените параметр типа, в журнале может быть указано, что сообщение об ошибке создается Foo, когда оно фактически создается Bar. - person StriplingWarrior; 05.01.2012
comment
@Strip: Я думаю, что случай логгера особенный в этом отношении, по крайней мере, в зависимости от того, как вы это делаете. Для меня каждый класс, которому требуется ведение журнала, получает ILoggerFactory, введенный через DI, а затем вызывает LoggerFactory.Create (this); в его конструкторе. Поскольку утверждение по своей природе ссылается на себя, нет никаких ошибок копирования / вставки, о которых можно было бы говорить. - person Chris; 06.01.2012
comment
Фильтры действий разделяются между запросами в MVC 3. Это крайне небезопасно для потоков. - person usr; 02.10.2014
comment
@usr: Я ценю ваш комментарий, но я не думаю, что ответ заслужил вашего отрицательного голоса. Этот код по своей сути небезопасен для потоков. Он не является потокобезопасным только в том случае, если MyService.DoSomething() не является потокобезопасным. Он не вносит никаких новых проблем с безопасностью потоков, которых еще не было в коде OP. И он не менее потокобезопасен, чем ответ Марка Симана. Однако я обновил ответ, чтобы указать, что фильтры действий используются в разных запросах, поэтому вам нужно следить за зависимыми зависимостями. - person StriplingWarrior; 03.10.2014
comment
Хорошо, я снял голос против. Это было неуместно. Со временем я удалю эти комментарии. Изменение MVC3 для создания одиночных фильтров, на мой взгляд, не имеет положительного значения и очень опасно. Мое намерение состояло в том, чтобы избавить других от неприятностей, когда они узнают об этом в процессе производства. - person usr; 03.10.2014
comment
Остерегайтесь, используя атрибут [Injected], вы берете зависимость от своего контейнера в своем фильтре действий. Я не знаком со многими контейнерами DI, но LightInject при настройке для Интернета API, будет правильно внедрять службу в фильтры действий без использования какого-либо атрибута [Injected]. - person 0xced; 09.02.2018

Да, есть и обратные стороны, поскольку есть множество проблем с 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()));

Более подробный пример этого метода, хотя он и применяется к веб-API ASP.NET вместо MVC, см. В этой статье: http://blog.ploeh.dk/2014/06/13/passive-attributes

person Mark Seemann    schedule 25.08.2011
comment
Спасибо за ответ, но без обид, все ваши ссылки просто указывают на философские рассуждения о наименовании паттернов. Прочитав все ваши ответы на другие вопросы, которые вы связали, и сообщения в блоге, я все еще не вижу никаких конкретных проблем, которые могли бы возникнуть при использовании моего подхода. Добавьте к этому, что теперь любой ActionFilter, который я использую в своем приложении, мне нужно помнить о добавлении к глобальным фильтрам, и я бы сказал, что этот подход может привести к собственным ошибкам. Если вы можете привести конкретный пример того, как мой подход может приводить к ошибкам, то я признаю, что веду себя тупоголовым :) - person BFree; 25.08.2011
comment
Вы спросили, каковы будут недостатки: недостатки - это снижение ремонтопригодности вашей кодовой базы, но это вряд ли кажется конкретным. Это то, что подкрадывается к вам. Я не могу сказать, что если вы сделаете то, что предлагаете, у вас будет состояние гонки, или процессор может перегреться, или котята умрут. Этого не произойдет, но если вы не будете следовать правильным шаблонам проектирования и избегать анти-шаблонов, ваш код будет гнить, и через четыре года вы захотите переписать приложение с нуля (но ваши заинтересованные стороны не позволят вам ). - person Mark Seemann; 25.08.2011
comment
Справедливо. Я соглашусь с вами, что если я создаю универсальный ActionFilter, который можно использовать в разных проектах, то да, это может быть анти-шаблон, поскольку может быть вызван конструктор по умолчанию, и если DependencyResolver не используется, вы получите несколько странных ошибок. Однако 1) ваш подход по-прежнему требует регистрации в GlobalFilters, что может привести к той же проблеме, о которой я говорил выше, и 2) Что, если этот фильтр действий не используется во всех приложениях? Что, если этот фильтр действий внутренний? Все еще так плохо? - person BFree; 25.08.2011
comment
В конечном итоге код, основанный на статическом состоянии, сложнее поддерживать. Это глобальные данные, и мы уже около 40 лет знаем, что это плохо. Нет никакого способа обойти это ... - person Mark Seemann; 26.08.2011
comment
Еще один вопрос (я не вызываю, мне искренне любопытно). Как бы вы реализовали те методы, которые вы закомментировали? Разве вы не дублируете логику, которая уже находится в базовом классе ActionFilterAttribute? Похоже, это ужасно много работы, чтобы избежать использования конструктора по умолчанию для класса, потребляющего службу ... - person BFree; 26.08.2011
comment
+1 для демонстрации того, как отделить код фильтра действий от кода атрибута. Я бы предпочел этот метод исключительно ради разделения проблем. Я действительно ценю разочарование ОП из-за неясности того, что не так с этой частью вопроса. Легко назвать что-то анти-шаблоном, но когда его конкретный код обращается к большинству аргументов против анти-шаблона (возможность модульного тестирования, привязка через конфигурацию и т. Д.), Было бы неплохо узнать почему этот шаблон заставляет код гнить быстрее, чем более чистый код. Не то чтобы я с вами не согласен. Мне понравилась твоя книга, кстати. - person StriplingWarrior; 26.08.2011
comment
@BFree: Кстати, Ремо Глор проделал фантастические вещи с расширением MVC3 для Ninject. github.com/ninject/ninject.web.mvc/ wiki / описывает, как вы можете использовать привязки Ninject для определения фильтра действий, который применяется к контроллерам или действиям с определенным атрибутом на них, вместо того, чтобы регистрировать фильтры глобально. Это передает еще больший контроль вашим привязкам Ninject, в этом весь смысл IoC. - person StriplingWarrior; 26.08.2011
comment
Как реализовать методы - набросок: ActionDescriptor, который является частью filterContext, реализует ICustomAttributeProvider, поэтому вы можете извлечь атрибут маркера оттуда. - person Mark Seemann; 26.08.2011
comment
@Mark: через четыре года вы захотите переписать приложение с нуля (но ваши заинтересованные стороны не позволят вам) - или они позволят ему и продукту умирает, потому что TTM слишком длинный. - person Johann Gerell; 26.08.2011
comment
Как MVC узнает, что нужно применять фильтр, когда вы украшаете действия атрибутом? Он просто использует соглашения об именах? - person ajbeaven; 16.01.2014
comment
@ajbeaven: Нет. MVC применяет фильтр глобально. Обратите внимание на комментарии кода, найдите атрибут маркера в filterContext или используйте какое-либо другое правило, чтобы определить, применять ли это поведение или нет. - person StriplingWarrior; 03.10.2014
comment
Это, безусловно, мой любимый подход. Я люблю блоги Марка - они всегда на высоте. Единственным недостатком этого подхода является то, что фильтр оценивается при каждом действии, независимо от того, будет ли он применен. Если бы нужно было зарегистрировать 100 таких глобальных атрибутов действия, где фактически применяется только один, будет ли это заметное снижение производительности? Это много вызовов рефлексии, проверяющих наличие атрибута, не так ли? В целом, я думаю, что ремонтопригодность и отсутствие необходимости использовать IoC для достижения DI перевешивают любой недостаток, но было бы интересно увидеть это на крайнем примере. - person crush; 06.03.2015
comment
@crush Как, по вашему мнению, платформа ASP.NET находит и вызывает атрибуты, которые определяет it? ;) - person Mark Seemann; 06.03.2015
comment
@MarkSeemann Я еще не изучал это, но я подумал, что они могут выполнять какие-то частные вызовы preAction и postAction после действия. Судя по вашему ответу, я предполагаю, что они просто регистрируют их как глобальные фильтры во время выполнения? Пора копать! - person crush; 06.03.2015
comment
Если вам не нравится глобальное добавление фильтров, выполнение IOC и, следовательно, использование DefaultControllerFactory.CreateController, вы можете сделать следующее (простите за однострочник, которого я не предлагаю): ((Controller)base.CreateController(requestContext, controllerName)).ActionInvoker = _customActionInvoker где _customActionInvoker реализует ControllerActionInvoker, и вы можете переопределить и контролировать GetFilters . Если вас интересует пример кода, я отправлю ответ Марку. - person Suamere; 09.03.2015

Решение, предложенное Марком Зееманном, кажется элегантным. Однако довольно сложно для простой задачи. Использование фреймворка путем реализации AuthorizeAttribute кажется более естественным.

Мое решение заключалось в создании AuthorizeAttribute со статической фабрикой делегатов для службы, зарегистрированной в global.asax. Он работает с любым контейнером DI и кажется немного лучше, чем Service Locator.

В 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);
    }
}
person Jakob    schedule 25.03.2015
comment
Мне нравится этот код, потому что вы не связываете локатор сервисов с MyAuthorizeAttribute. - person Akira Yamamoto; 17.05.2016