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, 15 de junio de 2021
.NET Core

No hace demasiado tiempo he descubierto en el framework la clase ActivatorUtilities, una pequeña joyita disponible en Microsoft.Extensions.DependencyInjection, que puede resultar interesante para implementar fácilmente factorías o cualquier tipo de código donde tengamos que instanciar manualmente clases con dependencias hacia servicios registrados en el contenedor de nuestra aplicación.

La clase estática ActivatorUtilities ofrece básicamente tres métodos públicos:

  • CreateFactory(), para crear factorías de objetos.
  • CreateInstance() / CreateInstance<T>(), para crear instancias de objetos.
  • GetServiceOrCreateInstance() / GetServiceOrCreateInstance<T>(), para obtener una instancia desde el inyector, o bien crearla manualmente con CreateInstance().
Veámoslos en mayor detalle.

CreateFactory()

Este método retorna un delegado que permitirá crear instancias del tipo que le indiquemos. Para ello, es necesario suministrarle el tipo que queremos crear, así como un array de tipos de los parámetros de su constructor.

Por ejemplo, imaginemos la siguiente clase:

public class MyClass
{
    public MyClass(int i, string s)
    {
        // TODO
    }
}

El siguiente código crea una factoría reutilizable para la clase MyClass, cuyo constructor recibe un parámetro de tipo entero y otro de tipo texto:

// First, create the factory:
ObjectFactory myFactory = ActivatorUtilities.CreateFactory(
    typeof(MyClass),
    new[] { typeof(int), typeof(string) }
);

// Then, use it to create objects:
var myObject1 = (MyClass)myFactory(
    serviceProvider                 // IServiceProvider instance
    new object[] { 1, "Hello" }     // Constructor arguments
);
var myObject2 = (MyClass)myFactory(
    serviceProvider                 // IServiceProvider instance
    new object[] { 2, "World" }     // Constructor arguments
);

CreateFactory() construye internamente un árbol de expresión con la llamada al constructor y finalmente lo compila, por lo que el delegado que retorna es código ejecutable puro y permitirá crear instancias a toda pastilla :)

También es interesante saber que si el constructor define parámetros cuyos valores no pueden ser encontrados en la lista de argumentos que indicamos al utilizar la factoría, intentará recuperarlos desde el proveedor de servicios IServiceProvider que debemos suministrar en la llamada. Por ejemplo, el código anterior también funcionaría con la siguiente clase, porque la instancia del IConfiguration sería obtenida desde el proveedor:

public class MyClass
{
    public MyClass(int i, string s, IConfiguration config)
    {
        // TODO
    }
}

El matching de los valores que suministramos cuando usamos la factoría para crear objetos con los parámetros del constructor se realiza según su tipo, y por orden de aparición. Es decir, de nuevo, el código visto anteriormente para crear objetos también funcionaría si la clase fuera la siguiente, donde hemos cambiado el orden de los parámetros de su constructor:

public class MyClass
{
    public MyClass(string s, IConfiguration config, int i)
    {
        // TODO
    }
}

Si existiese un problema de ambigüedad debido a la existencia de más de un constructor que encaje con los parámetros especificados, se lanzará una excepción en tiempo de ejecución. Pero en este caso podemos forzar el uso de un constructor determinado decorándolo con el atributo [ActivatorUtilitiesConstructor].

CreateInstance() / CreateInstance<T>()

Estos métodos tienen exactamente la misma utilidad: crear la instancia de una clase, cuyo tipo indicamos mediante un parámetro formal o genérico, según el método elegido.

Continuando con la clase MyClass que hemos visto anteriormente, a continuación se muestran dos fórmulas para utilizar estos métodos en la creación de instancias de la misma:

var myObj = (MyClass)ActivatorUtilities.CreateInstance(
    serviceProvider         // IServiceProvider instance
    typeof(MyClass),        // Class to be instantiated
    1, "Hello"              // Constructor arguments
);

// Or alternatively:
var myObj = ActivatorUtilities.CreateInstance<MyClass>(
    serviceProvider         // IServiceProvider instance
    1, "Hello"              // Constructor arguments
);

GetServiceOrCreateInstance() / GetServiceOrCreateInstance<T>()

Como podemos intuir, estos métodos son muy similares a CreateInstance() y CreateInstance<T>() que hemos visto algo más arriba. La diferencia principal respecto a éstos es que, antes de crear las instancias, consultan el proveedor de servicios para ver si pueden obtenerla desde allí; en caso afirmativo, retornarán la instancia retornada por el proveedor, y, sólo cuando no es posible, crearán nuevos objetos sobre la marcha.

Otra diferencia es que en este caso no podemos enviar parámetros, por lo que su uso se reduce a clases cuyos constructores no presentan parámetros, o si sólo reciben dependencias que pueden ser satisfechas por el inyector. Debido a esta simplificación del alcance, su forma de utilización es muy sencilla:

var myObj = ActivatorUtilities.GetServiceOrCreateInstance<AnotherClass>(
    serviceProvider         // IServiceProvider instance
);

¡Y esto es todo! Espero que conocer esta pequeña perla del sistema de inyección de dependencias os haya resultado interesante, y os sea útil si alguna vez necesitáis hacer algo parecido.

Publicado en Variable not found.

7 Comentarios:

Álvaro dijo...

Muy interesante. Justo tengo pendiente una refactorización de un sistema de plugins y lo voy a probar en cuanto me ponga con ello. Gracias!

José María Aguilar dijo...

Hola! Me alegro de que te pueda resultar útil :)

Gracias por comentar!

Alberto Baigorria dijo...

Excelente, muchas gracias José María.

José María Aguilar dijo...

Muchas gracias, Alberto!

Jose Cousiño dijo...

Gracias por tu inestimable ayuda

Entiendo que para poder utilizar estos métodos, la clase que se encuentra en un ensamblado distinto al de la aplicación, ¿este tiene que estar registrado en el contexto de la aplicación? Y si nuestro ensamblado tiene referencias a otros ensamblados, ¿estos también tiene que estar registrados en el contexto de la aplicación?

Gracias!!!

José María Aguilar dijo...

Hola!

Si el ensamblado está referenciado por el proyecto desde el que creas las instancias, no debería haber problema.

Si no fuera así, y la clase a crear se encuentra en un ensamblado distinto, lo descrito aquí no te valdría tal cual, tendrías que usar otras fórmulas para, en primer lugar, cargar el ensamblado en memoria, y luego instanciar la clase. Probablemente, aunque no lo he probado, valiéndote de reflection podrías obtener datos suficientes para usar los métodos descritos en el post.

Saludos!



Jose Cousiño dijo...

Gracias!!!

Un saludo,