Introduction
AutoMapper est une bibliothèque .NET incontournable pour automatiser le mapping entre objets DTO, entités et modèles de vue, évitant les boilerplates fastidieux. En 2026, avec .NET 10, elle reste optimisée pour les performances et intègre nativement les projections EF Core. Pourquoi l'utiliser ? Imaginez mapper un UserEntity complexe vers un UserDto lightweight en une ligne : c'est 80% de code en moins, moins d'erreurs et un refactoring accéléré. Ce tutoriel intermediate part des bases pour aller aux configurations avancées comme les profiles, résolveurs custom et validations. Vous obtiendrez un projet console fonctionnel, extensible à ASP.NET Core, avec des exemples concrets sur un scénario e-commerce : mapping de commandes et produits. Résultat : un mapper robuste, testable et performant dès la fin de lecture.
Prérequis
- .NET 10 SDK installé (vérifiez avec
dotnet --version) - Visual Studio 2026 ou VS Code avec C# extension
- Connaissances de base en C# (classes, LINQ, DI)
- Un éditeur de code et terminal
Créer le projet et installer AutoMapper
dotnet new console -n AutoMapperDemo
cd AutoMapperDemo
dotnet add package AutoMapper
dotnet add package AutoMapper.Extensions.Microsoft.DependencyInjection
dotnet add package Microsoft.Extensions.DependencyInjection
dotnet add package Microsoft.Extensions.Hosting
dotnet add package Microsoft.Extensions.Hosting.Abstractions
code .Cette commande crée un projet console .NET 10, installe AutoMapper core, son extension DI et les paquets Hosting/DI pour un setup moderne. L'extension DI simplifie l'injection du IMapper partout. Évitez les versions anciennes : utilisez toujours les dernières pour les optimisations JIT et AOT.
Définir les modèles source et cible
Avant de mapper, définissons des classes concrètes. Prenons un scénario e-commerce : OrderEntity (de la DB) vers OrderDto (pour l'API). L'entité a des champs internes comme Id auto-généré et une liste de produits ; le DTO expose seulement les infos publiques avec un total calculé.
Classes modèles complètes
namespace AutoMapperDemo;
public class ProductEntity
{
public int Id { get; set; }
public string Name { get; set; } = string.Empty;
public decimal Price { get; set; }
}
public class ProductDto
{
public string Name { get; set; } = string.Empty;
public decimal Price { get; set; }
}
public class OrderEntity
{
public int Id { get; set; }
public string CustomerName { get; set; } = string.Empty;
public DateTime OrderDate { get; set; }
public List<ProductEntity> Products { get; set; } = new();
public decimal Discount { get; set; }
}
public class OrderDto
{
public int Id { get; set; }
public string CustomerName { get; set; } = string.Empty;
public DateTime OrderDate { get; set; }
public List<ProductDto> Products { get; set; } = new();
public decimal Total => Products.Sum(p => p.Price) - Discount;
}Ces classes illustrent un mapping typique : l'entité a un Discount source, le DTO calcule un Total dérivé. Notez les collections imbriquées – AutoMapper les gère récursivement. Piège : oubliez pas les initialisations de listes pour éviter les NullRef en runtime.
Configurer le mapper de base
Analogie : Le mapper est comme un traducteur automatique qui match les propriétés par nom/convention. Ici, on configure un mapping simple qui ignore Id si besoin et mappe les collections.
Configuration basique du IMapper
using AutoMapper;
namespace AutoMapperDemo;
public class MappingProfile : Profile
{
public MappingProfile()
{
CreateMap<ProductEntity, ProductDto>();
CreateMap<OrderEntity, OrderDto>()
.ForMember(dest => dest.Id, opt => opt.Ignore());
}
}
public static class MapperConfig
{
public static MapperConfiguration CreateConfiguration()
{
var config = new MapperConfiguration(cfg =>
{
cfg.AddProfile<MappingProfile>();
});
return config;
}
}On utilise un Profile pour organiser les mappings (best practice). CreateMap auto-map par nom ; ForMember ignore Id pour simuler un DTO sans PK. Cette config est thread-safe et réutilisable. Piège : sans Ignore(), AutoMapper écraserait l'ID client-side.
Intégrer avec Dependency Injection
Pour un usage pro, injectez IMapper via DI. C'est scalable pour APIs ASP.NET Core. On hoste l'app avec IHost pour tester.
Setup DI et mapping initial
using AutoMapper;
using AutoMapperDemo;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
class Program
{
static async Task Main(string[] args)
{
var host = Host.CreateDefaultBuilder(args)
.ConfigureServices((context, services) =>
{
var config = MapperConfig.CreateConfiguration();
services.AddSingleton(config.CreateMapper());
services.AddSingleton<IMapper>(sp => config.CreateMapper());
})
.Build();
var mapper = host.Services.GetRequiredService<IMapper>();
var service = new MappingService(mapper);
await service.RunDemo();
}
}
public class MappingService
{
private readonly IMapper _mapper;
public MappingService(IMapper mapper) => _mapper = mapper;
public async Task RunDemo()
{
var orderEntity = new OrderEntity
{
Id = 1,
CustomerName = "Alice",
OrderDate = DateTime.Now,
Products = new List<ProductEntity>
{
new() { Name = "Laptop", Price = 1200m },
new() { Name = "Souris", Price = 25m }
},
Discount = 50m
};
var orderDto = _mapper.Map<OrderDto>(orderEntity);
Console.WriteLine($"Total: {orderDto.Total}");
await Task.CompletedTask;
}
}DI avec AddSingleton pour performance (mappers sont stateless). Le MappingService demo un mapping live : entité → DTO avec total calculé (1225 - 50 = 1175). Exécutez dotnet run : output confirme. Piège : double CreateMapper() – utilisez une seule instance.
Mappings avancés avec conditions
Passez au niveau intermediate : conditions (Condition), résolveurs custom pour logiques métier.
Résolveur custom et conditions
using AutoMapper;
namespace AutoMapperDemo;
public class CustomTotalResolver : IValueResolver<OrderEntity, OrderDto, decimal>
{
public decimal Resolve(OrderEntity source, OrderDto destination, decimal destMember, ResolutionContext context)
{
var total = source.Products.Sum(p => p.Price);
return total > 1000 ? total * 0.9m : total - source.Discount;
}
}
public class AdvancedMappingProfile : Profile
{
public AdvancedMappingProfile()
{
CreateMap<ProductEntity, ProductDto>();
CreateMap<OrderEntity, OrderDto>()
.ForMember(dest => dest.Total, opt => opt.MapFrom<CustomTotalResolver>())
.ForPath(dest => dest.Products.First().Name, opt => opt.Condition((src, dest, name) => !string.IsNullOrEmpty(name)));
}
}Le IValueResolver custom applique une promo 10% si total >1000 (logique métier). Condition skippe si nom vide. Ajoutez ce profile à MapperConfig via cfg.AddProfile. Remplacez dans Program.cs : total devient 1080 (10% off). Piège : résolveurs non thread-safe – gardez-les stateless.
Validation et tests unitaires
Best practice : Validez toujours les mappings au startup pour catcher les erreurs tôt.
Validation et test unitaire
using AutoMapper;
using FluentAssertions;
using Xunit;
namespace AutoMapperDemo.Tests;
public class MapperTests
{
[Fact]
public void ShouldMapOrderCorrectly()
{
// Arrange
var config = MapperConfig.CreateConfiguration();
config.AssertConfigurationIsValid(); // Validation
var mapper = config.CreateMapper();
var entity = new OrderEntity { /* data as before */ };
// Act
var dto = mapper.Map<OrderDto>(entity);
// Assert
dto.CustomerName.Should().Be(entity.CustomerName);
dto.Products.Should().HaveCount(2);
}
}AssertConfigurationIsValid() lève si mapping incomplet (ex: prop manquante). Intégrez à tests Xunit (ajoutez dotnet add package xunit). FluentAssertions pour asserts lisibles. Piège : sans validation, runtime boom sur props non mappées.
Bonnes pratiques
- Utilisez toujours des Profiles : séparez les mappings par domaine (OrdersProfile, UsersProfile) pour maintenabilité.
- Injectez IMapper via DI : singleton pour perf, scoped en ASP.NET pour contexte DB.
- Validez au startup :
mapper.ConfigurationProvider.AssertConfigurationIsValid()enProgram.cs. - Préférez ProjectTo pour EF :
context.Orders.ProjectToau lieu de Map post-query.(mapper.ConfigurationProvider) - Ignorez les IDs sensibles :
ForMember(dest => dest.Id, opt => opt.Ignore())pour sécurité.
Erreurs courantes à éviter
- Mapper null ou non injecté : Vérifiez
services.AddAutoMapper(typeof(Program))avec l'extension DI. - Performances sur grandes collections : Utilisez
ProjectToEF au lieu deToList().Map()(évite N+1). - Over-posting en DTO : Mappez seulement les props nécessaires ; validez avec
ForAllMembers(opt => opt.Condition(...)). - Mauvaise gestion Null : Ajoutez
AllowNullCollections = trueou initialisez listes dans DTO.
Pour aller plus loin
- Docs officielles : AutoMapper.org
- Intégrez avec EF Core : lisez sur
ProjectTopour queries optimisées. - Source du projet complet : adaptez à ASP.NET Core pour APIs.
- Découvrez nos formations Learni sur .NET avancé pour maîtriser MediatR + AutoMapper en Clean Architecture.