Injizieren von Abhängigkeiten in ASP.NET MVC 3-Aktionsfilter. Was ist falsch an diesem Ansatz?

78

Hier ist das Setup. Angenommen, ich habe einen Aktionsfilter, der eine Instanz eines Dienstes benötigt:

public interface IMyService
{
   void DoSomething();
}

public class MyService : IMyService
{
   public void DoSomething(){}
}

Ich habe dann einen ActionFilter, der eine Instanz dieses Dienstes benötigt:

public class MyActionFilter : ActionFilterAttribute
{
   private IMyService _myService; // <--- How do we get this injected

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

In MVC 1/2 war das Injizieren von Abhängigkeiten in Aktionsfilter ein bisschen nervig. Der gebräuchlichste Ansatz war die Verwendung eines benutzerdefinierten Aktionsaufrufers, wie hier zu sehen ist: http://www.jeremyskinner.co.uk/2008/11/08/dependency-injection-with-aspnet-mvc-action-filters/ The Hauptmotivation für diese Problemumgehung war, dass dieser folgende Ansatz als schlampige und enge Kopplung mit dem Container angesehen wurde:

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

Hier verwenden wir die Konstruktorinjektion und überladen den Konstruktor, um den Container zu verwenden und den Service zu injizieren. Ich bin damit einverstanden, dass der Container eng mit dem ActionFilter gekoppelt ist.

Meine Frage lautet jedoch: Sind in ASP.NET MVC 3, wo wir eine Abstraktion des verwendeten Containers haben (über den DependencyResolver), all diese Rahmen noch erforderlich? Gestatten Sie mir zu demonstrieren:

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

Jetzt weiß ich, dass einige Puristen sich darüber lustig machen könnten, aber im Ernst, was wäre der Nachteil? Es ist weiterhin testbar, da Sie den Konstruktor verwenden können, der zur Testzeit einen IMyService verwendet und auf diese Weise einen Mock-Service einfügt. Sie sind nicht an eine Implementierung eines DI-Containers gebunden, da Sie den DependencyResolver verwenden. Gibt es also Nachteile bei diesem Ansatz?

Im Übrigen ist hier ein weiterer guter Ansatz, um dies in MVC3 mithilfe der neuen IFilterProvider-Schnittstelle zu tun: http://www.thecodinghumanist.com/blog/archives/2011/1/27/structuremap-action-filters-and-dependency-injection-in -asp-net-mvc-3

BKostenlos
quelle
Danke, dass du auf meinen Beitrag verlinkt hast :). Ich denke das wäre in Ordnung. Trotz meiner Blog-Beiträge von Anfang dieses Jahres bin ich kein großer Fan der DI, die in MVC 3 enthalten sind, und habe sie in letzter Zeit nicht verwendet. Es scheint zu funktionieren, fühlt sich aber manchmal etwas unangenehm an.
Mallioch
Wenn Sie Ninject verwenden, könnte dies ein möglicher Ansatz sein: stackoverflow.com/questions/6193414/…
Robin van der Knaap
+1, obwohl der Service Locator von vielen als Anti-Pattern angesehen wird, bevorzuge ich Ihren Ansatz wegen seiner Einfachheit und der Tatsache, dass die Abhängigkeit an einem Ort, dem IOC-Container, aufgelöst wird, gegenüber Marks, während Sie dies in Marks Beispiel tun würden müssen an zwei Stellen aufgelöst werden, im Bootstrapper und bei der Registrierung der globalen Filter, was sich falsch anfühlt.
Magritte
Sie können den "DependencyResolver.Current.GetService (Type) jederzeit verwenden, wenn Sie möchten.
Mert Susur

Antworten:

31

Ich bin nicht positiv, aber ich glaube, Sie können einfach einen leeren Konstruktor (für den Attributteil ) verwenden und dann einen Konstruktor haben, der den Wert tatsächlich einfügt (für den Filterteil ). *

Bearbeiten : Nach einigem Nachlesen scheint es, dass der akzeptierte Weg, dies zu tun, die Eigenschaftsinjektion ist:

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

