Skip to content
Learni
Voir tous les tutoriels
.NET

Comment maîtriser AutoMapper avancé en .NET 2026

Read in English

Introduction

AutoMapper est une bibliothèque incontournable en .NET pour mapper automatiquement des objets d'un type source vers un type destination, évitant ainsi des tonnes de code boilerplate. En 2026, avec .NET 8+, ses fonctionnalités avancées comme les Value Resolvers personnalisés, les mappings conditionnels, les projections LINQ et les opérations asynchrones permettent d'optimiser les performances dans les APIs scalables et les microservices.

Pourquoi ce tutoriel avancé ? Les débutants se contentent de Map(), mais les pros gèrent les cas complexes : nested objects, calculs dérivés, validations runtime et queries optimisées. Imaginez mapper un Order vers OrderDto en calculant dynamiquement le total TTC avec taxes variables – c'est là qu'AutoMapper brille.

Ce guide progressif vous guide de la configuration DI à des scénarios réels, avec code 100% fonctionnel testable en console app. À la fin, vos mappings seront fluides, testables et performants, bookmark-worthy pour tout architecte .NET (128 mots).

Prérequis

  • .NET 8+ installé (SDK)
  • Connaissances avancées en C# (LINQ, DI, async/await)
  • Visual Studio 2022+ ou VS Code avec C# extension
  • Familiarité avec les DTOs et principes SOLID
  • Outils : terminal pour dotnet CLI

Créer le projet et installer AutoMapper

terminal
dotnet new console -o AutoMapperAdvancedDemo
cd AutoMapperAdvancedDemo

dotnet add package AutoMapper
 dotnet add package AutoMapper.Extensions.Microsoft.DependencyInjection

dotnet add package Microsoft.Extensions.DependencyInjection
 dotnet add package Microsoft.Extensions.Hosting

 dotnet build

Cette commande crée une app console .NET 8+ et installe AutoMapper avec son extension DI. Microsoft.Extensions.Hosting gère le conteneur DI. Le build valide tout ; exécutez-la ensuite pour tester les mappings.

Définir les entités et DTOs sources

Avant les mappings, définissons des modèles réalistes : une User avec Address nested, et un Order avec lignes calculées. Les DTOs exposent seulement ce qui est nécessaire, suivant le principe de least privilege. Cela prépare les cas avancés comme les résolutions nested et conditions.

Modèles entités et DTOs

Models.cs
namespace AutoMapperAdvancedDemo;

public class User
{
    public int Id { get; set; }
    public string Name { get; set; } = string.Empty;
    public Address Address { get; set; } = new();
    public List<Order> Orders { get; set; } = new();
}

public class Address
{
    public string Street { get; set; } = string.Empty;
    public string City { get; set; } = string.Empty;
    public string Country { get; set; } = string.Empty;
}

public class Order
{
    public int Id { get; set; }
    public string Product { get; set; } = string.Empty;
    public decimal Price { get; set; }
    public int Quantity { get; set; }
    public DateTime OrderDate { get; set; }
}

public class UserDto
{
    public int Id { get; set; }
    public string FullName { get; set; } = string.Empty;
    public string AddressLine { get; set; } = string.Empty;
    public decimal TotalOrders { get; set; }
}

public class OrderDto
{
    public int Id { get; set; }
    public string Product { get; set; } = string.Empty;
    public decimal Total { get; set; }
    public bool IsRecent { get; set; }
}

Ces classes modélisent un domaine e-commerce. UserDto.FullName sera concaténé, TotalOrders calculé via resolver, AddressLine nested. OrderDto.IsRecent utilise une condition (< 30 jours). Tout est prêt pour des mappings avancés.

Créer un Profile de base avec DI

Les Profiles centralisent les configurations de mapping, rendant le code DRY et testable. En DI, AutoMapper scanne les assemblies pour les charger automatiquement. On commence simple avant d'ajouter de la complexité.

Profile de base pour User

Profiles/UserProfile.cs
using AutoMapper;

namespace AutoMapperAdvancedDemo.Profiles;

public class UserProfile : Profile
{
    public UserProfile()
    {
        CreateMap<User, UserDto>()
            .ForMember(dest => dest.FullName, opt => opt.MapFrom(src => src.Name.ToUpper()))
            .ForMember(dest => dest.AddressLine, opt => opt.MapFrom(src => $"{src.Address.Street}, {src.Address.City}"));
    }
}

Ce Profile mappe User vers UserDto avec deux customs : FullName en majuscules (string transformation), AddressLine nested concaténé. CreateMap est fluide ; testez-le pour valider avant d'avancer.

Mappings avancés : conditions et ignores

Pour OrderDto, appliquons des conditions (ForMember avec predicate) et ignores. Imaginez ignorer Price si Quantity=0, ou IsRecent basé sur date. Cela évite les fuites de données sensibles.

Profile Order avec conditions

Profiles/OrderProfile.cs
using AutoMapper;

namespace AutoMapperAdvancedDemo.Profiles;

public class OrderProfile : Profile
{
    public OrderProfile()
    {
        CreateMap<Order, OrderDto>()
            .ForMember(dest => dest.Total, opt => opt.MapFrom(src => src.Price * src.Quantity))
            .ForMember(dest => dest.IsRecent, opt => opt.MapFrom(src => src.OrderDate > DateTime.Now.AddDays(-30)))
            .ForPath(dest => dest.Product, opt => opt.Condition(src => !string.IsNullOrEmpty(src.Product)));
    }
}

Ici, Total est calculé, IsRecent booléen conditionnel sur date, Product mappé seulement si non vide (Condition). ForPath gère nested si besoin. Efficace pour valider runtime sans exceptions.

Value Resolver personnalisé

