Inyectando dependencias en los filtros de acción de ASP.NET MVC 3. ¿Qué tiene de malo este enfoque?

78

Aquí está la configuración. Digamos que tengo algún filtro de acción que necesita una instancia de un servicio:

public interface IMyService
{
   void DoSomething();
}

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

Luego tengo un ActionFilter que necesita una instancia de ese servicio:

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

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

En MVC 1/2, inyectar dependencias en filtros de acción fue un poco molesto. El método más común era utilizar un invocador acción personalizada como se puede ver aquí: http://www.jeremyskinner.co.uk/2008/11/08/dependency-injection-with-aspnet-mvc-action-filters/ la La principal motivación detrás de esta solución fue porque este enfoque siguiente se consideró descuidado y un acoplamiento estrecho con el contenedor:

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

Aquí estamos usando la inyección del constructor y sobrecargando el constructor para usar el contenedor e inyectar el servicio. Estoy de acuerdo en que acopla estrechamente el contenedor con ActionFilter.

Sin embargo, mi pregunta es la siguiente: ahora en ASP.NET MVC 3, donde tenemos una abstracción del contenedor que se está utilizando (a través del DependencyResolver), ¿siguen siendo necesarios todos estos aros? Permítame demostrar:

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

Ahora sé que algunos puristas podrían burlarse de esto, pero en serio, ¿cuál sería la desventaja? Todavía es comprobable, ya que puede usar el constructor que toma un IMyService en el momento de la prueba e inyectar un servicio simulado de esa manera. No está atado a ninguna implementación de contenedor DI ya que está utilizando DependencyResolver, entonces, ¿hay alguna desventaja en este enfoque?

Por cierto, aquí hay otro buen enfoque para hacer esto en MVC3 usando la nueva interfaz IFilterProvider: http://www.thecodinghumanist.com/blog/archives/2011/1/27/structuremap-action-filters-and-dependency-injection-in -asp-net-mvc-3

BFree
fuente
Gracias por vincular a mi publicación :). Creo que esto estaría bien. A pesar de las publicaciones de mi blog de principios de este año, en realidad no soy un gran admirador del DI que incluyeron en MVC 3 y no lo he estado usando últimamente. Parece funcionar, pero a veces se siente un poco incómodo.
Mallioch
Si está utilizando Ninject, este podría ser un enfoque posible: stackoverflow.com/questions/6193414/…
Robin van der Knaap
+1, aunque muchos consideran que el localizador de servicios es un anti-patrón, creo que prefiero su enfoque sobre Marks por su simplicidad y también por el hecho de que la dependencia se resuelve en un solo lugar, el contenedor IOC, mientras que en el ejemplo de Mark lo haría tiene que resolver en dos lugares, en el bootstrapper y al registrar los filtros globales, lo que se siente mal.
magritte
todavía puede utilizar el "DependencyResolver.Current.GetService (Tipo) en cualquier momento que desee.
Mert Susur

Respuestas:

31

No estoy seguro, pero creo que puede usar un constructor vacío (para la parte del atributo ) y luego tener un constructor que realmente inyecte el valor (para la parte del filtro ). *

Editar : Después de leer un poco, parece que la forma aceptada de hacer esto es mediante la inyección de propiedades:

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

En cuanto a por qué no utilizar una pregunta del Localizador de servicios : en su mayoría, solo reduce la flexibilidad de su inyección de dependencia. Por ejemplo, ¿qué pasaría si estuviera inyectando un servicio de registro y quisiera darle automáticamente al servicio de registro el nombre de la clase en la que se está inyectando? Si usa inyección de constructor, eso funcionaría muy bien. Si está utilizando un localizador de servicios / solucionador de dependencias, no tendrá suerte.

Actualizar

Dado que esto fue aceptado como la respuesta, me gustaría dejar constancia de que prefiero el enfoque de Mark Seeman porque separa la responsabilidad del Filtro de acción del Atributo. Además, la extensión MVC3 de Ninject tiene algunas formas muy poderosas de configurar filtros de acción a través de enlaces. Consulte las siguientes referencias para obtener más detalles:

Actualización 2

Como @usr señaló en los comentarios a continuación, ActionFilterAttributelos correos electrónicos se crean cuando se carga la clase y duran toda la vida útil de la aplicación. Si IMyServicese supone que la interfaz no es un Singleton, entonces termina siendo una Dependencia Cautiva . Si su implementación no es segura para subprocesos, podría sufrir mucho dolor.

Siempre que tenga una dependencia con una vida útil más corta que la vida útil esperada de su clase, es aconsejable inyectar una fábrica para producir esa dependencia a pedido, en lugar de inyectarla directamente.

