DEV Community

Isaac Ojeda
Isaac Ojeda

Posted on

[Parte 3] ASP.NET: AutoMapper

Introducción

Continuando con esta serie de Posts donde hablamos de cómo podemos implementar CQRS con MediatR.

Hemos visto como emplear CQRS, como validar Requests y ahora en esta publicación, veremos cómo hacer uso de AutoMapper para el mappeo de entidades.

El mappeo de objetos es algo muy común cuando dividimos nuestra aplicación por Technical Concerns. Los DTOs sirven para mostrar información relevante al endpoint que se ejecuta, y puede involucrar varios domain entities, por lo que regresar todo en uno es una tarea común.

Con CQRS estamos trabajando con un concepto muy sencillo: Request -> Handler -> Response.

Por lo que, será de lo más común, tener que mappear un Request a un Domain Entity cuando ejecutamos un comando, pero también será algo recurrente el tener que mappear un Domain Entity a un Response en los Queries.

Esta tarea suele ser repetitiva y monotona, así que veamos cómo solucionarlo con AutoMapper.

Puedes encontrar aquí el código fuente actualizado a este post -> DevToPosts/MediatrValidationExample at post-part3 · isaacOjeda/DevToPosts (github.com)

¿Qué es AutoMapper?

AutoMapper es un object-object mapper. O sea, crea un objeto copiandolo de otro.

Image description

Por ejemplo, en el listado de Productos que hemos hecho en posts anteriores, tenemos el Entity Product y el DTO GetProductsQueryResponse.

public class Product
{
    public int ProductId { get; set; }
    public string Description { get; set; } = default!;
    public double Price { get; set; }
}
Enter fullscreen mode Exit fullscreen mode
public class GetProductsQueryResponse
{
    public int ProductId { get; set; }
    public string Description { get; set; } = default!;
    public double Price { get; set; }
}
Enter fullscreen mode Exit fullscreen mode

Realmente estas clases son iguales, pero tienen propósitos distintos. La intención de AutoMapper (o de cualquier mapper) es ahorrarnos la molestia de tener que asignar las propiedades de clase Type A a clase Type B.

Es muy común que suceda esto, y a veces sucede con clases gigantes. Este proceso suele ser repetitivo, por lo tanto, hay que ahorrarnos esa tarea.

En GetProductsQuery existe este mappeo manual:

.Select(s => new GetProductsQueryResponse
    {
        ProductId = s.ProductId,
        Description = s.Description,
        Price = s.Price
    })
Enter fullscreen mode Exit fullscreen mode

Esta asignación repetitiva es lo que nos queremos ahorrar, y tal vez aquí no vemos el problema, es un mappeo muy sencillo (de tres propiedades) pero suelen complicarse.

Agregando AutoMapper

Para comenzar a utilizarlo, necesitamos agregar el siguiente paquete

dotnet add package AutoMapper.Extensions.Microsoft.DependencyInjection
Enter fullscreen mode Exit fullscreen mode

Se podría instalar solamente AutoMapper, pero en este ya viene incluido y queremos una pequeña ayuda para registrar los Mappers como dependencias.

Image description

Registrando AutoMapper

Para que todo lo que vayamos a hacer con AutoMapper funcione, debemos de registrar toda la configuración como ya es costumbre, en el Program, y así registrar las dependencias necesarias

// code...
builder.Services.AddAutoMapper(Assembly.GetExecutingAssembly());
// code...
Enter fullscreen mode Exit fullscreen mode

Aquí buscará todos los Mapping Profiles registrados en el proyecto.

Creando Mapping Profiles

Un Profile es la descripción de como AutoMapper va a mappear un objeto, a veces todo es de manera automática, pero en varias ocasiones será necesario decirle un poco más a AutoMapper de como mappear los objetos.

Siguiendo el ejemplo de GetProductsQuery, en este mismo archivo agregaremos la clase GetProductsQueryProfileque describirá el Mappeo que queremos hacer.

public class GetProductsQueryProfile : Profile
{
    public GetProductsQueryProfile() =>
        CreateMap<Product, GetProductsQueryResponse>();
}
Enter fullscreen mode Exit fullscreen mode

Realmente es muy sencillo, aquí el mappeo se puede hacer directamente utilizando el método CreateMap<TSource, TDestination>(). Como se puede obviar, el primero es el tipo origen y el segundo el destino.

En el momento que AutoMapper quiera hacer un mappeo, buscará todos los Profiles registrados, si no lo encuentra, lanzará una excepción.

IMapper y Queries

Para hacer eso de este mappeo tenemos la opción de hacerlo desde Entity Framework como parte del IQueryable<T>. Esto es genial ya que los queries se pueden formar a partir de un Profile.

Entonces, actualizando el Query queda de la siguiente forma:

public class GetProductsQueryHandler : IRequestHandler<GetProductsQuery, List<GetProductsQueryResponse>>
{
    private readonly MyAppDbContext _context;
    private readonly IMapper _mapper;

