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, 12 de febrero de 2019
ASP.NET Core Como sabemos, ASP.NET Core incluye un sistema de inyección de dependencias que, aunque es algo simple comparado con otras alternativas más veteranas, cubre la mayoría de necesidades habituales. Por ejemplo, un aspecto que no es muy conocido y que puede ser útil en muchas ocasiones es su capacidad para registrar y recuperar múltiples implementaciones de una misma interfaz.

En este post vamos a ver cómo conseguirlo, y un caso práctico de uso de esta técnica en un escenario muy frecuente.

Registro de múltiples implementaciones de la misma interfaz

Como se puede intuir, el registro de distintas implementaciones de la misma interfaz se realiza… pues eso, registrándolas ;) No hay ningún misterio aquí, simplemente añadimos el par <TService, TImplementation> en la clase Startup de ASP.NET Core, usando el lifetime apropiado (singleton, en el ejemplo de abajo):
public class Startup
{
    public void ConfigureServices(IServiceCollection services)
    {
        ...
        services.AddSingleton<IImageFileProcessor, JpegFileProcessor>();
        services.AddSingleton<IImageFileProcessor, HeavyFileProcessor>();
        ... // Add more IImageFileProcessor implementations
    }
    ...
}

Obtención de múltiples implementaciones registradas

Aquí viene la parte que es menos intuitiva, aunque tampoco anda muy lejos de lo que podríamos esperar del sistema de inyección de dependencias de ASP.NET Core. Para reclamar al contenedor las instancias de tipos asociados a la interfaz TService, simplemente añadimos un parámetro de tipo IEnumerable<TService> en el constructor:
public class ImageController: Controller
{
    private readonly IEnumerable<IImageFileProcessor> _imageProcessors;

    public ImageController(IEnumerable<IImageFileProcessor> imageProcessors)
    {
        _imageProcessors = imageProcessors;
    }
    ...
}
Esta colección de instancias de TService que ya tenemos en memoria podemos utilizarla para implementar las funcionalidades que necesitemos para nuestra aplicación.

Pero mejor, veámoslo todo sobre un caso práctico…

Un caso práctico: SOLIDificar código

Imaginad un código como el siguiente, donde se muestra un método Process() que lo único que hace es llamar a otros métodos que procesan objetos de tipo ImageFile bajo determinadas circunstancias:
public class ImageFileProcessor
{
    public ImageFile Process(ImageFile imageFile)
    {
        if (imageFile.Type == "JPG")
            imageFile = ProcessJpeg(imageFile);

        if (imageFile.Type == "PNG")
            imageFile = ProcessPng(imageFile);

        if (imageFile.Size > 100_000)
            imageFile = ProcessHeavyFile(imageFile);

        if (imageFile.Width > 1024 || imageFile.Height > 768)
            imageFile = ProcessBigFile(imageFile);

        [...] // Call to another processors when needed

        return f;
    }

    private ImageFile ProcessJpeg(ImageFile f) { ... }
    private ImageFile ProcessPng(ImageFile f) { ... }
    private ImageFile ProcessHeavyFile(ImageFile f) { ... }
    private ImageFile ProcessBigFile(ImageFile f) { .. }
    [...] // Other processors
}
Entre otros, este código atenta directamente contra Principio de Responsabilidad Única, dando lugar a una clase que a todas luces tendrá a ser muy extensa y difícilmente mantenible al tener demasiadas responsabilidades. Nada que no podamos romper usando algo de Estrategia ;)

El primer paso para mejorar esta implementación sería detectar que en nuestra aplicación existen procesadores de archivos de imagen que se aplican en función de determinados criterios. Con esta información podríamos crear la siguiente abstracción:
public interface IImageFileProcessor
{
    bool CanProcess(ImageFile imageFile);
    ImageFile Process(ImageFile imageFile);
}
En nuestra aplicación existirán tantas implementaciones de IImageProcessor como formas de procesar los ficheros de imagen, y cada uno de estos componentes incluirá tanto la lógica para decidir si puede procesarlas como el proceso en sí mismo:
public class JpegFileProcessor : IImageFileProcessor
{
    public bool CanProcess(ImageFile imageFile) => imageFile.Type == "JPG";

    public ImageFile Process(ImageFile imageFile)
    {
        // Process here the JPEG file
    }
}

public class HeavyFileProcessor : IImageFileProcessor
{
    public bool CanProcess(ImageFile imageFile) => imageFile.Size > 100_000;

    public ImageFile Process(ImageFile imageFile)
    {
        // Process here the heavy file
    }
}

...
De momento, esta estructura ya nos permitiría simplificar la clase inicial, liberándola de responsabilidades que no le corresponden. Serán los componentes procesadores, las distintas implementaciones de IImageFileProcessor, los que llevarán ese peso.

Aplicando la técnica que hemos visto anteriormente, podríamos simplificar la clase inicial ImageFileProcessor suministrándole mediante inyección de dependencias la colección de procesadores, y haciendo que su método Process() simplemente los recorra y ejecute de forma secuencial:
public class ImageFileProcessor
{
    private readonly IEnumerable<IImageFileProcessor> _imageProcessors;

    public ImageFileProcessor(IEnumerable<IImageFileProcessor> imageProcessors)
    {
        _imageProcessors = imageProcessors;
    }

    public ImageFile Process(ImageFile imageFile)
    {
        foreach (var processor in _imageProcessors)
        {
            if (processor.CanProcess(imageFile))
            {
                imageFile = processor.Process(imageFile);
            }
        }
        return imageFile;
    }
}
Para que el componente ImageFileProcessor esté disponible en todos los puntos de nuestra aplicación deberíamos registrarlo en el contenedor de dependencias. Asimismo, ahí registraríamos todos los procesadores de imágenes que hayamos implementado:
public class Startup
{
    public void ConfigureServices(IServiceCollection services)
    {
        ...
        services.AddSingleton<ImageFileProcessor>();
        services.AddSingleton<IImageFileProcessor, JpegFileProcessor>();
        services.AddSingleton<IImageFileProcessor, HeavyFileProcessor>();
        ... // Add more IImageFileProcessor implementations
    }
    ...
}
Ojo, aunque en este caso utilizamos un registro de tipo singleton, esto puede variar en cada escenario en función de las necesidades.
Fijaos que si quisiéramos añadir un nuevo procesador de imágenes, por ejemplo un GifProcessor no sería necesario tocar la clase ImageFileProcessor, ni ninguno de los procesadores existentes. Simplemente crearíamos la clase correspondiente implementando IImageFileProcessor y la registraríamos en el contenedor… and that’s all, folks!
...
services.AddSingleton<IImageFileProcessor, GifFileProcessor>();
En resumen, en este post hemos visto cómo utilizar el sistema de inyección de dependencias de ASP.NET Core para registrar múltiples implementaciones de la misma interfaz y cómo recuperarlas más adelante para implementar soluciones muy limpias a problemas frecuentes.

Espero que podáis aplicarlo a vuestros propios desarrollos ;)

Publicado en Variable not found.

2 Comentarios:

Yago dijo...

Solo una sugerencia, por si alguien va a implementar este código para probarlo, en la interfaz debes cambiar el Preprocess por Process si no te dará error.

Un saludo y gracias por tan buen material.

José María Aguilar dijo...

Hola, Yago!

Tienes razón, se coló un bug en el código ;D

Ya está solucionado, ¡muchas gracias por comentarlo!