Saltar al contenido

Artículos, tutoriales, trucos, curiosidades, reflexiones y links sobre programación web ASP.NET Core, MVC, Blazor, SignalR, Entity Framework, C#, Azure, Javascript... y lo que venga ;)

17 años online

el blog de José M. Aguilar

Inicio El autor Contactar

Artículos, tutoriales, trucos, curiosidades, reflexiones y links sobre programación web
ASP.NET Core, MVC, Blazor, SignalR, Entity Framework, C#, Azure, Javascript...

¡Microsoft MVP!
martes, 16 de julio de 2013
Microsoft patterns & practicesSin duda, las convenciones están de moda. Cualquier framework actual que se precie trae su propio conjunto de convenciones, que no son sino una serie de reglas predefinidas, normalmente de carácter opcional, cuyo cumplimiento evitará que tengamos que tomar decisiones, evitará que cometamos errores, y, normalmente, aumentarán nuestra productividad.

Pues bien, comentaba hace poco que una de las sorpresas que acompañaba a la nueva versión de Unity, el Contenedor de Inversión de Control creado por el equipo de Patterns & Practices de Microsoft, es precisamente la posibilidad de usar convenciones a la hora de registrar asociaciones entre interfaces y clases de nuestro sistema, lo cual nos vendrá de fábula en muchas ocasiones.

Imaginad, por ejemplo, el siguiente código para Unity, muy habitual en sistemas que usan cualquier tipo de contenedor IoC para aplicar inyección de dependencias:
container.RegisterType<IProductServices, ProductServices>(
    new PerRequestLifetimeManager());
container.RegisterType<IInvoiceServices, InvoiceServices>(
    new PerRequestLifetimeManager());
container.RegisterType<ICustomerServices, CustomerServices>
    (new PerRequestLifetimeManager());

container.RegisterType<IProductRepository, ProductRepository>(
    new PerRequestLifetimeManager());
container.RegisterType<IInvoiceRepository, InvoiceRepository>(
    new PerRequestLifetimeManager());
container.RegisterType<ICustomerRepository, CustomerRepository>(
    new PerRequestLifetimeManager());

container.RegisterType<IUnitOfWork, UnitOfWork>(
    new PerRequestLifetimeManager());
container.RegisterType<ILogger, Logger>(
    new PerRequestLifetimeManager());
container.RegisterType<IFileStorage, FileStorage>(
    new PerRequestLifetimeManager());

// etc...
Como habréis observado, en este caso el registro de interfaces y clases tiene un patrón común: simplemente mapeamos los interfaces con nombre ISomething a su implementación concreta en la clase Something, una y otra vez, para todas las dependencias de nuestro sistema. Esto, además de ser bastante tedioso y repetitivo, es muy propenso a fallos, pues es normal olvidarse de registrar nuevas clases e interfaces conforme se van incluyendo en el proyecto.

Pues bien, esto es un buen ejemplo de cómo las convenciones pueden ayudarnos bastante. Fijaos que el código anterior refleja intrínsecamente que existe una convención de nombrado de componentes en nuestro proyecto: todas las interfaces “ILoquesea” serán mapeadas a las clases “Loquesea” que lo implementen.

Usando Unity 3, todo ese código podría quedar de la siguiente forma:
container.RegisterTypes(
       AllClasses.FromAssemblies(typeof(MvcApplication).Assembly),
       WithMappings.FromMatchingInterface,
       WithName.Default, 
       WithLifetime.Custom<PerRequestLifetimeManager>
);
Y lo mejor es que no necesitaremos venir al registro a introducir las nuevas interfaces y clases que vayamos añadiendo a nuestro proyecto: si siguen la convención de nombrado serán registradas automáticamente.

