mapper (1.0.1)

Published 2026-07-20 16:03:10 +02:00 by Yoann

Installation

dotnet nuget add source --name Yoann --username your_username --password your_token 
dotnet add package --source Yoann --version 1.0.1 mapper

About this package

Librairie qui sert de mapper pour les objets

Quelques mots sur ce NuGet

L'utilité de ce package est arrivée lors de la mise en place d'une tarification du package AutoMapper lors de le mise en production d'un projet. Ce package permet :

  • un gain de temps non négligeable
  • une réduction significative du code
  • un contrôle totale sur le code
  • une source unique pour bénéficier de cette fonctionnalité
  • une meilleure performance (moins de réflexion)
  • un import beaucoup plus léger

Table des matières

Installation

Utilisation

Installation

Une documentation est disponible sur ce sujet ici 👉 docmost

Mais assurez vous d'avoir cette source https://git.lightyear-corp.fr/api/packages/Yoann/nuget/index.json dans votre fichier qui regroupe les sources d'approvisionnement des packages.

Après avoir lu la documentation, vous n'avez plus qu'à ajouter le package si ça n'est pas déjà fait en executant cette commande.

dotnet add package mapper

Prérequis

.NET 9.0 ou supérieur

Utilisation

1. Définir vos mappings

Créez une classe de configuration (similaire à un Profile AutoMapper) :

using mapper;

public static class UserMapperProfile
{
/// <summary>
/// clsse qui illustre tous les mappings relatifs aux utilisateurs.
/// cette méthode est appelée au démarrage de l'application.
/// </summary>
public static void Configure(MappingConfiguration cfg)
{
// mapping simple DTO -> Entity
// copie automatiquement toutes les propriétés avec le même nom
cfg.CreateMap<CreateUserDto, User>();
cfg.CreateMap<UpdateUserDto, User>();

        // Mapping Entity -> DTO avec transformations
        cfg.CreateMap<User, UserDto>()
            // Concatène FirstName et LastName pour créer FullName
            .ForMember(d => d.FullName, src => $"{src.FirstName} {src.LastName}")
            // Calcule IsActive en fonction de DeletedAt
            .ForMember(d => d.IsActive, src => src.DeletedAt == null)
            // Ignore les propriétés système
            .Ignore(d => d.Id);

        // Mapping pour résumé avec propriétés ignorées
        cfg.CreateMap<User, UserSummaryDto>()
            .Ignore(d => d.CreatedAt)
            .Ignore(d => d.UpdatedAt);

        // Mapping pour PATCH : ignore les valeurs NULL dans le DTO source
        // Permet les mises à jour partielles sans écraser les champs non fournis
        cfg.CreateMap<PatchUserDto, User>()
            .SkipNullSourceValues();
    }
}

2. Enregistrer dans l'injection de dépendances

Dans votre fichier Program.cs ou dans votre classe qui sert de configuration :

// Configuration de mapper
builder.Services.AddSingleton<IObjectMapper>(serviceProvider =>
{
var cfg = new MappingConfiguration();

    // Chargez tous vos profils de mapping
    UserMapperProfile.Configure(cfg);
    RoleMapperProfile.Configure(cfg);
    OrderMapperProfile.Configure(cfg);
    
    // retourne le mapper configuré (singleton)
    return new ObjectMapper(cfg);
});

Pourquoi Singleton ? Le mapper ne contient que de la configuration statique, il n'a pas d'état changeant. Un singleton économise la mémoire et améliore les performances.

3. Utiliser dans vos services

public class UserService
{
private readonly IObjectMapper _mapper;
private readonly IUserRepository _userRepo;

    public UserService(IObjectMapper mapper, IUserRepository userRepo)
    {
        _mapper = mapper;
        _userRepo = userRepo;
    }

    /// <summary>
    /// CREATE : Convertit un DTO en entité, la sauvegarde, puis retourne un DTO
    /// </summary>
    public async Task<UserDto> CreateAsync(CreateUserDto dto)
    {
        // DTO -> Entity
        var user = _mapper.Map<User>(dto);
        
        // ajoute à la base de données
        await _userRepo.AddAsync(user);
        
        // Entity -> DTO (pour la réponse API)
        return _mapper.Map<UserDto>(user);
    }

    /// <summary>
    /// READ : Récupère une entité et la convertit en DTO
    /// </summary>
    public async Task<UserDto> GetByIdAsync(int id)
    {
        var user = await _userRepo.GetByIdAsync(id);
        if (user == null)
            throw new NotFoundException($"User {id} non trouvé");
            
        return _mapper.Map<UserDto>(user);
    }

