Atributos con comportamiento: un mal diseño

Tarde o temprano, todo desarrollador ya se de ASP.NET MVC o WebApi necesita hacer sus propios filtros para validaciones propias de peticiones, logging, comprobación de precondiciones… En fin, lo habitual para lo que se usan los filtros, vamos.

Y tarde o temprano este desarrollador se da cuenta de que su filtro debería acceder a un determinado servicio de su aplicación: quizá necesita hacer una consulta a la bbdd, o a un determinado elemento de negocio, o acceder al sistema de logging o cualquier cosa más. Y este desarrollador, que conoce (y usa) la inyección de dependencias se encuentra con que no es posible inyectar dependencias en un filtro. Algunos desarrolladores buscaran cualquier otra alternativa, algunas mejores que otras, pero ninguna satisfactoria: crear un singleton, una clase estática o instanciar directamente el objeto en lugar de obtenerlo como una dependencia (y rompiendo cualquier abstracción realizada). Otros desarrolladores continuarán en su búsqueda. En esta fase de terquedad que nos caracteriza, pensaran “no, no es posible. Tiene que ver alguna manera”.

Y al final su búsqueda dará sus frutos y se encontrarán con artículos como este de variable not found, donde se cuenta como hacerlo. Hay otros mecanismos para conseguirlo, pero ninguno es del todo elegante. En asp.net mvc core han introducido ServiceFilter, que a pesar de su nombre es una factoría genérica de filtros y que proporciona una solución más limpia. De nuevo José M. Aguilar lo cuenta fenomenal o sea que miraros este otro post de su blog.

Pero pocos se plantearán el fondo de la cuestión, que es muy simple. Los filtros de ASP.NET MVC son atributos y no debería haber motivos para tener inyectar nada a un atributo.

Los atributos son especiales. Los crea el CLR y su objetivo es que contengan metadatos y los metadatos son datos. Los datos son eso datos. No tienen comportamiento. No deben tenerlo. Un filtro de ASP.NET MVC tiene comportamiento. Tomemos por ejemplo el, posiblemente, más famoso de todos ellos: [Authorize].

Podemos decorar una acción con este atributo y automáticamente ASP.NET MVC se encargará de asegurar que la petición está autenticada (p. ej. con la cookie) antes de permitir llamar a la acción. Si no lo está, devolverá un HTTP 401 como respuesta. Su uso es, realmente muy cómodo:

  1. [Authorize]
  2. public ActionResult UserProfile()
  3. {
  4.     // Codigo
  5. }

¡Eso está bien! Es fácil de usar para el desarrollador. El problema viene con la implementación de esto.  Basta con ver el código fuente de AuthorizeAttribute para ver que es la propia clase que define el atributo la que hace todo el trabajo y que se encarga de verificar que la petición está autenticada.

Y eso es un error. El atributo [Authorize] deberían tan solo “marcar” que una acción debe estar protegida contra acceso anónimo (eso son metadatos asociados a la acción, está bien que estén en un atributo). Y el framework debería ofrecer otro interfaz (y una o más posibles implementaciones) para ofrecer el comportamiento.

De este modo separamos los metadatos del comportamiento. Los primeros se mantienen en los atributos y el comportamiento se traslada a clases normales.

Veamos un ejemplo de como podría ser esta supuesta API. Dado que tenemos varios tipos de filtro (autenticación, de acción) que reciben “contextos” distintos podríamos usar una clase base derivada de Attribute para diferenciarlos. Así MVC podría definir:

  1. public class MvcActionAttribute : Attribute { }
  2. public class MvcAuthAttribute : Attribute { }

Así AuthorizeAttribute heredaría de MvcAuthAttribute y solo añadiría los datos necesarios (p. ej. el nombre de los usuarios o los roles). Lo mismo para el resto de atributos: solo contendrían datos (propiedades).

Ahora el siguiente punto es asociar el comportamiento a cada tipo de atributo. Esto se puede hacer de varias maneras. Una podría ser que MVC nos expusiese una interfaz que fuese una factoría para crear esas clases con comportamiento:

  1. public interface IAuthFilterFactory {
  2.  
  3.     IAuthFilterHandler CreateForType(Type attribute);
  4. }

Tendríamos una factoría para cada uno de los tipos de filtros (autenticación, autorización, acción, error) ya que el tipo del objeto que gestiona esos filtros es distinto (reciben contextos distintos). O podría haber una sola factoría con varios métodos.

La siguiente interfaz que MVC nos tiene que proveer es la IAuthFilterHandler (de nuevo habría una interfaz para cada tipo de filtro):

  1. public interface IAuthFilterHandler
  2. {
  3.     void ApplyFilter (MvcAuthAttribute filter, AuthorizationContext context);
  4. }

