Fast, diff-aware .NET object mapper. MIT licensed. Minimal dependencies.
| Scenario | DeltaMapper | Mapperly | AutoMapper | Hand-written |
|---|---|---|---|---|
| Flat object (5 props) | 7 ns / 48 B | 7 ns / 48 B | 47 ns / 48 B | 7 ns / 48 B |
| Nested object (2 levels) | 24 ns / 80 B | 21 ns / 120 B | 55 ns / 120 B | 19 ns / 120 B |
| Collection (10 items) | 22 ns / 64 B | 101 ns / 520 B | 183 ns / 712 B | 121 ns / 592 B |
Source-generated
[GenerateMap]benchmarks: flat scenario uses direct calls; nested/collection use theIMappersource-gen path. Numbers are rounded from BENCHMARKS.md. Apple M1 Max, .NET 10.
- Near-zero overhead — source-generated direct calls run at 7 ns, comparable to hand-written code
MappingDiff<T>— map and get a structured change set in one call- Source generator —
[GenerateMap]emits assignment code at build time, zero reflection;[IgnoreMember],[MapMember], and[NullSubstitute]attributes customize maps without runtime Profiles - Full IMapper pipeline — DI, middleware, hooks, EF Core proxy detection, OpenTelemetry tracing
ProjectTo<T>()— translate profile maps into EF Core-compatible SQL projections viaIQueryable
dotnet add package DeltaMapper// 1. Define a profile
public class UserProfile : Profile
{
public UserProfile()
{
CreateMap<User, UserDto>()
.ForMember(d => d.FullName, o => o.MapFrom(s => $"{s.First} {s.Last}"))
.ReverseMap();
}
}
// 2. Build & map
var mapper = MapperConfiguration.Create(cfg => cfg.AddProfile<UserProfile>())
.CreateMapper();
var dto = mapper.Map<User, UserDto>(user);Optional packages
dotnet add package DeltaMapper.SourceGen # compile-time codegen
dotnet add package DeltaMapper.EFCore # EF Core proxy awareness + ProjectTo
dotnet add package DeltaMapper.OpenTelemetry # Activity spansRequires .NET 8+ (ships net8.0, net9.0, and net10.0 assets).
var diff = mapper.Patch(updateDto, existingUser);
if (diff.HasChanges)
await auditLog.RecordAsync(userId, diff.Changes);diff.Changes is IReadOnlyList<PropertyChange> — each entry has PropertyName, From, To, ChangeKind. Nested paths use dot-notation ("Address.City").
Project directly from an IQueryable to a DTO using your existing profile — no separate projection configuration needed. The mapping expression is translated to SQL by EF Core.
dotnet add package DeltaMapper.EFCorevar config = MapperConfiguration.Create(cfg => cfg.AddProfile<OrderProfile>());
var dtos = await dbContext.Orders
.Where(o => o.IsActive)
.ProjectTo<Order, OrderDto>(config)
.ToListAsync();ProjectTo supports convention matching, ForMember/MapFrom, Ignore, NullSubstitute, flattening, nested objects, and collection navigations. BeforeMap, AfterMap, ConstructUsing, and Condition are not supported in projection context.
DeltaMapper automatically flattens nested objects to flat DTOs and unflattens flat DTOs back to nested objects — no configuration needed.
// Source model
public class Order
{
public int Id { get; set; }
public Customer? Customer { get; set; }
}
public class Customer { public string? Name { get; set; } }
// Flat DTO — convention maps Order.Customer.Name → CustomerName
public class OrderFlatDto
{
public int Id { get; set; }
public string? CustomerName { get; set; }
}
// Flattening: nested → flat
var flat = mapper.Map<Order, OrderFlatDto>(order);
// flat.CustomerName == order.Customer.Name
// Unflattening: flat → nested (reverse map or separate CreateMap)
var restored = mapper.Map<OrderFlatDto, Order>(flat);
// restored.Customer.Name == flat.CustomerNameNull intermediate objects in the flattened chain return null without throwing. Multi-level chains (Customer.Address.Zip → CustomerAddressZip) are also resolved automatically.
Register all profiles in an assembly in one call instead of listing them individually.
// Scan by assembly reference
var mapper = MapperConfiguration.Create(cfg =>
cfg.AddProfilesFromAssembly(typeof(UserProfile).Assembly))
.CreateMapper();
// Or scan by any type in the target assembly
var mapper = MapperConfiguration.Create(cfg =>
cfg.AddProfilesFromAssemblyContaining<UserProfile>())
.CreateMapper();Abstract profiles and profiles without a parameterless constructor are silently skipped. Assembly scanning and explicit AddProfile<T>() calls can be combined in the same configuration.
Register a global converter for a type pair once and it applies automatically across all maps.
var mapper = MapperConfiguration.Create(cfg =>
{
cfg.CreateTypeConverter<string, DateTime>(s => DateTime.Parse(s));
cfg.CreateTypeConverter<int, string>(i => i.ToString("D6"));
cfg.AddProfilesFromAssemblyContaining<UserProfile>();
})
.CreateMapper();
// Any map with a string → DateTime property pair now uses the converter
var dto = mapper.Map<OrderRequest, OrderDto>(request);Skip a property mapping when a condition is not met — the destination property keeps its default or existing value.
public class OrderProfile : Profile
{
public OrderProfile()
{
CreateMap<Order, OrderDto>()
.ForMember(d => d.Discount, o => o.Condition(s => s.IsPremiumCustomer))
.ForMember(d => d.Notes, o => o.Condition(s => s.Notes != null));
}
}Conditions work alongside MapFrom and NullSubstitute — the condition is evaluated first, and if false the member option is skipped entirely. Ignore and Condition cannot be combined on the same member; use Condition alone to conditionally skip mapping.
DeltaMapper.SourceGen attributes let you customize compile-time maps directly on the profile class — no runtime Profile or ForMember calls required.
dotnet add package DeltaMapper.SourceGen[GenerateMap(typeof(User), typeof(UserDto))]
[IgnoreMember(typeof(User), typeof(UserDto), nameof(UserDto.InternalId))]
[MapMember(typeof(User), typeof(UserDto), nameof(UserDto.FullName), nameof(User.Name))]
[NullSubstitute(typeof(User), typeof(UserDto), nameof(UserDto.DisplayName), "Anonymous")]
public partial class UserMappingProfile { }| Attribute | Effect |
|---|---|
[IgnoreMember(src, dst, member)] |
Exclude a destination member from the generated map |
[MapMember(src, dst, dstMember, srcMember)] |
Rename: map a source member to a differently named destination member |
[NullSubstitute(src, dst, member, value)] |
Use value when the source member is null |
Each attribute takes explicit (Type sourceType, Type destinationType, ...) so a single profile class can carry attributes for multiple type pairs without ambiguity.
New diagnostics added alongside these attributes:
| Code | Severity | Description |
|---|---|---|
| DM003 | Warning | Attribute references a property that does not exist on the type |
| DM004 | Warning | [MapMember] source and destination property types are incompatible |
DeltaMapper's source generator produces code comparable to hand-written — and on collections, faster than every competitor tested.
| What's being mapped | DeltaMapper | vs Mapperly | vs AutoMapper |
|---|---|---|---|
| Simple object (5 properties) | 7 ns | Comparable (7 ns vs 7 ns) | 7x faster |
| Nested object (parent + child) | 24 ns | Within 15%, 33% less memory | 2x faster |
| Collection (10 items) | 22 ns | 5x faster, 8x less memory | 8x faster |
.NET 10. Times are per single mapping operation. DeltaMapper allocates only the destination object — no framework overhead.
Full benchmark results and methodology
| Guide | Description |
|---|---|
| API Reference | MapperConfiguration, Profile, IMapper, conventions, flattening, assembly scanning, type converters, middleware, DI |
| Source Generator | [GenerateMap], source gen attributes, direct calls, analyzer diagnostics |
| EF Core Integration | Proxy detection, lazy loading safety, ProjectTo |
| OpenTelemetry Tracing | Activity spans, zero-overhead fast path |
| Migration from AutoMapper | Concept mapping table, rename scripts |
- GitHub Issues — bug reports and feature requests
- GitHub Discussions — questions and community support
Contributions are welcome. Please open an issue first to discuss what you'd like to change.
- Fork the repo
- Create a branch (
git checkout -b my-feature) - Make your changes and add tests
- Run
dotnet testto verify all tests pass - Open a pull request
MIT. See LICENSE.