    /// <summary>
    /// UPDATE complet : Remplace toutes les propriétés
    /// </summary>
    public async Task UpdateAsync(UpdateUserDto dto)
    {
        var user = await _userRepo.GetByIdAsync(dto.Id);
        
        // Map le DTO sur l'instance existante (UPDATE)
        // Cela modifie directement 'user' avec les valeurs du DTO
        _mapper.Map(dto, user);
        
        await _userRepo.UpdateAsync(user);
    }

    /// <summary>
    /// PATCH partiel : Met à jour seulement les propriétés non-null
    /// </summary>
    public async Task PatchAsync(int id, PatchUserDto patch)
    {
        var user = await _userRepo.GetByIdAsync(id);
        
        // SkipNullSourceValues fait que seules les props non-null du patch sont copiées
        // si patch.Email est null, le email de 'user' n'est pas modifié
        _mapper.Map(patch, user);
        
        await _userRepo.UpdateAsync(user);
    }

    /// <summary>
    /// lisdt : Récupère plusieurs entités et les convertit en DTOs
    /// </summary>
    public async Task<List<UserDto>> GetAllAsync()
    {
        var users = await _userRepo.GetAllAsync();
        
        // Depuis 1.0.1, mapper gère les collections : _mapper.Map<List<UserDto>>(users) fonctionne.
        // La forme LINQ ci-dessous reste équivalente si vous préférez l'expliciter.
        return users.Select(u => _mapper.Map<UserDto>(u)).ToList();
    }

    /// <summary>
    /// Exemple avec filtre avancé
    /// </summary>
    public async Task<List<UserDto>> GetActiveUsersAsync()
    {
        var users = await _userRepo.GetAllAsync();
        
        // filtre puis mappe
        return users
            .Where(u => u.DeletedAt == null)
            .Select(u => _mapper.Map<UserDto>(u))
            .ToList();
    }
}

Concepts fondamentaux

Mapping unidirectionnel vs bidirectionnel

// Unidirectionnel (une seule direction)
cfg.CreateMap<CreateUserDto, User>();  // DTO -> Entity seulement

// Bidirectionnel (deux directions)
cfg.CreateMap<User, UserDto>();        // Entity -> DTO
cfg.CreateMap<UserDto, User>();        // DTO -> Entity

// les deux directions ne sont PAS automatiques
// vous devez explicitement créer les deux mappings

CreateMap : Copie automatique des propriétés

// createMao copie AUTOMATIQUEMENT toutes les propriétés ayant :
// - le même nom (case-insensitive)
// - un type compatible

public class User
{
public int Id { get; set; }           
public string FirstName { get; set; } 
public string Email { get; set; }     
}

public class UserDto
{
public int Id { get; set; }           
public string FirstName { get; set; } 
public string Email { get; set; }  
}

cfg.CreateMap<User, UserDto>(); // aucune config nécessaire!!!

ForMember : Personnaliser le mapping d'une propriété

cfg.CreateMap<User, UserDto>()
// prop complexe : créer à partir de plusieurs champs
.ForMember(d => d.FullName, src => $"{src.FirstName} {src.LastName}")

    // prop calculée : transformer les données
    .ForMember(d => d.IsActive, src => src.DeletedAt == null)
    
    // enum : mapper à partir d'une string
    .ForMember(d => d.RoleLabel, src => 
        src.Role switch
        {
            UserRole.Admin => "Administrateur",
            UserRole.User => "Utilisateur",
            _ => "Inconnu"
        })
    
    // accès à des propriétés imbriquées
    .ForMember(d => d.ManagerName, src => 
        src.Manager?.FirstName + " " + src.Manager?.LastName)
    
    // convertion de type
    .ForMember(d => d.BirthDateString, src => src.BirthDate.ToString("dd/MM/yyyy"));

Ignore : Ignorer certaines propriétés

cfg.CreateMap<User, UserDto>()

    // prop sensibles (sécurité)
    .Ignore(d => d.PasswordHash)
    .Ignore(d => d.PasswordSalt)
    .Ignore(d => d.InternalNotes)
    
    // prop trop volumineuses (perf)
    .Ignore(d => d.LargeDocumentContent);

SkipNullSourceValues : PATCH intelligent

cfg.CreateMap<PatchUserDto, User>()
.SkipNullSourceValues(); // ignore les propriétés null du DTO

// avant l'utilisation de SkipNullSourceValues :
var patch = new PatchUserDto { Email = "new@example.com", FirstName = null };
_mapper.Map(patch, user);
// user.FirstName = null (écrasé!)

// Après SkipNullSourceValues :
var patch = new PatchUserDto { Email = "new@example.com", FirstName = null };
_mapper.Map(patch, user);
// user.Email = "new@example.com" (changé)
// user.FirstName reste inchangé (null ignoré)