In Bezug auf die Frage, warum Sie nicht eine Service Locator- Frage verwenden sollten: Sie verringert meist nur die Flexibilität Ihrer Abhängigkeitsinjektion. Was wäre zum Beispiel, wenn Sie einen Protokollierungsdienst injizieren und dem Protokollierungsdienst automatisch den Namen der Klasse geben möchten, in die er injiziert wird? Wenn Sie Konstruktorinjektion verwenden, würde das großartig funktionieren. Wenn Sie einen Dependency Resolver / Service Locator verwenden, haben Sie kein Glück.

Aktualisieren

Da dies als Antwort akzeptiert wurde, möchte ich auf die Aufzeichnung eingehen und sagen, dass ich Mark Seemans Ansatz bevorzuge, da er die Verantwortung des Aktionsfilters vom Attribut trennt. Darüber hinaus bietet die MVC3-Erweiterung von Ninject einige sehr leistungsstarke Möglichkeiten, Aktionsfilter über Bindungen zu konfigurieren. Weitere Informationen finden Sie in den folgenden Referenzen:

Update 2

Wie @usr in den Kommentaren unten ausgeführt hat, werden ActionFilterAttributes beim Laden der Klasse instanziiert und dauern die gesamte Lebensdauer der Anwendung. Wenn die IMyServiceSchnittstelle kein Singleton sein soll, handelt es sich letztendlich um eine Captive-Abhängigkeit . Wenn die Implementierung nicht threadsicher ist, kann dies zu erheblichen Schmerzen führen.

Wenn Sie eine Abhängigkeit mit einer kürzeren Lebensdauer als der erwarteten Lebensdauer Ihrer Klasse haben, ist es ratsam, eine Fabrik zu injizieren, um diese Abhängigkeit bei Bedarf zu erzeugen, anstatt sie direkt zu injizieren.

StriplingWarrior
quelle
Zu Ihrem Kommentar zum Mangel an Flexibilität: Hinter dem DependencyResolver verbirgt sich ein tatsächlicher IOC-Container, der ihn antreibt, sodass Sie beim Erstellen eines Objekts jede gewünschte benutzerdefinierte Logik hinzufügen können. Ich bin mir nicht sicher, ob ich deinem Standpunkt folge .....
BFree
@BFree: Beim Aufruf DependencyResolver.GetServicehat die Bindungsmethode keine Ahnung, in welche Klasse diese Abhängigkeit eingefügt wird. Was wäre, wenn Sie IMyServicefür bestimmte Arten von Aktionsfiltern einen anderen erstellen möchten? Oder, wie ich in meiner Antwort sagte, was wäre, wenn Sie der MyServiceImplementierung ein spezielles Argument liefern möchten, um ihr mitzuteilen, in welche Klasse sie injiziert wurde (was für Logger nützlich ist)?
StriplingWarrior
OK, ich habe ein bisschen rumgespielt, und Sie haben zu 100% Recht. Es gibt keine Möglichkeit, den "Kontext" zu ermitteln, in dem die aktuelle Auflösung stattfindet. Ja, das ist ein Nachteil. Guter Punkt. Ich würde jedoch argumentieren, dass das Hinzufügen eines Inject-Attributs ebenfalls hässlich ist, da dies Ihren Service auch an eine Implementierung eines bestimmten Containers bindet, wo dies bei meinem DependencyResolver-Ansatz nicht der Fall ist. Ich werde diese Frage ein wenig offen lassen, ich bin nur neugierig, mehr Meinungen zu hören. Vielen Dank!
BFree
3
Aktionsfilter werden in MVC 3 für alle Anforderungen gemeinsam genutzt. Dies ist äußerst threadsicher.
usr
3
OK, ich habe das Downvote entfernt. Es war nicht angemessen. Ich werde diese Kommentare irgendwann löschen. Die MVC3-Änderung, um Filter zu Singletons zu machen, ist meiner Meinung nach ohne positiven Wert und sehr gefährlich. Meine Absicht war es, anderen Ärger zu ersparen, wenn sie in der Produktion davon erfahren.
usr
92

Ja, es gibt Nachteile, da es viele Probleme mit IDependencyResolver selbst gibt, und zu diesen können Sie die Verwendung eines Singleton Service Locator sowie Bastard Injection hinzufügen .

Eine bessere Option besteht darin, den Filter als normale Klasse zu implementieren, in die Sie die gewünschten Dienste einfügen können:

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

Beachten Sie, wie der Filter den filterContext untersucht, um festzustellen, ob das Verhalten angewendet werden soll oder nicht.