StriplingGuerrero
fuente
Re su comentario sobre la falta de flexibilidad: Detrás del DependencyResolver hay un contenedor de IOC real que lo maneja, por lo que puede agregar cualquier lógica personalizada que desee allí mismo al construir un objeto. No estoy seguro de seguir su punto .....
BFree
@BFree: al llamar DependencyResolver.GetService, el método de enlace no tiene idea de en qué clase se está inyectando esta dependencia. ¿Y si quisiera crear un IMyServicefiltro diferente para ciertos tipos de filtros de acción? O, como dije en mi respuesta, ¿ MyServicequé pasa si quisiera proporcionar un argumento especial a la implementación para decirle en qué clase se ha inyectado (lo cual es útil para los registradores)?
StriplingWarrior
De acuerdo, hice algunas tonterías, y tienes 100% de razón, no hay forma de obtener el "contexto" en el que está sucediendo la resolución actual, así que sí, eso es una desventaja. Buen punto. Sin embargo, yo diría que agregar un atributo Inject también es feo, ya que eso vincula su servicio a una implementación de un contenedor en particular, mientras que mi enfoque DependencyResolver no lo hace. Dejaré esta pregunta abierta por un momento, solo tengo curiosidad por escuchar más opiniones. ¡Gracias!
BFree
3
Los filtros de acción se comparten entre las solicitudes en MVC 3. Esto es muy inseguro para los subprocesos.
usr
3
Bien, he eliminado el voto negativo. No fue apropiado. Eliminaré estos comentarios eventualmente. El cambio de MVC3 para hacer filtros singletons es, en mi opinión, sin valor positivo y muy peligroso. Mi intención era evitarles algunos problemas a otros cuando se enteraran de eso en la producción.
usr
92

Sí, hay desventajas, ya que hay muchos problemas con IDependencyResolver en sí, y a ellos puede agregar el uso de un localizador de servicios Singleton , así como Bastard Injection .

Una mejor opción es implementar el filtro como una clase normal en la que puede inyectar los servicios que desee:

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

Observe cómo el filtro examina el filterContext para determinar si se debe aplicar el comportamiento o no.

Esto significa que aún puede usar atributos para controlar si el filtro debe aplicarse o no:

public class MyActionFilterAttribute : Attribute { }

Sin embargo, ahora ese atributo es completamente inerte.

El filtro se puede componer con la dependencia requerida y agregar a los filtros globales en global.asax:

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

Para obtener un ejemplo más detallado de esta técnica, aunque se aplica a ASP.NET Web API en lugar de MVC, consulte este artículo: http://blog.ploeh.dk/2014/06/13/passive-attributes

Mark Seemann
fuente
22
Usted preguntó cuáles serían las desventajas: las desventajas son una menor capacidad de mantenimiento de su base de código, pero eso apenas se siente concreto . Esto es algo que te acecha. No puedo decir que si hace lo que se propone hacer, tendrá una condición de carrera, o la CPU se sobrecalentará o los gatitos morirán. Eso no va a suceder, pero si no sigue los patrones de diseño adecuados y evita los anti-patrones, su código se pudrirá y dentro de cuatro años querrá reescribir la aplicación desde cero (pero sus partes interesadas no lo dejarán ).
Mark Seemann
4
+1 para mostrar cómo separar el código de filtro de acción del código de atributo. Preferiría este método únicamente por el bien de la separación de preocupaciones. Sin embargo, aprecio la frustración del OP con la vaguedad en la parte de la pregunta de "qué está mal con esto". Es fácil llamar a algo un antipatrón, pero cuando su código específico aborda la mayoría de los argumentos contra el antipatrón (capacidad de prueba de la unidad, enlace a través de la configuración, etc.), sería bueno saber por qué este patrón hace que el código se pudra más rápido que el código "más puro". No es que no esté de acuerdo contigo. Disfruté tu libro, por cierto.
StriplingWarrior
5
@BFree: Por cierto, Remo Gloor ha hecho cosas fantásticas con la extensión MVC3 para Ninject. github.com/ninject/ninject.web.mvc/wiki/… describe cómo puede usar los enlaces Ninject para definir un filtro de acción que se aplica a controladores o acciones con un atributo específico en ellos, en lugar de tener que registrar los filtros globalmente. Esto transfiere aún más control a sus enlaces Ninject, que es el objetivo de IoC.
StriplingWarrior
1
Cómo implementar los métodos - sketch: el ActionDescriptor que es parte de filterContext implementa ICustomAttributeProvider, por lo que puede extraer el atributo de marcador desde allí.
Mark Seemann
1
@ Marcos: cuatro años a partir de ahora van a querer volver a escribir la aplicación desde cero (pero no sus grupos de interés que le permitirán) - o que van a él y dejar que las matrices de productos debido a que el TTM es demasiado largo.
Johann Gerell
7

La solución que sugirió Mark Seemann parece elegante. Sin embargo bastante complejo para un problema simple. Usar el marco implementando AuthorizeAttribute se siente más natural.

Mi solución fue crear un AuthorizeAttribute con una fábrica de delegados estáticos a un servicio registrado en global.asax. Funciona para cualquier contenedor DI y se siente un poco mejor que un localizador de servicios.

En global.asax:

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

Mi clase de atributo personalizado:

[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
fuente
Me gusta este código porque no emparejas el localizador de servicios con MyAuthorizeAttribute.
Akira Yamamoto