NovelyMapper est un mapper d'objets leger et fluent pour .NET 8.0, concu pour faciliter la transformation d'objets entre differents types. Il s'inspire d'AutoMapper mais reste 100% gratuit et open-source.
- Compilation d'Expression Trees pour des performances optimales
- Cache thread-safe via
ConcurrentDictionary - API fluent avec profils,
ForMember,Ignore,ReverseMap,ConvertUsing, etc. - Support des
recordtypes (constructeurs parametres) - Mapping d'objets imbriques et de collections
- Conversion automatique des types nullable (
T?→T,T→T?) - Resolution automatique des mappings imbriques dans
MapFrom - Gestion des references circulaires (pas de
StackOverflowException) - Inference de collection dans
Map<TTarget>(object)(ex:Map<IEnumerable<Dto>>(list)) - Projection
IQueryable(ProjectTo) pour Entity Framework - Injection de dependances avec profils multiples
- Installation
- Utilisation de base
- Profils
- Injection de dependances
- Fonctionnalites avancees
- Ignore
- MapFrom
- Condition (MapWhen)
- NullSubstitute
- ConvertUsing (mapping complet)
- ConvertUsing (par membre)
- ReverseMap
- BeforeMap / AfterMap
- Map vers un objet existant
- Mapping de records
- Mapping d'objets imbriques
- Mapping de collections imbriquees
- Conversion automatique des types nullable
- Resolution automatique dans MapFrom
- References circulaires
- Mapping de collection via Map<TTarget>
- ProjectTo (IQueryable)
- Validation de la configuration
- Options globales
- API Reference
Via la CLI .NET :
dotnet add package NovelyMapperVia le Package Manager Console (Visual Studio) :
Install-Package NovelyMapperPrerequis : .NET 8.0 ou superieur.
using Novely.Mapper;
// Creer un mapper et enregistrer un mapping
var mapper = new NovelyMapper();
mapper.CreateMap<Source, Target>();
// Mapper un objet
var source = new Source { Id = 1, Name = "Alice" };
var target = mapper.Map<Source, Target>(source);
// target.Id == 1, target.Name == "Alice"Les proprietes sont matchees automatiquement par nom. Si les noms correspondent, aucune configuration supplementaire n'est necessaire.
var sources = new List<Source>
{
new() { Id = 1, Name = "Alice" },
new() { Id = 2, Name = "Bob" }
};
IEnumerable<Target> targets = mapper.Map<Source, Target>(sources);Le mapping de collections est lazy (IEnumerable<T>) : les objets sont mappes un par un lors de l'iteration.
Lorsque les noms de proprietes different, utilisez ForMember pour configurer le mapping :
mapper.CreateMap<Source, Target>()
.ForMember(dest => dest.Nom, opt => opt.MapFrom(src => src.Name));L'API MemberOptions offre un ensemble complet d'options chainables :
mapper.CreateMap<Source, Target>()
.ForMember(dest => dest.X, opt => opt.Ignore())
.ForMember(dest => dest.Y, opt => opt.MapFrom(src => src.Z))
.ForMember(dest => dest.Status, opt => opt
.MapFrom(src => src.IsActive ? "Actif" : "Inactif")
.MapWhen(src => src.IsEnabled))
.ForMember(dest => dest.Label, opt => opt
.MapFrom(src => src.Label)
.NullSubstitute("N/A"));Un profil regroupe les configurations de mapping. Chaque profil recoit l'instance du mapper via son constructeur :
public class AppMapperProfile : NovelyMapperProfile
{
public AppMapperProfile(NovelyMapper mapper) : base(mapper)
{
CreateMap<User, UserDto>();
CreateMap<Order, OrderDto>()
.ForMember(d => d.ClientName, opt => opt.MapFrom(s => s.Client.Name));
CreateMap<Product, ProductDto>()
.ForMember(d => d.InternalCode, opt => opt.Ignore());
}
}Vous pouvez separer les mappings en plusieurs profils pour une meilleure organisation :
public class UserProfile : NovelyMapperProfile
{
public UserProfile(NovelyMapper mapper) : base(mapper)
{
CreateMap<User, UserDto>();
CreateMap<User, UserSummaryDto>();
}
}
public class OrderProfile : NovelyMapperProfile
{
public OrderProfile(NovelyMapper mapper) : base(mapper)
{
CreateMap<Order, OrderDto>();
}
}Enregistrement avec plusieurs profils :
// Par types explicites
builder.Services.UseNovelyMapper(typeof(UserProfile), typeof(OrderProfile));Detecte automatiquement tous les profils d'un ou plusieurs assemblies :
builder.Services.UseNovelyMapper(typeof(Program).Assembly);Tous les types non-abstraits heritant de NovelyMapperProfile sont decouverts et instancies automatiquement.
Enregistrez le mapper en singleton dans votre conteneur DI :
// Avec un profil unique
builder.Services.UseNovelyMapper<AppMapperProfile>();
// Sans profil (mapper vide, pour ajouter des mappings manuellement)
builder.Services.UseNovelyMapper();
// Avec plusieurs profils
builder.Services.UseNovelyMapper(typeof(UserProfile), typeof(OrderProfile));
// Avec scan d'assemblies
builder.Services.UseNovelyMapper(typeof(Program).Assembly);Utilisez ensuite IMapper via injection :
public class UserService
{
private readonly IMapper _mapper;
public UserService(IMapper mapper)
{
_mapper = mapper;
}
public UserDto GetUser(int id)
{
var user = _repository.GetById(id);
return _mapper.Map<User, UserDto>(user);
}
}Note :
INovelyMapperest toujours disponible pour retrocompatibilite (il herite deIMapper). Les deux interfaces sont enregistrees dans le conteneur DI.
Isolation : chaque appel a
UseNovelyMappercree une instance independante du mapper. DeuxServiceProvideravec des profils differents ne s'interferent pas.
Exclut une propriete du mapping. La propriete conserve sa valeur par defaut (ou sa valeur existante lors d'un Map vers objet existant).
mapper.CreateMap<Source, Target>()
.ForMember(d => d.Secret, opt => opt.Ignore())
.ForMember(d => d.InternalId, opt => opt.Ignore());Specifie une expression source personnalisee pour une propriete cible :
mapper.CreateMap<User, UserDto>()
.ForMember(d => d.FullName, opt => opt.MapFrom(s => $"{s.FirstName} {s.LastName}"))
.ForMember(d => d.Age, opt => opt.MapFrom(s => DateTime.Now.Year - s.BirthYear));Applique le mapping uniquement si une condition est remplie. Sinon, la propriete conserve sa valeur par defaut (default(T)) ou sa valeur existante (lors d'un Map vers objet existant).
mapper.CreateMap<Source, Target>()
.ForMember(d => d.Status, opt => opt
.MapFrom(s => s.Status)
.MapWhen(s => s.IsEnabled));Comportement :
- Map vers nouvel objet : si la condition est fausse, la propriete vaut
default(T) - Map vers objet existant : si la condition est fausse, la propriete conserve sa valeur d'origine
Fournit une valeur de remplacement lorsque la valeur source est null :
mapper.CreateMap<Source, Target>()
.ForMember(d => d.Label, opt => opt.NullSubstitute("N/A"))
.ForMember(d => d.Description, opt => opt
.MapFrom(s => s.Description)
.NullSubstitute("Aucune description"));Remplace entierement la logique de mapping par un convertisseur personnalise. Aucun Expression Tree n'est genere :
mapper.CreateMap<Source, Target>()
.ConvertUsing(src => new Target
{
Id = src.Id,
FullName = $"{src.FirstName} {src.LastName}",
Score = CalculateScore(src)
});Convertisseur personnalise pour une propriete specifique :
mapper.CreateMap<Source, Target>()
.ForMember(d => d.Length, opt => opt.ConvertUsing(s => s.Value?.Length ?? 0));Cree automatiquement le mapping inverse. Les MapFrom simples (expressions MemberExpression) sont inverses. Les mappings par convention sont naturellement bidirectionnels.
mapper.CreateMap<Source, Target>()
.ForMember(d => d.Nom, opt => opt.MapFrom(s => s.Name))
.ReverseMap();
// Forward : Source → Target
var target = mapper.Map<Source, Target>(source);
// Reverse : Target → Source (automatiquement configure)
var source = mapper.Map<Target, Source>(target);ReverseMap() retourne le config du mapping inverse, permettant de le configurer davantage :
mapper.CreateMap<Source, Target>()
.ForMember(d => d.Nom, opt => opt.MapFrom(s => s.Name))
.ReverseMap()
.ForMember(d => d.Name, opt => opt.MapFrom(s => s.Nom)); // config supplementaire du reverseExecutez des actions avant ou apres le mapping :
mapper.CreateMap<Source, Target>()
.BeforeMap((src, dest) =>
{
// dest est vide a ce stade (constructeur par defaut)
Log.Info($"Mapping de {src.Id}");
})
.AfterMap((src, dest) =>
{
// dest est rempli avec les valeurs mappees
dest.FullLabel = $"{dest.Id}-{dest.Name}";
});Note :
BeforeMapnecessite un type cible avec constructeur sans parametre (lesrecordne sont pas supportes avecBeforeMap).
Met a jour les proprietes d'un objet existant au lieu d'en creer un nouveau :
mapper.CreateMap<Source, Target>();
var existing = new Target { Id = 0, Name = "ancien", Extra = "a conserver" };
mapper.Map(source, existing);
// existing.Id et existing.Name sont mis a jour
// existing.Extra est conserve (pas de propriete correspondante dans Source)Les proprietes ignorees (Ignore()) conservent leur valeur d'origine. Les conditions (MapWhen) preservent la valeur existante si la condition est fausse.
NovelyMapper detecte automatiquement les types sans constructeur sans parametre et resout le meilleur constructeur :
public record PersonDto(int Id, string Name);
mapper.CreateMap<Person, PersonDto>();
var dto = mapper.Map<Person, PersonDto>(person);
// Le constructeur PersonDto(int, string) est utilise automatiquementLes parametres du constructeur sont resolus par correspondance de nom (case-insensitive) avec les proprietes source. Les parametres avec valeur par defaut ou ignores utilisent default(T).
public record PersonWithEmail(int Id, string Name, string? Email = null);
mapper.CreateMap<Person, PersonWithEmail>()
.ForMember(d => d.Email, opt => opt.Ignore());
// Email recoit null (parametre ignore dans le constructeur)Si un mapping est enregistre pour les types imbriques, NovelyMapper les mappe automatiquement par convention de nom :
// Modeles
public class Parent { public Child Child { get; set; } }
public class ParentDto { public ChildDto Child { get; set; } }
// Enregistrer les deux mappings
mapper.CreateMap<Child, ChildDto>();
mapper.CreateMap<Parent, ParentDto>();
var dto = mapper.Map<Parent, ParentDto>(parent);
// dto.Child est automatiquement mappe via le mapping Child → ChildDtoLes objets imbriques null restent null dans le resultat.
Les collections de types complexes sont automatiquement mappees si le mapping element est enregistre :
public class Parent { public List<Child> Children { get; set; } }
public class ParentDto { public List<ChildDto> Children { get; set; } }
mapper.CreateMap<Child, ChildDto>();
mapper.CreateMap<Parent, ParentDto>();
// parent.Children (List<Child>) → dto.Children (List<ChildDto>)Types de collections supportes : List<T>, IEnumerable<T>, ICollection<T>, IList<T>, T[].
NovelyMapper convertit automatiquement entre types nullable et non-nullable quand les proprietes ont le meme nom :
public class Source { public int? Value { get; set; } }
public class Target { public int Value { get; set; } }
mapper.CreateMap<Source, Target>();
var result = mapper.Map<Source, Target>(new Source { Value = 42 });
// result.Value == 42
var result2 = mapper.Map<Source, Target>(new Source { Value = null });
// result2.Value == 0 (default)Conversions supportees :
T?→T: utilise.GetValueOrDefault()(retournedefault(T)si null)T→T?: conversion impliciteNullable<T>→Nullable<U>: conversion quand les types sous-jacents sont compatibles
Fonctionne pour les classes, les records (constructeurs parametres) et Map(source, target).
MapFrom resout automatiquement les mappings imbriques quand le type retourne differe du type cible :
mapper.CreateMap<Inner, InnerDto>();
mapper.CreateMap<Source, Target>()
.ForMember(d => d.Info, opt => opt.MapFrom(s => s.Data));
// Data est de type Inner, Info est de type InnerDto
// → le mapping Inner → InnerDto est applique automatiquementCela fonctionne aussi pour les collections et les types nullable, sans ConvertUsing explicite :
// Collection : List<Inner> → List<InnerDto> via MapFrom
mapper.CreateMap<Source, Target>()
.ForMember(d => d.Items, opt => opt.MapFrom(s => s.OriginalItems));
// Nullable : int? → int via MapFrom
mapper.CreateMap<Source, Target>()
.ForMember(d => d.Value, opt => opt.MapFrom(s => s.NullableValue));NovelyMapper detecte et gere les references circulaires automatiquement, sans configuration :
public class Customer
{
public int Id { get; set; }
public Supplier? Supplier { get; set; }
}
public class Supplier
{
public int Id { get; set; }
public Customer? Customer { get; set; } // reference circulaire !
}
mapper.CreateMap<Customer, CustomerDto>();
mapper.CreateMap<Supplier, SupplierDto>();
// Aucun StackOverflowException
var dto = mapper.Map<Customer, CustomerDto>(customer);Comportement :
- A la compilation : quand un cycle est detecte dans les types (ex:
Customer → Supplier → Customer), le mapper emet un appel runtimeMap<S,T>()au lieu de recurser infiniment dans l'Expression Tree - Au runtime : quand un meme objet source est rencontre une seconde fois dans le graphe (cycle dans les donnees), la back-reference retourne
null - Les arbres (ex:
TreeNodeavecChildren: List<TreeNode>) fonctionnent normalement tant qu'il n'y a pas de cycle dans les donnees
Map<TTarget>(object) detecte automatiquement les collections et infere le mapping element par element :
mapper.CreateMap<Customer, CustomerDto>();
var customers = db.Customers.ToList();
// Infere automatiquement le mapping Customer → CustomerDto
var dtos = mapper.Map<IEnumerable<CustomerDto>>(customers);
var list = mapper.Map<List<CustomerDto>>(customers);
var array = mapper.Map<CustomerDto[]>(customers);Pas besoin d'enregistrer un mapping List<Customer> → IEnumerable<CustomerDto>. Le mapper detecte que TTarget est une collection, extrait les types elements, et route vers le mapping enregistre.
Projette un IQueryable vers un type cible en generant une Expression<Func<S,T>> traduisible en SQL par Entity Framework :
using Novely.Mapper;
// Avec IQueryable<TSource> (generique)
IQueryable<UserDto> dtos = dbContext.Users
.Where(u => u.IsActive)
.ProjectTo<User, UserDto>(mapper);
// Avec IQueryable (non-generique)
IQueryable<UserDto> dtos = ((IQueryable)dbContext.Users)
.ProjectTo<UserDto>(mapper);Vous pouvez aussi recuperer l'expression brute :
Expression<Func<User, UserDto>> expr = mapper.GetProjectionExpression<User, UserDto>();Limitations ProjectTo : seules les expressions pures sont supportees.
BeforeMap/AfterMap,ConvertUsing(delegate), etMapWhen(delegate) ne sont pas traduits en SQL. UtilisezMapFromavec des expressions pour les mappings personnalises dansProjectTo.
Verifiez que toutes les proprietes cibles ont une source :
mapper.CreateMap<Source, Target>()
.ForMember(d => d.Unmapped, opt => opt.Ignore());
// Leve NovelyMapperValidationException si des proprietes ne sont pas mappees
mapper.AssertConfigurationIsValid();Une propriete est consideree comme valide si elle est :
- Matchee par convention (meme nom dans le type source)
- Configuree via
MapFromouConvertUsing - Ignoree via
Ignore() - Couverte par un parametre du constructeur (records)
- Couverte par un mapping imbrique enregistre
try
{
mapper.AssertConfigurationIsValid();
}
catch (NovelyMapperValidationException ex)
{
foreach (var error in ex.Errors)
Console.WriteLine(error);
}Configurez le comportement global du mapper :
builder.Services.UseNovelyMapper<AppProfile>(options =>
{
options.MissingPropertyBehavior = MissingPropertyBehavior.Throw;
});| Option | Valeurs | Description |
|---|---|---|
MissingPropertyBehavior |
Silent (defaut), Throw |
Comportement quand une propriete cible n'a pas de source |
Interface principale pour l'injection de dependances.
| Methode | Description |
|---|---|
CreateMap<TSource, TTarget>() |
Enregistre un mapping et retourne la config |
Map<TTarget>(object source) |
Mappe un objet (type infere au runtime). Supporte les collections |
Map<TSource, TTarget>(source) |
Mappe un objet vers un nouveau TTarget |
Map<TSource, TTarget>(source, target) |
Met a jour un TTarget existant |
Map<TSource, TTarget>(IEnumerable) |
Mappe une collection (lazy) |
GetProjectionExpression<S,T>() |
Retourne l'expression pour ProjectTo |
AssertConfigurationIsValid() |
Valide tous les mappings enregistres |
INovelyMapperherite deIMapperet reste disponible pour retrocompatibilite.
| Methode | Description |
|---|---|
ForMember(selector, Action<MemberOptions>) |
Configure une propriete |
ReverseMap() |
Cree le mapping inverse |
BeforeMap(Action<S,T>) |
Action avant le mapping |
AfterMap(Action<S,T>) |
Action apres le mapping |
ConvertUsing(Func<S,T>) |
Convertisseur complet |
| Methode | Description |
|---|---|
MapFrom(Expression<Func<S, object>>) |
Expression source |
Ignore() |
Ignore cette propriete |
MapWhen(Func<S, bool>) |
Condition d'application |
NullSubstitute(object) |
Valeur si source null |
ConvertUsing(Func<S, object>) |
Convertisseur par membre |
| Methode | Description |
|---|---|
services.UseNovelyMapper<TProfile>() |
Enregistre avec un profil |
services.UseNovelyMapper(params Type[]) |
Enregistre avec plusieurs profils |
services.UseNovelyMapper(params Assembly[]) |
Scan d'assemblies |
query.ProjectTo<S,T>(mapper) |
Projection IQueryable generique |
query.ProjectTo<T>(mapper) |
Projection IQueryable non-generique |
Ce projet est distribue sous licence open-source. Voir le fichier LICENSE pour plus de details.