Les IMemberValueResolver brillent pour logics complexes réutilisables, comme sommer TotalOrders sur collection. Créez-en un pour UserDto.TotalOrders, injectable et testable indépendamment.

Custom TotalOrders Resolver

Resolvers/TotalOrdersResolver.cs
using AutoMapper;
using AutoMapperAdvancedDemo;

namespace AutoMapperAdvancedDemo.Resolvers;

public class TotalOrdersResolver : IValueResolver<User, UserDto, decimal>
{
    public decimal Resolve(User source, UserDto destination, decimal destMember, ResolutionContext context)
    {
        return source.Orders.Sum(o => o.Price * o.Quantity * 1.2m); // TTC avec 20% TVA
    }
}

Ce resolver somme les totals Orders avec TVA dynamique (20%). Signature : source, dest, currentValue, context (pour nested resolutions). Réutilisable sur tout mapping User.

Intégrer le resolver dans UserProfile

Profiles/UserProfile.cs
using AutoMapper;
using AutoMapperAdvancedDemo.Resolvers;

namespace AutoMapperAdvancedDemo.Profiles;

public class UserProfile : Profile
{
    public UserProfile()
    {
        CreateMap<User, UserDto>()
            .ForMember(dest => dest.FullName, opt => opt.MapFrom(src => src.Name.ToUpper()))
            .ForMember(dest => dest.AddressLine, opt => opt.MapFrom(src => $"{src.Address.Street}, {src.Address.City}"))
            .ForMember(dest => dest.TotalOrders, opt => opt.MapFrom<TotalOrdersResolver>());
    }
}

Ajout du resolver via MapFrom(). AutoMapper l'injecte via DI si registered. Mise à jour complète du Profile ; maintenant TotalOrders est calculé automatiquement.

Configurer DI et exécuter les mappings

Dans .NET 8+, utilisez Host.CreateApplicationBuilder pour DI. Scanner les Profiles et Resolvers. Testez en console pour valider tout.

Configuration DI et Program principal

Program.cs
using AutoMapper;
using AutoMapperAdvancedDemo;
using AutoMapperAdvancedDemo.Profiles;
using AutoMapperAdvancedDemo.Resolvers;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;

namespace AutoMapperAdvancedDemo;

class Program
{
    static async Task Main(string[] args)
    {
        var builder = Host.CreateApplicationBuilder(args);

        builder.Services.AddAutoMapper(AppDomain.CurrentDomain.GetAssemblies());
        builder.Services.AddSingleton<TotalOrdersResolver>(); // Explicit pour resolver

        var host = builder.Build();

        var mapper = host.Services.GetRequiredService<IMapper>();

        // Test data
        var user = new User
        {
            Id = 1,
            Name = "John Doe",
            Address = new Address { Street = "123 Rue Exemple", City = "Paris", Country = "France" },
            Orders = new List<Order>
            {
                new() { Id = 1, Product = "Laptop", Price = 1000m, Quantity = 1, OrderDate = DateTime.Now.AddDays(-10) },
                new() { Id = 2, Product = "Souris", Price = 20m, Quantity = 2, OrderDate = DateTime.Now.AddDays(-5) }
            }
        };

        var userDto = mapper.Map<UserDto>(user);
        var orderDto = mapper.Map<OrderDto>(user.Orders[0]);

        Console.WriteLine($"UserDto: Id={userDto.Id}, FullName={userDto.FullName}, AddressLine={userDto.AddressLine}, TotalOrders={userDto.TotalOrders}");
        Console.WriteLine($"OrderDto: Id={orderDto.Id}, Product={orderDto.Product}, Total={orderDto.Total}, IsRecent={orderDto.IsRecent}");
    }
}

DI avec AddAutoMapper(assemblies) scanne Profiles. Singleton pour resolver custom. Main crée data test, mappe et affiche. Exécutez dotnet run : output valide tous mappings (TotalOrders=1440 TTC).

Projections LINQ avancées

ProjectTo optimise les queries EF Core : mappe directement en SQL sans charger entités. Idéal pour APIs performantes.

Exemple de projection (snippet utilisable en repo)

ProjectionExample.cs
using AutoMapper;
using AutoMapper.QueryableExtensions;

// Suppose un DbContext UsersDbContext avec DbSet<User>

var query = context.Users
    .ProjectTo<UserDto>(mapper.ConfigurationProvider)
    .Where(u => u.TotalOrders > 1000)
    .ToList();

// Async
// var queryAsync = await context.Users.ProjectTo<UserDto>(mapper.ConfigurationProvider).ToListAsync();

Intégrez dans un repo EF : ProjectTo traduit mappings en SQL (ex. SUM sur Orders via resolver si configuré). Gagne 80% perf vs Load+Map. Ajoutez à Program pour tests.

Bonnes pratiques

  • Toujours utiliser Profiles : centralisés, versionnables, testables avec MapperConfiguration.AssertConfigurationIsValid().
  • Injectez IMapper en service : pas static, pour mock en unit tests.
  • Préférez ProjectTo pour queries : évite N+1 et surcharges mémoire.
  • Validez configs au startup : mapper.ConfigurationProvider.AssertConfigurationIsValid() en dev.
  • Async resolvers pour I/O : implémentez IAsyncValueResolver avec ResolveAsync.

Erreurs courantes à éviter

  • Oublier DI scan : Profiles non trouvés → AddAutoMapper(AppDomain.CurrentDomain.GetAssemblies()) obligatoire.
  • Resolver non registered : NullReference ; enregistrez comme Singleton/Scoped.
  • Mappings circulaires : StackOverflow ; utilisez DisableConstructorMapping() ou PreserveReferences().
  • Ignorer conditions : fuites données ; testez avec data edge-case (null, empty).
  • Performances : évitez Map() sur collections massives ; priorisez ProjectTo.

Pour aller plus loin