AutoMapper .NET 6.0

Aujourd’hui nous allons voir comment utiliser AutoMapper de façon simple dans une solution d’API .NET 6.0.

Pour commencer, créez solution API .NET 6.0 avec Visual Studio 2022 et décochez la case « Use Controllers » car on va utiliser par la même occasion les minimal API de .NET 6.0.

Une fois que cela est fait, ajoutezle package AutoMapper dans votre projet via le Nuget Package Manager.

Créez ensuite deux classes liéés par un identifiant comme suit :

Des auteurs :

public class AuthorModel
    {
        [Key]
        public int Id
        {
            get; set;
        }
        public string? FirstName
        {
            get; set;
        }
        public string? LastName
        {
            get; set;
        }

        public virtual ICollection<BookModel> BooksCollection {            get; set; }
    }

et des livres :

 public class BookModel
    {
        [Key]
        public int BookId
        {
            get; set;
        }
        public string? BookTitle
        {
            get; set;
        }

        [ForeignKey("Author")]
        public int? AuthorId
        {
            get;set;
        }

        public virtual AuthorModel Author
        {
            get; set;
        }
    }

Ces classes vont nous servir à requêter notre modèle via des DTO(Data transfer object).

Créons maintenant les DTO :

Un DTO simple pour récupérer un livre :

public class BookSimpleDTO
    {
        public int BookId
        {
            get; set;
        }
        public string? BookTitle
        {
            get; set;
        }
    }

Puis un DTO un peu plus complexe pour récupérer un livre et son auteur :

 public class BookFullDTO
    {
        public int BookId
        {
            get; set;
        }
        public string? BookTitle
        {
            get; set;
        }

        public string? FirstName
        {
            get; set;
        }
        public string? LastName
        {
            get; set;
        }
    }

Rajoutons aussi un context Entity Framework pour accéder à la base de données et un repository :

Contexte (qui va remplir la base avec des données):

   public class DBContext : Microsoft.EntityFrameworkCore.DbContext
    {
        public DBContext(DbContextOptions<DBContext> options) : base(options)
        {

        }

        public virtual DbSet<BookModel>? Books { get; set; }
        public virtual DbSet<AuthorModel>? Authors { get; set; }

        protected override void OnModelCreating(ModelBuilder modelBuilder)
        {
            modelBuilder.Entity<BookModel>()
                .HasOne(c => c.Author)
                .WithMany(e => e.BooksCollection)
                .HasForeignKey(c=>c.AuthorId);

            modelBuilder.Entity<AuthorModel>()
                .HasMany(c => c.BooksCollection)
                .WithOne(c => c.Author);

            modelBuilder.Entity<AuthorModel>().HasData(new AuthorModel
            {
                Id = 1,
                FirstName = "Alexandre",
                LastName = "Castro"

            });
            modelBuilder.Entity<AuthorModel>().HasData(new AuthorModel
            {
                Id = 2,
                FirstName = "Martin",
                LastName = "Fowler"

            });
            modelBuilder.Entity<AuthorModel>().HasData(new AuthorModel
            {
                Id = 3,
                FirstName = "Gang",
                LastName = "Of Four"

            });
            modelBuilder.Entity<AuthorModel>().HasData(new AuthorModel
            {
                Id = 4,
                FirstName = "Christophe",
                LastName = "Mommer"

            });

            modelBuilder.Entity<BookModel>().HasData(new BookModel
            {
                BookId = 1,
                BookTitle = "AutoMapper samples",
                AuthorId =1 
            });
            modelBuilder.Entity<BookModel>().HasData(new BookModel
            {
                BookId = 2,
                BookTitle = "Clean Code",
                AuthorId =2
            });
            modelBuilder.Entity<BookModel>().HasData(new BookModel
            {
                BookId = 3,
                BookTitle = "Design patterns",
                AuthorId = 3
            });
            modelBuilder.Entity<BookModel>().HasData(new BookModel
            {
                BookId = 4,
                BookTitle = "Docker pour les développeurs .Net",
                AuthorId = 4
            });
            modelBuilder.Entity<BookModel>().HasData(new BookModel
            {
                BookId = 5,
                BookTitle = "Blazor :développement front end d'application web dynamiques",
                AuthorId = 4
            });
            base.OnModelCreating(modelBuilder);
        }
    }

Repository :

 public class BookRepository : IBookRepository
    {
        private readonly DBContext _dbContext;
        public BookRepository(DBContext dbContext)
        {
            _dbContext = dbContext;
        }

        public IEnumerable<BookModel> GetBooks()
        {
            return _dbContext.Books?.OrderByDescending(book => book.BookId);
        }

        public BookModel GetBook(int id)
        {
            _dbContext.Authors.Load();
            return _dbContext.Books.Include(c => c.Author).FirstOrDefault(book => book.BookId == id);
        }

         public IQueryable<BookModel> GetBookQueryable(int id)
        {
            _dbContext.Authors.Load();
            return _dbContext.Books.Where(book => book.BookId == id).AsQueryable();
        }

    }

