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

18 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, 1 de octubre de 2024
Imagen decorativa mostrando una jeringa inyectando algo en un circuito

Como sabemos, una forma habitual de registrar servicios en el contenedor de dependencias de .NET consiste en indicar al framework la implementación de la interfaz o clase abstracta que debe ser utilizada cuando se solicite una instancia de la misma. Por ejemplo, en el siguiente código, que podría pertenecer a la inicialización de una aplicación ASP.NET Core, se registra la implementación FriendDbRepository para la interfaz IFriendRepository con un ámbito scoped o por petición:

builder.Services.AddScoped<IFriendRepository, FriendDbRepository>();

Hecho esto, podríamos solicitar una instancia de IFriendRepository en cualquier parte de nuestra aplicación que admita inyección de dependencias, como los controladores, manejadores Minimal API, otras dependencias, etc. El inyector encargará de proporcionarnos una instancia de FriendDbRepository allá donde la necesitemos:

public class FriendsController: Controller
{
    public FriendsController(IFriendRepository friendRepository)
    {
        // ... Aquí la tenemos!
    }
}

O bien, podríamos obtenerla directamente desde la colección de servicios:

public void DoSomething(IServiceProvider services)
{
    var repo = services.GetRequiredService<IFriendRepository>();
    ...
}

Como vemos en ambos casos, la única forma disponible para identificar el servicio que deseamos obtener es utilizar su tipo, o muy habitualmente, la abstracción (interfaz o clase abstracta) a la que está asociado.

Pero eso cambió con la llegada de .NET 8...

¡Bienvenidos, servicios con nombre o keyed services!

En .NET 8 se añadió otra interesante posibilidad para registrar y obtener servicios desde el contenedor de dependencias: los keyed services, o servicios con nombre. Básicamente, la idea consiste en utilizar una clave o nombre en el momento de registrar el servicio, de forma que más adelante pueden solicitarse instancias usando dicho identificador.

Por ejemplo, imaginad que tenemos una aplicación que maneja estas abstracciones y clases:

interface IAnimal
{
    string SayHello();
}

public class Dog : IAnimal
{
    public string SayHello() => "Woof!";
}

public class Cat : IAnimal
{
    public string SayHello() => "Meow!";
}

Los keyed services permiten registrar ambas implementaciones de la interfaz IAnimal usando un nombre o clave para identificarlas, como en el siguiente ejemplo:

builder.Services.AddKeyedScoped<IAnimal, Cat>("gato");
builder.Services.AddKeyedScoped<IAnimal, Dog>("perro");

En primer lugar, fijaos en que hemos usado la extensión AddKeyedScoped() en lugar del tradicional AddScoped(), gracias a la cual podemos suministrar el nombre a la hora de realizar el registro.

Como podéis intuir, existen también los métodos AddKeyedSingleton() y AddKeyedTransient() si queremos registrar servicios con otros ámbitos. Por ejemplo, podríamos usar AddKeyedSingleton() para registrar dos instancias singleton con distinto nombre:

builder.Services.AddKeyedSingleton("john", new Friend("John"));
builder.Services.AddKeyedSingleton("peter", new Friend("Peter"));

En los ejemplos anteriores, los literales "gato", "perro" o "john" que hemos usado para identificar los servicios podrían haber sido cualquier otro tipo de objeto, porque, en realidad, la clave o identificador es de tipo object. La cuestión es que sean únicos e identifiquen claramente los servicios.

Una vez hemos registrado los servicios de esta forma, ya no podremos recuperarlos usando simplemente el tipo deseado; en su lugar, tendremos que indicar adicional y obligatoriamente el nombre o clave usada en su registro. La única excepción a esto es que cuando registramos un keyed service usando el valor null como clave, podremos recuperarlo usando los mecanismos habituales.

Para obtener una instancia de un servicio con nombre desde el constructor de una clase, simplemente debemos usar el atributo [FromKeyedServices] suministrándole la clave correspondiente:

public class HomeController : Controller
{
    private readonly IAnimal _animal;

    public HomeController([FromKeyedServices("gato")]IAnimal animal)
    {
        _animal = animal; // Aquí seguro que tendremos un objeto Cat
        _logger = logger;
    }
    ..
}

También podemos usar el atributo [FromKeyedServices] directamente en manejadores de Minimal API o acciones MVC:

// En Minimal APIs:
app.MapGet("/", ([FromKeyedServices("gato")] IAnimal animal) => animal.SayHello());

// En una acción de controlador:
public class PetController: Controller
{
    public string SayHello([FromKeyedServices("perro")] IAnimal animal) 
        => animal.SayHello();
}

El sistema usará tanto el tipo del parámetro como la clave para determinar el servicio que debe ser retornado. Si no se encuentra, retornará un valor nulo.

De la misma forma, podemos obtener un servicio con nombre desde la colección de servicios usando el método GetKeyedService():

public void DoSomething(IServiceProvider services)
{
    // Devolverá un nulo si no existe:
    var gato = services.GetKeyedService<IAnimal>("gato");

    // O bien, se lanza excepción si no existe:
    var perro = services.GetRequiredKeyedService<IAnimal>("perro");
    ...
}

En definitiva, se trata de un mecanismo muy interesante y que puede ayudarnos a simplificar algunos escenarios, que antes nos veíamos obligados a solucionar usando factorías o implementando interfaces específicas para cada clase.

¡Espero que os resulte útil! 

Publicado en Variable not found.

Aún no hay comentarios, ¡sé el primero!