Classe présente dans le package NuGet

Interface MappingConfiguration

public class MappingConfiguration
{
/// <summary>
/// Crée un nouveau mapping de Source vers Destination
/// </summary>
public MappingExpression<TSource, TDestination> CreateMap<TSource, TDestination>()
where TSource : class
where TDestination : class, new();
}

Interface MappingExpression

public class MappingExpression<TSource, TDestination>
{
/// <summary>
/// Personnalise le mapping d'une propriété destination
/// </summary>
/// <param name="destinationSelector">La propriété à mapper</param>
/// <param name="sourceMember">La transformation à appliquer</param>
public MappingExpression<TSource, TDestination> ForMember<TMember>(
Expression<Func<TDestination, TMember>> destinationSelector,
Func<TSource, TMember> sourceMember);

    /// <summary>
    /// Ignore complètement une propriété (ne la mappe jamais)
    /// </summary>
    public MappingExpression<TSource, TDestination> Ignore<TMember>(
        Expression<Func<TDestination, TMember>> destinationSelector);

    /// <summary>
    /// Ignore les propriétés null dans la source lors du mapping
    /// Utile pour les PATCH
    /// </summary>
    public MappingExpression<TSource, TDestination> SkipNullSourceValues();
}

Exemples d'utilisation complète

  1. Mapping simple (copie automatique)
cfg.CreateMap<User, UserDto>();

// ça ;équivayut à :
var user = new User { Id = 1, FirstName = "Yoann", Email = "john.doe@example.com" };
var dto = _mapper.Map<UserDto>(user);
// dto.Id = 1
// dto.FirstName = "Yoann"
// dto.Email = "john.doe@example.com"
  1. Mapping avec transformations