Une fois que tout ceci est fait nous allons pouvoir requêter nos entités et renvoyer des DTO formatés comme notre front le souhaiterait idéalement.

Allons dans notre Program.cs qui est notre point d’entrée et créons le nécessaire pour requêter notre repository.

Injection du repository et du context :

builder.Services.AddDbContext<DBContext>(options => options.UseSqlServer(builder.Configuration.GetConnectionString("DefaultConnection")));
builder.Services.AddTransient<IBookRepository, BookRepository>();

Injection d’AutoMapper:

builder.Services.AddAutoMapper(typeof(Program));

Mapping direct

Passons sur le mapping simple qui ne requiert pas grand chose :

var mapperConfig = new MapperConfiguration(mc =>
{
    mc.CreateMap<BookModel, BookSimpleDTO>();
});

Ici nous ne faisons que dire à notre mapper que la classe BookModel sera mappée avec BookSimpleDTO et si nous avons bien fait les choses, nous n’avons pas besoin de plus. Quand je dis « bien fait les choses », explicitement il faut nommer les champs de destination exactement comme les champs de la source.

Notre première API qui utiliser ce mapper :

app.MapGet("/BookSimpleDTO", (IBookRepository _bookRepository, IMapper mapper) =>
{

    var result = _bookRepository.GetBook(1);
    var destination = mapper.Map<BookModel, BookSimpleDTO>(result);
    return destination;
})
.WithName("GetBookSimpleDTO");

Petit détail , nous injectons directement notre repository et notre mapper dans notre controller :

  1. Injection des dépendances
  2. Appel du repostory pour récupérer un « BookModel »
  3. Utilisation du Mapper
  4. Renvoi des données

Ce qui donne :

Mapping complexe:

Pour notre seconde API nous allons avoir besoin d’un MappingProfile, c’est à dire une classe qui définir le mapping champ par champ :

public class MappingProfile : Profile
    {
        public MappingProfile()
        {
            CreateMap<BookModel, BookFullDTO>()
                .ForMember(m => m.FirstName, opt => opt.MapFrom(s => s.Author.FirstName))
                .ForMember(m => m.LastName, opt => opt.MapFrom(s => s.Author.LastName));
            CreateMap<AuthorModel, AuthorDTO>();
        }

    }

Ici nous définissons un mapping pour BookModel vers BookFullDTO et comme AuthorModel et contenu dans un entité relationnelle à BookModel, il n’est pas possible de récupérer directement ses propriétés donc nous définission que le membre FirstName du DTO est mappé à Author.FirstName du BookModel , même chose pour LastName vous avec compris l’idée.

Et le MappingProfile est capable de faire les choses dans les deux sens en rajoutant .ReverseMap() :

  • BookModel vers BookFullDTO
  • BookFullDTO vers BookModel

Voici la deuxième API qui va effectuer ce mapping :

app.MapGet("/BookFullDTO", (IBookRepository _bookRepository, IMapper mapper) =>
{
    var result = _bookRepository.GetBook(1);
    var destination = mapper.Map<BookModel, BookFullDTO>(result);
    return destination;
})
.WithName("GetBookFullDTO");

et le résultat :

AutoMapper et ProjectTo

Pour moi le projectTo est la feature la plus importante d’AutoMapper car il permet de requêter des objets avec ses entités liés à partir d’un IQueryable sans avoir à faire tous les include dans la requête du repository.

En effet, si vous regardez la méthode du repository GetBookQueryable(int id) , elle ne demande pas explicitement l’entité liée AuthorModel mais AutoMapper sera en capacité de la renvoyer.

et si vous regardez le code éxécuté, il faut automatiquement la jointure SQL :

Faisons un endpoint pour vérifier celà :

app.MapGet("/GetBookFullDTOWithProject", (IBookRepository _bookRepository, IMapper mapper) =>
{
    var result = _bookRepository.GetBookQueryable(1);
    var destination = mapper.ProjectTo<BookFullDTO>(result);
    return destination;
})
.WithName("GetBookFullDTOWithProject");

Nous devrions avoir la même chose que la méthode précédente mais sans avoir eu à faire un mappingProfile :

En effet c’est le cas !

Voila j’ai fait un tour rapide des capacités d’AutoMapper et mon projet d’exemple est disponible ici.

Et si vous souhaitez en savoir plus ça se passe ici => http://automapper.org/

Have fun !