Ahora, podríamos crear unas versiones genéricas, para que el método “ApplyFilter” no reciba la clase base, si no realmente el atributo con su tipo y evitarnos castings. Primero, la interfaz genérica:

  1. public interface IAuthFilterAttribute<TA> : IAuthFilterHandler
  2.     where TA : MvcAuthAttribute
  3. {
  4.     void ApplyFilter(TA filter, AuthorizationContext context);
  5. }

Y luego una clase que se encargue de hacer el “puente” entre la interfaz genérica y la que no es genérica:

  1. public abstract class AuthFilterHandler<X> : IAuthFilterAttribute<X>
  2.     where X : MvcAuthAttribute
  3. {
  4.     void IAuthFilterHandler.ApplyFilter(MvcAuthAttribute filter, AuthorizationContext context)
  5.     {
  6.         ApplyFilter((X)filter, context);
  7.     }
  8.  
  9.     public abstract void ApplyFilter(X filter, AuthorizationContext context);
  10. }

Toda esa infrastructura estaría proveída por el framework (ASP.NET MVC en nuestro ejemplo). Ahora veamos como la podríamos usar.

Primero nos definiríamos un filtro:

  1. public class MyAuthorizeAttribute : MvcAuthAttribute { }

Luego la clase que contiene el comportamiento (el handler):

  1. public class MyAuthorizeFilterHandler : AuthFilterHandler<MyAuthorizeAttribute>
  2. {
  3.     public override void ApplyFilter(MyAuthorizeAttribute filter, AuthorizationContext context)
  4.     {
  5.         // Hacer lo necesario
  6.     }
  7. }

Observa que heredamos de la AuthFilterHandler genérica que hemos creado antes por lo que tan solo debemos implementar el ApplyFilter que ya recibe el atributo del tipo correcto.

Y finalmente debemos implementar la factoría:

  1. public class AuthFilterFactory : IAuthFilterFactory
  2. {
  3.     public IAuthFilterHandler CreateForType(Type attribute)
  4.     {
  5.         // Resolver el IAuthFilterHandler correspondiente segn el tipo del atributo
  6.         // AQUI PODEMOS USAR DI
  7.     }
  8. }

Y esa es la clave, la factoría es nuestra y puede usar el contenedor de IoC para crear los handlers si quiere y así inyectarles cualquier dependencia. Los atributos contienen solo datos y los handlers el comportamiento asociado.

Y “mágicamente” todas las fricciones con la inyección de dependencias se resuelven.

Cierto, faltaría como informamos al framework que factoría para crear los handlers, debe usar. Bueno, en el caso de MVC podría ser algo como:

  1. System.Web.Mvc.FilterCreator.Current.
  2.     SetAuthFactory(new AuthFilterFactory());

(Siguiendo su estilo de tener singletons para la configuración).

Y eso es todo. La idea de este post es que veas que no es buena idea tener atributos con comportamiento. Si desarrollas un framework y empiezas a crear atributos con comportamiento, yo te recomendaría que pararas y pensaras lo que haces. Para mi es ¡un error de diseño clarísimo!

Saludos!

2 comentarios sobre “Atributos con comportamiento: un mal diseño”

  1. Un planteamiento muy interesante. Estoy totalmente de acuerdo en que conceptualmente un atributo es sólo una marca establecida en los metadatos y en principio es buena idea separar responsabilidades.

    En mi caso en ciertos casos adoptaría tu solución, coincide exactamente con mi forma de trabajar, sin embargo tengo mis dudas de que una inmensa mayoría de desarrolladores consideren que ofrece una buena usabilidad. El hecho de separar el atributo en dos y luego realizar la inyección puede llegar a considerarse «sobre-ingeniería». Al final, por separar datos y comportamiento se exige una mayor complejidad que posiblemente sólo sea interesante para incluir servicios en el atributo.

    En cualquier caso me ha gustado mucho la idea.

    1. Buenas!
      Como siempre debe buscarse el equilibrio 🙂

      De todos modos creo que es una mala práctica que haya comportamiento en los atributos, con independencia de si se debe inyectar o no dependencias en ellos. Separar el comportamiento de los datos permite otros escenarios tales como modificación del comportamiento en run-time, además de que en mi opinión tu framework está más bien diseñado.

      ¿Le complicas la vida a tus desarrolladores? Un poco, pero tampoco tanto y creo que merece la pena 😉

      Gracias por comentar!

Responder a Jeremías Cancelar respuesta

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *