Введення залежностей у фільтри дій 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 впорскування залежностей у фільтри дій було болем у дупі. Найбільш поширений підхід полягає у використанні 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


Дякую за посилання на мій пост :). Я думаю, це було б добре. Незважаючи на мої повідомлення в блозі з початку цього року, я насправді не є великим шанувальником DI, який вони включили в MVC 3, і останнім часом не використовую його. Здається, це працює, але іноді відчуває себе незручно.
Mallioch

Якщо ви використовуєте Ninject, це може бути можливим підходом: stackoverflow.com/questions/6193414/…
Робін ван дер Кнаап,

+1, хоча багато хто вважає локатор послуг анти-шаблоном, я вважаю, що я віддаю перевагу вашому підходу над Marks за його простоту, а також той факт, що залежність вирішується в одному місці, контейнері IOC, тоді як у прикладі Марка ви б доводиться вирішувати у двох місцях, у завантажувачі та при реєстрації глобальних фільтрів, що здається неправильним.
magritte

Ви все ще можете використовувати "DependencyResolver.Current.GetService (Type) у будь-який час, коли захочете.
Мерт Сусур

Відповіді:


31

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

Редагувати : Після невеликого читання виявляється, що прийнятим способом цього є введення властивостей:

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

Щодо того, чому не використовувати запитання Service Locator : це здебільшого просто зменшує гнучкість введення вашої залежності. Наприклад, що робити, якщо ви вводите послугу журналювання, і ви хочете автоматично дати службі реєстрації ім'я класу, в який вона вводиться? Якщо ви використовуєте інжектор конструктора, це буде чудово працювати. Якщо ви використовуєте вирішувач залежностей / локатор послуг, вам не пощастить.

Оновлення

Оскільки це було прийнято як відповідь, я хотів би продовжити, щоб сказати, що я віддаю перевагу підходу Марка Сімана, оскільки він відокремлює відповідальність фільтра дій від атрибуту. Крім того, розширення MVC3 від Ninject має кілька дуже потужних способів налаштування фільтрів дій за допомогою прив'язок. Детальніше див. Наступні посилання:

Оновлення 2

Як зазначив @usr у коментарях нижче, ActionFilterAttributeекземпляри s створюються під час завантаження класу, і вони тривають протягом усього життя програми. Якщо IMyServiceінтерфейс не повинен бути одностороннім, то він в кінцевому підсумку є залежною залежністю . Якщо його реалізація не є безпечною для потоків, вам може бути багато болю.

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


Повторіть свій коментар щодо відсутності гнучкості: За DependencyResolver стоїть фактичний контейнер IOC, що керує ним, тому ви можете додати будь-яку власну логіку, яку хочете, прямо там, коли будуєте об’єкт. Не впевнений, що дотримуюся вашої
думки

@BFree: Під час виклику DependencyResolver.GetServiceметод прив'язки не уявляє, до якого класу вводиться ця залежність. Що робити, якщо ви хочете створити різні IMyServiceдля певних типів фільтрів дій? Або, як я вже сказав у своїй відповіді, що, якщо ви хочете надати спеціальний аргумент MyServiceреалізації, щоб сказати йому, до якого класу вона введена (що корисно для реєстраторів)?
StriplingWarrior

Добре, я дещо возився, і ти на 100% правий, немає можливості отримати "контекст", в якому відбувається поточна резолюція, так що так, це є мінусом. Влучне зауваження. Хоча я б стверджував, що додавання атрибута Inject теж некрасиво, оскільки це також пов’язує ваш сервіс із реалізацією певного контейнера, де, як це не робить мій підхід DependencyResolver. Я трохи залишу це питання відкритим, мені просто цікаво почути більше думок. Дякую!
BFree

3
Фільтри дій спільно використовуються між запитами в MVC 3. Це дуже небезпечно для потоків.
usr

3
Добре, я видалив голос проти. Це було недоречно. Зрештою я видалю ці коментарі. На мій погляд, зміна MVC3 на виготовлення фільтрів-одинарних не має позитивного значення і дуже небезпечна. Я мав намір позбавити інших проблем, коли вони дізнаються про це на виробництві.
usr

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

Детальніший приклад цієї техніки, хоча і застосовується до веб-API ASP.NET замість MVC, див. У цій статті: http://blog.ploeh.dk/2014/06/13/passive-attributes


22
Ви запитали про те, якими можуть бути мінуси: мінусами є зменшена ремонтопридатність вашої кодової бази, але це навряд чи здається конкретним . Це щось підкрадається до вас. Я не можу сказати, що якщо ви зробите те, що пропонуєте зробити, у вас буде стан перегонів, або процесор може перегрітися, або кошенята загинуть. Цього не трапиться, але якщо ви не будете слідувати належним шаблонам дизайну і уникати анти-шаблонів, ваш код згниє і через чотири роки ви захочете переписати програму з нуля (але ваші зацікавлені сторони не дозволять вам ).
Mark Seemann

4
+1 для показу, як відокремити код фільтру дій від коду атрибута. Я віддав би перевагу цьому методу виключно для відокремлення проблем. Я ціную розчарування ОП розмитістю щодо невизначеності у тому, "що з цим не так" у частині питання. Легко назвати щось анти-шаблоном, але коли його конкретний код звертається до більшості аргументів проти анти-шаблону (тестування одиниць, прив’язка за допомогою конфігурації тощо), було б непогано знати, чому цей шаблон змушує код гнити швидше, ніж "чистіший" код. Не те, що я з вами не погоджуюсь. Мені сподобалась твоя книга, ДОТВІ.
StriplingWarrior

5
@BFree: До речі, Remo Gloor зробив кілька фантастичних речей із розширенням MVC3 для Ninject. github.com/ninject/ninject.web.mvc/wiki/… описує, як за допомогою прив’язок Ninject можна визначити фільтр дій, який застосовується до контролерів або дій із певним атрибутом, замість того, щоб реєструвати фільтри глобально. Це передає ще більше контролю у ваші прив’язки Ninject, у чому суть IoC.
StriplingWarrior

1
Як реалізувати методи - ескіз: ActionDescriptor, який є частиною filterContext, реалізує ICustomAttributeProvider, тому ви можете витягнути звідти атрибут marker.
Mark Seemann

1
@Mark: через чотири роки ви хочете переписати програму з нуля (але ваші зацікавлені сторони не будуть перешкоджати вам) - або вони будуть нехай і плашки продукту , тому що TTM занадто довго.
Йоганн Герелл,

7

Рішення, яке запропонував Марк Сіманн, здається елегантним. Однак досить складний для простої задачі. Використання фреймворку шляхом реалізації AuthorizeAttribute здається більш природним.

Моїм рішенням було створити AuthorizeAttribute із заводу статичних делегатів до служби, зареєстрованої в global.asax. Він працює для будь-якого контейнера DI і відчуває себе трохи краще, ніж сервер 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);
    }
}

Мені подобається цей код, тому що ви не дуже добре поєднуєте Службу локатора з MyAuthorizeAttribute.
Акіра Ямамото,
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.