Básicamente, el método RegisterTypes() acepta los siguientes parámetros:
  1. una colección de los tipos de datos a mapear, en forma de IEnumerable<Type>. En el ejemplo anterior, usamos la clase AllClasses, provista también por Unity, para obtener los tipos cargados en el ensamblado de la aplicación mediante su método FromAssemblies(), pero,  por supuesto, podríamos incluir aquí otros ensamblados separándolos por comas, o incluso indicar que queremos escanear todos los ensamblados cargados usando el método FromLoadedAssemblies().
     
  2. una función lambda o delegado que recibirá cada uno de los tipos anteriores y retornará los  interfaces a asociar a dicho tipo en el registro de Unity. La clase estática WithMappings proporciona varios métodos que retornan este delegado “precocinado”:
    • FromMatchingInterface, el usado en el ejemplo anterior, que retorna todos los interfaces implementados por el tipo suministrado, y cuyo nombre corresponde al patrón “I”+nombre de la clase.      
    • FromAllInterfaces, que retornará todos los interfaces que implementen el tipo suministrado.     
    • FromAllInterfacesInSameAssembly, que, como su nombre indica, es idéntico al anterior, pero referido exclusivamente a los interfaces definidos en el mismo ensamblado que la clase que lo implementa.
       
  3. una lambda que permite especificar el nombre con el que será almacenada la asociación entre interfaz y clase en el registro de Unity. En el ejemplo podemos ver que la clase WithName ofrece algunos métodos válidos, como Default (que establece a null el nombre), o TypeName, que lo obtendrá del nombre del tipo del componente. En la mayoría de los casos, pasar null o WithName.Default bastará, pues este nombre se usa exclusivamente cuando se desea resolver un tipo haciendo referencia expresa a su asociación en el registro, lo cual no es muy frecuente.
     
  4. otra lambda, que será invocada para cada uno de los tipos de datos pasados en el primer parámetro y que retornará el LifetimeManager apropiado para cada uno. La clase estática WithLifeTime ofrece varios métodos de utilidad ya preparados, como Hierarchical, PerThread, Transient, o Custom<T>, siendo T el LifetimeManagerque nos interese. En este caso, observad que otorgábamos a todos los objetos creados desde el contenedor una vida “PerRequest”, es decir, que serán liberados cuando termine el proceso de la petición, que es lo habitual en desarrollos para la web.
Este diseño, muy al estilo funcional y apoyándose en expresiones lambda, hacen que resulte realmente sencillo adaptar Unity a las convenciones de nuestro equipo de desarrollo. Por ejemplo, si queremos introducir en el registro sólo las clases definidas en el espacio de nombres MyProject.Application de cualquier ensamblado, podríamos hacerlo así, retocando el primer parámetro:
container.RegisterTypes(
    AllClasses.FromLoadedAssemblies()
              .Where(a=>a.Namespace == "MyProject.Application"),
    WithMappings.FromMatchingInterface,
    WithName.Default,
    WithLifetime.Custom<PerRequestLifetimeManager>
);
O, por ejemplo, si quisiéramos usar un LifetimeManager per request sólo para las clases pertenecientes a dicho espacio de nombres:
container.RegisterTypes(
    AllClasses.FromAssemblies(typeof (MvcApplication).Assembly),
    WithMappings.FromMatchingInterface,
    WithName.Default,
    type => type.Namespace == "MyProject.Application" ? 
        (LifetimeManager)new PerRequestLifetimeManager() : (LifetimeManager)null
);
O, algo simplificado, si quisiéramos cambiar la convención de nombrado de interfaces de IComponent a ComponentInterface, podríamos reflejarlo así en el momento realizar el registro:
container.RegisterTypes(
    AllClasses.FromAssemblies(typeof(MvcApplication).Assembly),
    type => type.GetInterfaces().Where(i => i.Name == type.Name + "Interface"),
    WithName.Default,
    WithLifetime.Custom<PerRequestLifetimeManager>
);
En definitiva, el uso de convenciones en el registro de componentes de nuestro contenedor IoC es un avance que ya teníamos disponible en otros paquetes, pero que Unity acaba de incorporar en su versión 3.0. Sin duda, una gran ayuda para reducir la base de código, evitar errores y ser más productivos.

Publicado en: Variable not found.

8 Comentarios:

Sergio León dijo...

Hola Jose,

Genial el post, la verdad es que esto ahorrará mucho código y parece muy flexible.

Te animo a que sigas escribiendo cosas de Unity, estoy con él y me viene de perlas ;-)

Un saludo.

Amoedo dijo...

Buenas.

Estoy desarrollando una aplicación para Windows 8, y estoy usando Unity,
¿puedo usar las convenciones?

La verdad es que me quitaría mucho trabajo.

Un saludo, y gracias.

josé M. Aguilar dijo...

Hola!

Muchas gracias a ambos por comentar :)

@Sergio, algo más tengo pendiente, no acaba aquí la cosa con Unity ;)