cfg.CreateMap<Order, OrderSummaryDto>()
// Concat strings
.ForMember(d => d.CustomerInfo,
src => $"{src.Customer.FirstName} {src.Customer.LastName}")

    // Calcul mathématique
    .ForMember(d => d.TotalPrice, 
        src => src.Items.Sum(i => i.Price * i.Quantity))
    
    // Condition
    .ForMember(d => d.StatusLabel, 
        src => src.IsPaid ? "Payée" : "En attente")
    
    // Switch expression
    .ForMember(d => d.Priority,
        src => src.Amount switch
        {
            > 1000 => "Haute",
            > 500 => "Moyenne",
            _ => "Basse"
        });
  1. Mapping avec null-safety (si une prop est nulle l'applciation va crash)
cfg.CreateMap<Employee, EmployeeDto>()

.ForMember(d => d.DepartmentName,
src => src.Department?.Name ?? "Non assigné")

    // null-conditionnel + default
    .ForMember(d => d.ManagerFullName, 
        src => src.Manager != null 
            ? $"{src.Manager.FirstName} {src.Manager.LastName}" 
            : "Pas de manager")
    
    // collecction null-safe
    .ForMember(d => d.ProjectCount, 
        src => (src.Projects ?? new List<Project>()).Count);
  1. Mapping pour PATCH (mise à jour partielle)
cfg.CreateMap<PatchUserDto, User>()
.SkipNullSourceValues()
.ForMember(d => d.Email, src => src.Email?.ToLower())  // normalisation optionnelle
.Ignore(d => d.Id)          // jamais modifier l'ID
.Ignore(d => d.CreatedAt)  
.Ignore(d => d.DeletedAt); 

// utilisation de notre package :
var patch = new PatchUserDto
{
FirstName = "Nouveau",
Email = null  // null du coup ça n'est pas copié
};
_mapper.Map(patch, user);
// user.FirstName = "Nouveau"
// user.Email reste inchangé

Les collections

Depuis la version 1.0.1, mapper prend en charge le mapping direct de collections. En interne, il mappe chaque élément un par un (même logique unitaire), puis regroupe le résultat dans le type de destination.

var users = new List<User> { ... };

var dtos  = _mapper.Map<List<UserDto>>(users);        // List<UserDto>
var dtos2 = _mapper.Map<IEnumerable<UserDto>>(users); // IEnumerable<UserDto>
var dtos3 = _mapper.Map<UserDto[]>(users);            // UserDto[]

Types de destination pris en charge : List<T>, IEnumerable<T>, ICollection<T>, IList<T>, IReadOnlyList<T> et les tableaux T[].

La forme explicite users.Select(_mapper.Map<UserDto>).ToList() reste évidemment valide et strictement équivalente ; utilisez celle que vous préférez.

Intégration dans une classe Repository générique

BaseRepository (avec projection_

public class BaseRepository<TEntity> : IBaseRepository<TEntity>
where TEntity : class
{
private readonly IObjectMapper _mapper;

    /// <summary>
    /// Récupère des entités avec projection vers un DTO
    /// </summary>
    public async Task<List<TDto>> GetAllWithProjectionAsync<TDto>(
        CancellationToken cancellationToken = default)
        where TDto : class
    {
        var entities = await _dbSet.ToListAsync(cancellationToken);
        
        // optimisation : pas de mapping si même type
        if (typeof(TDto) == typeof(TEntity))
            return entities.Cast<TDto>().ToList();
        
        // mapping élément par élément
        return entities
            .Select(e => _mapper.MapWithoutCtor<TEntity, TDto>(e))
            .ToList();
    }

    /// <summary>
    /// Récupère des entités avec filtre et projection vers un DTO
    /// </summary>
    public async Task<List<TDto>> GetWithCustomFilterAsync<TDto>(
        Expression<Func<TEntity, bool>>? filter = null,
        CancellationToken cancellationToken = default)
        where TDto : class
    {
        var query = _dbSet.AsNoTracking();
        
        if (filter != null)
            query = query.Where(filter);
        
        var entities = await query.ToListAsync(cancellationToken);
        
        // 
        if (typeof(TDto) == typeof(TEntity))
            return entities.Cast<TDto>().ToList();
        
        // mapping
        return entities
            .Select(e => _mapper.MapWithoutCtor<TEntity, TDto>(e))
            .ToList();
    }

    /// <summary>
    /// Récupère une seule entité avec projection vers un DTO
    /// </summary>
    public async Task<TDto?> GetSingleWithFilterAsync<TDto>(
        Expression<Func<TEntity, bool>> filter,
        CancellationToken cancellationToken = default)
        where TDto : class
    {
        var entity = await _dbSet
            .FirstOrDefaultAsync(filter, cancellationToken);
        
        if (entity == null)
            return null;
        
        // optimisation
        if (typeof(TDto) == typeof(TEntity))
            return entity as TDto;
        
        // mapping
        return _mapper.MapWithoutCtor<TEntity, TDto>(entity);
    }

    /// <summary>
    /// Récupère des entités paginées avec projection vers un DTO
    /// </summary>
    public async Task<(List<TDto> Data, int Total, int Pages)> GetPaginatedAsync<TDto>(
        int pageNumber = 1,
        int pageSize = 10,
        Expression<Func<TEntity, bool>>? filter = null)
        where TDto : class
    {
        var query = _dbSet.AsNoTracking();
        
        if (filter != null)
            query = query.Where(filter);
        
        var total = await query.CountAsync();
        var pages = (int)Math.Ceiling(total / (double)pageSize);
        
        var entities = await query
            .Skip((pageNumber - 1) * pageSize)
            .Take(pageSize)
            .ToListAsync();
        
        // optimisation
        if (typeof(TDto) == typeof(TEntity))
            return (entities.Cast<TDto>().ToList(), total, pages);
        
        // mapping
        var dtos = entities
            .Select(e => _mapper.MapWithoutCtor<TEntity, TDto>(e))
            .ToList();
        
        return (dtos, total, pages);
    }
}

Mise à jour partielle (PATCH)

// DTOs
public class PatchUserDto
{
public string? FirstName { get; set; }
public string? LastName { get; set; }
public string? Email { get; set; }
public DateTime? BirthDate { get; set; }
}

// Configuration (avec normalisation)
cfg.CreateMap<PatchUserDto, User>()
.SkipNullSourceValues()
.ForMember(d => d.Email, src => src.Email?.ToLower().Trim())
.ForMember(d => d.FirstName, src => src.FirstName?.Trim())
.ForMember(d => d.LastName, src => src.LastName?.Trim())
.Ignore(d => d.Id)
.Ignore(d => d.CreatedAt)
.Ignore(d => d.UpdatedAt);

// Service
public async Task<UserDto> PatchUserAsync(int id, PatchUserDto patch)
{
var user = await _userRepository.GetByIdAsync(id);

    // map du patch (seules les props non-null sont modifiées)
    _mapper.Map(patch, user);
    
    // sauvegarde bdd
    await _userRepository.UpdateAsync(user);
    
    return _mapper.Map<UserDto>(user);
}

// Exemple d'utilisation :
var patch = new PatchUserDto
{
FirstName = "Yoann",  // sera copié
Email = "john.doe@example.com",  // sera copié
LastName = null  // null est ignoré, LastName de l'utilisateur reste inchangé
};
await PatchUserAsync(userId, patch);
Details
NuGet
2026-07-20 16:03:10 +02:00
5
Yoann Lengrand
25 KiB
Assets (2)
Versions (2) View all
1.0.1 2026-07-20
1.0.0 2026-07-20