Dies bedeutet, dass Sie weiterhin Attribute verwenden können, um zu steuern, ob der Filter angewendet werden soll oder nicht:

public class MyActionFilterAttribute : Attribute { }

Jetzt ist dieses Attribut jedoch vollständig inert.

Der Filter kann mit der erforderlichen Abhängigkeit erstellt und zu den globalen Filtern in global.asax hinzugefügt werden:

GlobalFilters.Filters.Add(new MyActionFilter(new MyService()));

Ein detaillierteres Beispiel für diese Technik finden Sie in diesem Artikel, obwohl sie auf die ASP.NET-Web-API anstelle von MVC angewendet wird: http://blog.ploeh.dk/2014/06/13/passive-attributes

Mark Seemann
quelle
22
Sie haben nach den Nachteilen gefragt: Die Nachteile sind eine verminderte Wartbarkeit Ihrer Codebasis, aber das fühlt sich kaum konkret an . Das ist etwas, das sich auf dich einschleicht. Ich kann nicht sagen, dass wenn Sie das tun, was Sie vorhaben, Sie eine Rennbedingung haben oder die CPU überhitzt oder Kätzchen sterben. Das wird nicht passieren, aber wenn Sie nicht den richtigen Entwurfsmustern folgen und Anti-Muster vermeiden, wird Ihr Code verrotten und in vier Jahren möchten Sie die Anwendung von Grund auf neu schreiben (aber Ihre Stakeholder lassen Sie nicht zu ).
Mark Seemann
4
+1, um zu zeigen, wie der Aktionsfiltercode vom Attributcode getrennt wird. Ich würde diese Methode nur aus Gründen der Trennung von Bedenken bevorzugen. Ich schätze die Frustration des OP über die Unbestimmtheit des Teils der Frage "Was ist daran falsch?". Es ist leicht, etwas als Anti-Muster zu bezeichnen, aber wenn sein spezifischer Code die meisten Argumente gegen das Anti-Muster (Testbarkeit von Einheiten, Bindung über Konfiguration usw.) anspricht, wäre es schön zu wissen, warum dieses Muster Code verrottet schneller als "reinerer" Code. Nicht, dass ich mit dir nicht einverstanden wäre. Ich habe dein Buch genossen, übrigens.
StriplingWarrior
5
@BFree: Übrigens hat Remo Gloor mit der MVC3-Erweiterung für Ninject einige fantastische Sachen gemacht. github.com/ninject/ninject.web.mvc/wiki/… beschreibt, wie Sie mithilfe von Ninject-Bindungen einen Aktionsfilter definieren können, der auf Controller oder Aktionen mit einem bestimmten Attribut angewendet wird, anstatt die Filter global registrieren zu müssen. Dies überträgt noch mehr Kontrolle auf Ihre Ninject-Bindungen, worauf es bei IoC ankommt.
StriplingWarrior
1
So implementieren Sie die Methoden - Skizze: Der ActionDescriptor, der Teil des filterContext ist, implementiert ICustomAttributeProvider, sodass Sie das Marker-Attribut von dort abrufen können.
Mark Seemann
1
@ Mark: 4 Jahre ab jetzt werden Sie die Anwendung von Grund auf neu zu schreiben wollen (aber Ihre Stakeholder Sie nicht im Stich) - oder sie werden ihn und die Produkt stirbt lassen , weil die TTM zu lang ist.
Johann Gerell
7

Die von Mark Seemann vorgeschlagene Lösung erscheint elegant. Allerdings ziemlich komplex für ein einfaches Problem. Die Verwendung des Frameworks durch Implementierung von AuthorizeAttribute fühlt sich natürlicher an.

Meine Lösung bestand darin, ein AuthorizeAttribute mit einer statischen Delegate-Factory für einen in global.asax registrierten Dienst zu erstellen. Es funktioniert für jeden DI-Container und fühlt sich etwas besser an als ein Service Locator.

In global.asax:

MyAuthorizeAttribute.AuthorizeServiceFactory = () => Container.Resolve<IAuthorizeService>();

Meine benutzerdefinierte Attributklasse:

[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);
    }
}
Jakob
quelle
Ich mag diesen Code, weil Sie den Service Locator nicht mit MyAuthorizeAttribute koppeln.
Akira Yamamoto