@Amoedo, según leo, funcionando con apps Windows Store, aunque con algunas limitaciones (http://blogs.msdn.com/b/agile/archive/2013/03/12/unity-configuration-registration-by-convention.aspx).

Saludos.

Sergio León dijo...

Hola Jose,

Hoy he vuelto a tu post (una vez más) porque he cambiado de AutoFac a Unity en el proyecto MVC 4 que estoy desarrollando (a priori, me entero más con Unity y parece está más arraigado en el mundo .net).

Todo ha ido bien, pero me ha pasado una cosa "extraña" que no sé razonar.

Me ha dado por depurar la colección Registrations para ver que estaba todo OK, que sólo se registraba lo que yo quería, pero... sorpresa... estas 2 llamadas no registran lo mismo:

container.RegisterTypes(types, WithMappings.FromMatchingInterface);

container.RegisterTypes(types, WithMappings.FromMatchingInterface, WithName.Default, WithLifetime.Transient);

La primera hace su trabajo, busca interfaces con el mismo nombre que clases concretas y las registra con el lifetime Transient (el que utiliza Unity por defecto).

Sin embargo la segunda registra tanto la coincidencia con la interfaz como la misma clase ¿eihnn? :) Es decir, si tengo OrdersService e IOrderService hace 2 registros. El primero desde IOrdersService a OrdersService y el segundo desde OrdersService a OrdersService... y digo yo ¿Para qué y por qué quiero este segundo registro?

De hecho, este "extraño" comportamiento lo hace con WithLifetime.Transient, también con WithLifetime.None... no sé, no me entero. La única solución que he encontrado es pasar de utilizar la sobrecarga de RegisterTypes que no utiliza el parámetro getLifeTimeManager (pero claro, entonces las convenciones pierden algo de fuelle).

Lógicamente, aunque Unity registre x2 (porque él lo vale), la aplicación sigue funcionando sin problemas porque esos "registros fantasmas" no se utilizan, pero me gustaría registrar "sólo" lo que realmente quiero inyectar.

He googleado un poco y no he visto nada, si es una "duda muy peluda" o el post no el sitio, me lo dices e intento resolverla por otro lado :)

Gracias!

josé M. Aguilar dijo...

Hola,

bueno, creo que puede tener su sentido que cada clase esté registrada de forma independiente.

Hay veces que no hay interfaz para una clase pero quieres utilizar Unity como Service Locator, o para generar singletons, o para gestionar la vida del objeto.

Entiendo que ese doble registro es el que permite que puedas pedir a Unity una instancia de la clase "X" directamente, que él resuelva sus dependencias (quizás "X" requiera instancias de "Y" y de "Z" en su constructor), y, por ejemplo en el mundo web, que cuando acabe la petición se lo cepille de forma automática :)

Saludos.

Saludos!

Sergio León dijo...

Gracias Jose por tu respuesta.

Yo la verdad es que sigo mosca con el tema sobre todo porque estoy usando FromMatchingInterface. Es decir, según convención le estoy diciendo que sólo quiero coincidencias por Servicio e IServicio.

Entiendo lo de registrar clases concretas, sobre todo después de leerme el tocho de Mark Sheemman :-) pero que si le pasas unos parámetros haga una cosa y sino se los pasas haga otra... pues me ha dejado frío.

Mil gracias por contestar

Un saludo!

Alberto Baigorria dijo...

Hola Jose,

Saludos desde Santa Cruz, Bolivia. Agradecerte por el tiempo que le dedicas, desde aquí siempre te sigo.

Tengo una duda al respecto, si quisiera inyectar las interfaces haciendo uso de las convenciones, pero tengo algunas clases en la que tengo que inyectar el constructor como seria?

Saludos.

José María Aguilar dijo...

Hola, Luis!

Muchas gracias por tus comentarios :)

> tengo algunas clases en las que tengo que inyectar el constructor

No he entendido exactamente a qué te refieres... las convenciones las usas para simplificar el registro de servicios abstractos con servicios específicos (interfaces con implementaciones), pues evita tener que hacerlos uno a uno.

Pero si en algún caso necesitas hacer un registro más específico, por ejemplo usando un constructor concreto o inyectando parámetros, puedes hacerlo como siempre, usando clases como InjectionConstructor o InjectionMethod (https://msdn.microsoft.com/en-us/library/ff660882(v=pandp.20).aspx)

Saludos!