-
Notifications
You must be signed in to change notification settings - Fork 0
Advanced Features
This page covers ZMapper's advanced mapping capabilities: hooks, conditional mapping, reverse mapping, nested objects, and more.
Hooks let you execute custom logic before or after the property mapping step.
Runs before any properties are mapped. The destination object is already created but has default values:
config.CreateMap<InvoiceDto, Invoice>()
.BeforeMap((src, dest) =>
{
// Set audit fields before mapping
dest.CreatedAt = DateTime.UtcNow;
dest.CreatedBy = "system";
})
.ForMember(dest => dest.InvoiceId, opt => opt.MapFrom(src => src.Id));Runs after all properties have been mapped. You can access both the original source and the fully-mapped destination:
config.CreateMap<InvoiceDto, Invoice>()
.ForMember(dest => dest.InvoiceId, opt => opt.MapFrom(src => src.Id))
.AfterMap((src, dest) =>
{
// Compute derived values after mapping
dest.TotalWithTax = dest.Total * 1.21m;
dest.ProcessedBy = "ZMapper";
dest.DisplayName = $"INV-{dest.InvoiceId:D6}";
});config.CreateMap<OrderDto, Order>()
.IgnoreNonExisting()
.BeforeMap((src, dest) =>
{
dest.ReceivedAt = DateTime.UtcNow;
})
.AfterMap((src, dest) =>
{
dest.Status = dest.Items.Count > 0
? OrderStatus.Processing
: OrderStatus.Empty;
});1. dest = new TDestination() // Object created with defaults
2. BeforeMap(src, dest) // Your pre-processing logic
3. dest.Prop1 = src.Prop1 // Property assignments (generated)
dest.Prop2 = src.Prop2
...
4. AfterMap(src, dest) // Your post-processing logic
5. return dest // Fully mapped object returned
Note: Hooks require
MapperConfigurationto be passed when creating the mapper. See Profiles and Dependency Injection for details.
Map properties only when a condition is met using the When() method:
config.CreateMap<ProductDto, Product>()
.ForMember(dest => dest.Price, opt =>
{
opt.MapFrom(src => src.Price);
opt.When(src => src.Price > 0); // Only map positive prices
})
.ForMember(dest => dest.Description, opt =>
{
opt.MapFrom(src => src.Description!);
opt.When(src => src.Description != null); // Only map non-null values
})
.ForMember(dest => dest.DiscountedPrice, opt =>
{
opt.MapFrom(src => src.Price * 0.9m);
opt.When(src => src.IsOnSale); // Only when item is on sale
});When the condition evaluates to false, the destination property keeps its default value (or whatever was set by BeforeMap).
Create bidirectional mappings with .ReverseMap():
config.CreateMap<OrderDto, Order>()
.ForMember(dest => dest.Id, opt => opt.MapFrom(src => src.OrderId))
.ForMember(dest => dest.Customer, opt => opt.MapFrom(src => src.CustomerName))
.ReverseMap();This generates two mappings:
-
OrderDto->Order(explicit, as configured) -
Order->OrderDto(automatically inverted)
The ForMember configurations are automatically inverted:
| Original | Reversed |
|---|---|
dest.Id <- src.OrderId
|
dest.OrderId <- src.Id
|
dest.Customer <- src.CustomerName
|
dest.CustomerName <- src.Customer
|
Ignored members are excluded from the reverse mapping. IgnoreNonExisting() is also propagated to the reverse direction, so non-matching properties are silently skipped in both directions.
ZMapper supports mapping of deep object graphs. Register mappings for each type in the hierarchy:
// Register from leaf types up to the root
config.CreateMap<AddressDto, Address>();
config.CreateMap<OrderItemDto, OrderItem>();
config.CreateMap<CustomerDto, Customer>(); // Customer has Address property
config.CreateMap<OrderDto, Order>(); // Order has Customer + List<OrderItem>
// Map the entire object graph
var order = mapper.Map<OrderDto, Order>(orderDto);
// All nested objects (Customer, Address, OrderItems) are mapped recursivelyNull nested objects are handled safely — the result is null, no NullReferenceException:
var dto = new OrderDto
{
Id = 1,
Customer = null // null nested object
};
var order = mapper.Map<OrderDto, Order>(dto);
// order.Customer == null (no exception)ZMapper handles any nesting depth:
Order
├── Customer
│ ├── BillingAddress
│ └── ShippingAddress
├── Items[]
│ ├── Product
│ └── Warranty
└── StatusHistory[]
└── ChangedBy
As long as each type pair has a registered mapping, the entire graph is mapped.
ZMapper automatically maps properties from the entire inheritance chain:
public abstract class BaseEntity
{
public int Id { get; set; }
public DateTime CreatedAt { get; set; }
public DateTime UpdatedAt { get; set; }
}
public class Product : BaseEntity
{
public string Name { get; set; }
public decimal Price { get; set; }
}
// Id, CreatedAt, UpdatedAt from BaseEntity are mapped automatically
config.CreateMap<ProductDto, Product>();This works with any inheritance depth:
BaseEntity (Id, CreatedAt, UpdatedAt)
└── AuditableEntity (CreatedBy, UpdatedBy)
└── Product (Name, Price)
All properties from BaseEntity, AuditableEntity, and Product are included.
For every registered mapping, ZMapper generates a .ToXxx() extension method:
// Given: config.CreateMap<UserDto, User>()
// Generated: public static User ToUser(this UserDto source)
var user = dto.ToUser(); // Clean, discoverable, inlined by JITThese methods are marked with [MethodImpl(MethodImplOptions.AggressiveInlining)] and are the fastest way to map a single object.
| Scenario | Use |
|---|---|
| Simple single-object mapping |
dto.ToUser() extension method |
| Need dependency injection |
IMapper interface |
| Batch/collection mapping |
mapper.MapArray() / mapper.MapList()
|
| Map to existing instance | mapper.Map(source, destination) |
Update an existing object instead of creating a new one:
// Fetch from database
var existingUser = await dbContext.Users.FindAsync(id);
// Update properties from DTO
mapper.Map<UserDto, User>(dto, existingUser);
// existingUser now has updated properties
await dbContext.SaveChangesAsync();This is especially useful for update operations where you need to preserve navigation properties or tracked entities.
Implement ITypeConverter<TSource, TDestination> for complete control over the conversion:
public class DateTimeToStringConverter : ITypeConverter<DateTime, string>
{
public string Convert(DateTime source)
{
return source.ToString("yyyy-MM-dd");
}
}For individual member conversion, implement IMemberConverter<TSource, TSourceMember, TDestMember>:
public class UpperCaseConverter : IMemberConverter<UserDto, string, string>
{
public string Convert(UserDto source, string sourceMember)
{
return sourceMember.ToUpperInvariant();
}
}
// Usage
config.CreateMap<UserDto, User>()
.ForMember(dest => dest.Name, opt => opt.ConvertUsing<UpperCaseConverter>());All features can be combined in a single mapping configuration:
config.CreateMap<OrderDto, Order>()
// Custom property mappings
.ForMember(dest => dest.OrderNumber, opt => opt.MapFrom(src => src.Number))
.ForMember(dest => dest.CustomerName, opt => opt.MapFrom(src => src.Client.Name))
// Conditional mapping
.ForMember(dest => dest.Discount, opt =>
{
opt.MapFrom(src => src.DiscountAmount);
opt.When(src => src.HasDiscount);
})
// Ignored properties (set by hooks)
.ForMember(dest => dest.ProcessedAt, opt => opt.Ignore())
// Suppress warnings for unmapped props
.IgnoreNonExisting()
// Pre-processing
.BeforeMap((src, dest) => dest.ReceivedAt = DateTime.UtcNow)
// Post-processing
.AfterMap((src, dest) =>
{
dest.ProcessedAt = DateTime.UtcNow;
dest.TotalWithTax = dest.Total * 1.21m;
})
// Bidirectional
.ReverseMap();- Collections and Batch Mapping — Batch mapping with arrays, lists, Span
- API Reference — Complete interface and method reference
- Performance — Benchmarks and optimization details
ZMapper v1.2.1 | GitHub Repository | Report an Issue | MIT License