    public GetProductsQueryHandler(MyAppDbContext context, IMapper mapper)
    {
        _context = context;
        _mapper = mapper;
    }

    public Task<List<GetProductsQueryResponse>> Handle(GetProductsQuery request, CancellationToken cancellationToken) =>
        _context.Products
            .AsNoTracking()
            .ProjectTo<GetProductsQueryResponse>(_mapper.ConfigurationProvider)
            .ToListAsync();
}
Enter fullscreen mode Exit fullscreen mode

Utilizamos la extensión ProjectTo<TDestionation>()para aplicar el mappeo según el origen. El origen es el entity Product y el destino GetProductsQueryResponse.

Mappeo explícito con ForMember

El Profile que hicimos anteriormente es un mappeo directo, pero a veces necesitaremos especificar como mappear alguna propiedad en particular porque tal vez no se llaman igual o tiene una forma especial de conseguirse.

Para mostrar este ejemplo nos inventaremos una nueva propiedad llamada ListDescription que lo único que hará es mostrar el nombre y precio en una misma propiedad. No tiene mucho sentido, pero es la forma sencilla de mostrar este caso de uso. Agregando dicha propiedad en el actual response, vemos el Profile actualizado de la siguiente forma:

public class GetProductsQueryProfile : Profile
{
    public GetProductsQueryProfile() =>
        CreateMap<Product, GetProductsQueryResponse>()
            .ForMember(dest =>
                dest.ListDescription,
                opt => opt.MapFrom(mf => $"{mf.Description} - {mf.Price:c}"));

}
Enter fullscreen mode Exit fullscreen mode

Utilizamos ForMember para especificar como mappear propiedades en particular. En este caso, la nueva propiedad ListDescription será compuesta por dos propiedades existentes (incluso, el precio con formato Currency) esto de ninguna forma lo puede saber AutoMapper automaticamente, por eso es útil tener también esta posibilidad porque siempre habrá Entities más complicados que otros.

Si corremos el query con Swagger veremos el profile en acción:

[
  {
    "productId": 1,
    "description": "Product 01",
    "price": 16000,
    "listDescription": "Product 01 - $16,000.00"
  },
  {
    "productId": 2,
    "description": "Product 02",
    "price": 52200,
    "listDescription": "Product 02 - $52,200.00"
  }
]
Enter fullscreen mode Exit fullscreen mode

IMapper y Commands

También podemos mappear a la inversa, es decir, mappear de un DTO a un Entity.

Aquí no estamos haciendo Queries, por lo que no podemos usar ProjectTo<TDestionation>, pero sí podemos usar IMapper para mappear objetos individualmente.

public class CreateProductCommandHandler : IRequestHandler<CreateProductCommand>
{
    private readonly MyAppDbContext _context;
    private readonly IMapper _mapper;

    public CreateProductCommandHandler(MyAppDbContext context, IMapper mapper)
    {
        _context = context;
        _mapper = mapper;
    }


    public async Task<Unit> Handle(CreateProductCommand request, CancellationToken cancellationToken)
    {
        var newProduct = _mapper.Map<Product>(request);

        _context.Products.Add(newProduct);

        await _context.SaveChangesAsync();

        return Unit.Value;
    }
}
public class CreateProductCommandMapper : Profile
{
    public CreateProductCommandMapper() =>
        CreateMap<CreateProductCommand, Product>();
}
Enter fullscreen mode Exit fullscreen mode

De igual forma creamos el Profile, pero en lugar de usar ProjectTo<TDestionaiton> usamos el método Map<TDestionation>() que IMappernos da.

Dado a que el Profile existe, mapeará las propiedades sin ningún problema.

¿Ves ahora un nuevo problema?

¿Cuántos Profiles hemos creado que son casi iguales? es decir, una clase que en su constructor manda a llamar solamente el CreateMap y lo único que cambia es el TSource y TDestionation.

Es verdad que solucionamos un problema y agregamos otro (aunque no grave). Pero con Reflection se puede arreglar sin problema.

Jason Taylor y su template de Clean Architecture solucionan este problema de una forma muy elegante CleanArchitecture/src/Application/Common/Mappings at main · jasontaylordev/CleanArchitecture (github.com).

Si quieres que lo veamos a detalle en español, dime y con gusto expandimos este post.

Conclusión

El uso de librerías como AutoMapper ya debería ser un approach ya incluido en cualquier proyecto por default.

Es útil, ya que siempre haremos este tipo de mappeo. Lo hacemos manualmente o automáticamente, pero siempre haremos algún tipo de mappeo.

Espero te haya gustado este post, cualquier pregunta, no dudes en escribirla abajo en los comentarios 💬.

Referencias

Discussion (1)

Collapse
joseduquee profile image
joseduquee

Genial tu artículo!

Si pudieras publicar algo más a detalle explicando la solución de Jayson Taylor te agradecería montón!

Gracias!