Skip to content

Advanced Features

Martin Zahálka edited this page Feb 18, 2026 · 2 revisions

Advanced Features

This page covers ZMapper's advanced mapping capabilities: hooks, conditional mapping, reverse mapping, nested objects, and more.

BeforeMap / AfterMap Hooks

Hooks let you execute custom logic before or after the property mapping step.

BeforeMap

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));

AfterMap

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}";
    });

Combining BeforeMap and AfterMap

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;
    });

Hook Execution Order

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 MapperConfiguration to be passed when creating the mapper. See Profiles and Dependency Injection for details.

Conditional Mapping

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).

Reverse Mapping

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:

  1. OrderDto -> Order (explicit, as configured)
  2. Order -> OrderDto (automatically inverted)

How Reverse Mapping Works

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.

Nested Object Mapping

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 recursively

Null Safety

Null 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)

Deep Nesting

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.

Inheritance Support

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.

Extension Methods

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 JIT

These methods are marked with [MethodImpl(MethodImplOptions.AggressiveInlining)] and are the fastest way to map a single object.

When to Use Extension Methods vs IMapper

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)

Map to Existing Instance

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.

Custom Type Converters

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");
    }
}

Member Converters

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>());

Combining Features

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();

Next Steps

Clone this wiki locally