HyperMapper.Core
12.2.1
dotnet add package HyperMapper.Core --version 12.2.1
NuGet\Install-Package HyperMapper.Core -Version 12.2.1
<PackageReference Include="HyperMapper.Core" Version="12.2.1" />
<PackageVersion Include="HyperMapper.Core" Version="12.2.1" />
<PackageReference Include="HyperMapper.Core" />
paket add HyperMapper.Core --version 12.2.1
#r "nuget: HyperMapper.Core, 12.2.1"
#:package HyperMapper.Core@12.2.1
#addin nuget:?package=HyperMapper.Core&version=12.2.1
#tool nuget:?package=HyperMapper.Core&version=12.2.1
HyperMapper v12
100% AutoMapper API Compatible - Drop-in replacement with Source Generator support for maximum performance.
HyperMapper is a high-performance object mapping library designed to be fully compatible with AutoMapper's API while providing significantly better performance through Source Generators. It was created specifically to be a drop-in replacement for AutoMapper, requiring only a namespace change to migrate.
Architecture Overview
┌─────────────────────────────────────────────────────────────────────────────┐
│ HyperMapper v12 Architecture │
└─────────────────────────────────────────────────────────────────────────────┘
┌──────────────────────┐
│ Your Application │
│ using HyperMapper; │
└──────────┬───────────┘
│
├─────────────────────┬─────────────────────┐
│ │ │
▼ ▼ ▼
┌──────────────────┐ ┌──────────────────┐ ┌──────────────────┐
│ IMapper API │ │ MapperConfig │ │ Profile API │
│ │ │ │ │ │
│ • Map<T>() │ │ • AddProfile<>() │ │ • CreateMap<>() │
│ • Map<S,D>() │ │ • AddMaps() │ │ • ForMember() │
│ • Map(s, d) │ │ • Validate() │ │ • ReverseMap() │
└────────┬─────────┘ └────────┬─────────┘ └────────┬─────────┘
│ │ │
└─────────────────────┴─────────────────────┘
│
┌─────────────▼─────────────┐
│ Dual Execution Engine │
└─────────────┬─────────────┘
│
┌─────────────────────┴─────────────────────┐
│ │
▼ ▼
┌───────────────────────┐ ┌───────────────────────┐
│ RUNTIME MODE │ │ CODEGEN MODE │
│ (Dynamic) │ │ (Static) │
├───────────────────────┤ ├───────────────────────┤
│ │ │ │
│ Configuration Phase: │ │ Compile-Time Phase: │
│ • Profile Analysis │ │ • Roslyn Analyzer │
│ • TypeMap Building │ │ • Syntax Analysis │
│ │ │ • Code Generation │
│ Compilation Phase: │ │ │
│ • Expression Trees │ │ Generated Output: │
│ • IL Compilation │ │ • .g.cs files │
│ • Generic Cache │ │ • Static methods │
│ │ │ • Registry class │
│ Runtime Execution: │ │ │
│ • Delegate invoke │ │ Runtime Execution: │
│ • ~120ns per map │ │ • Direct method call │
│ • 1-5ms warm-up │ │ • ~44ns per map │
│ │ │ • Zero warm-up │
└───────────────────────┘ └───────────────────────┘
│ │
│ │
└────────────────┬──────────────────────────┘
│
▼
┌──────────────────────┐
│ Mapped Objects │
│ (Your DTOs/Models) │
└──────────────────────┘
┌─────────────────────────────────────────────────────────────────────────────┐
│ Performance Comparison │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ Manual Code: ██ 22ns │
│ HyperMapper CG: ███ 32ns ← 1.4x slower than manual ✅ │
│ HyperMapper RT: ███████████████ 147ns ← 6.7x slower than manual │
│ AutoMapper: ████████████████ 170ns ← 7.7x slower than manual │
│ │
│ 🎉 CodeGen is now 4.6x FASTER than Runtime! │
│ │
└─────────────────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────────────────┐
│ Key Differentiators │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ ✅ 100% AutoMapper API Compatible → Drop-in replacement │
│ ⚡ Dual-Mode Architecture → Runtime OR CodeGen │
│ 🚀 Source Generator Support → Compile-time code generation │
│ 🎯 Zero Reflection (CodeGen) → AOT/Native ready │
│ 🔥 Zero Warm-up (CodeGen) → Fast from first call │
│ 📦 .NET 8+ Compatible → Modern .NET support │
│ │
└─────────────────────────────────────────────────────────────────────────────┘
Key Features
- ✅ 100% AutoMapper API Compatible - Same interfaces, same methods, same behavior
- ⚡ 1.2x Faster than AutoMapper with Runtime Mode
- 🚀 Up to 5.4x Faster with CodeGen Mode (Source Generators)
- 🔥 4.6x Faster CodeGen vs Runtime - Use the fast path with typed API
- 🛡️ Compile-Time Safety - Catch mapping errors before runtime
- 🎯 AOT/Native Ready - Full Native AOT compilation support
- 📦 .NET 8+ Compatible - Works with .NET 8, 9, 10, and future versions
- 🧪 Extensively Tested - 856 tests with 82.4% code coverage
Performance Comparison
Environment: macOS, Apple M2 Pro, .NET 8.0.23, BenchmarkDotNet v0.14.0
| Scenario | Manual | HyperMapper Runtime | HyperMapper CodeGen | AutoMapper | Best Choice |
|---|---|---|---|---|---|
| Simple Mapping | 22 ns | 147 ns | 32 ns ✅ | 170 ns | CodeGen (1.4x) |
| Complex Object (Full) | 161 ns | 295 ns | 149 ns ✅ | 370 ns | CodeGen (-7%) |
| Complex Object (Sparse) | 86 ns | 200 ns | 85 ns ✅ | 246 ns | CodeGen (-1%) |
| Collection (1000 items) | 18,914 ns | 34,548 ns | 29,935 ns ✅ | 37,402 ns | CodeGen (1.6x) |
| Flattening | 51 ns | 161 ns | 75 ns ✅ | 202 ns | CodeGen (2.1x) |
| Deep Nesting (10 levels) | 230 ns | 343 ns | 255 ns ✅ | 365 ns | CodeGen (1.3x) |
Key Performance Insights
- HyperMapper CodeGen is FASTER in ALL scenarios after API optimization
- HyperMapper CodeGen is 4.6x faster than Runtime on simple mappings (32ns vs 147ns)
- HyperMapper CodeGen allocates 30% less memory than manual (184B vs 264B on complex objects)
- HyperMapper CodeGen is 2.5x faster than AutoMapper on complex objects (149ns vs 370ns)
- HyperMapper CodeGen is 2.7x faster than Runtime on flattening (75ns vs 161ns)
- Always use CodeGen Mode for production - best performance across all scenarios
Table of Contents
- Installation
- Quick Start
- Two Usage Modes
- Migration from AutoMapper
- Entity Framework & Lazy Loading Considerations
- Runtime Mode Documentation
- CodeGen Mode Documentation
- API Reference
- Advanced Features
- Examples
- Testing and Coverage
- Benchmarks
- Architecture
Installation
Runtime Mode Setup
Add the project reference to your .csproj:
<ItemGroup>
<ProjectReference Include="../HyperMapper/src/HyperMapper/HyperMapper.csproj" />
</ItemGroup>
This is all you need for Runtime Mode (AutoMapper-compatible API).
CodeGen Mode Setup (Recommended for Production)
To enable compile-time code generation with Source Generators:
1. Add HyperMapper reference (runtime library):
<ItemGroup>
<ProjectReference Include="../HyperMapper/src/HyperMapper/HyperMapper.csproj" />
</ItemGroup>
2. Add Source Generator (analyzer reference):
<ItemGroup>
<ProjectReference Include="../HyperMapper.SourceGenerator/HyperMapper.SourceGenerator.csproj"
OutputItemType="Analyzer"
ReferenceOutputAssembly="false" />
</ItemGroup>
3. Optional: Enable generated file inspection (for debugging):
<PropertyGroup>
<EmitCompilerGeneratedFiles>true</EmitCompilerGeneratedFiles>
<CompilerGeneratedFilesOutputPath>$(BaseIntermediateOutputPath)\Generated</CompilerGeneratedFilesOutputPath>
</PropertyGroup>
4. Build your project - .g.cs files will be generated automatically in obj/Generated/
5. Use generated mappers (after first build):
using HyperMapper.Generated;
var config = new MapperConfiguration(cfg => {
cfg.AddProfile<YourProfile>();
});
HyperMapperGeneratedRegistry.Initialize(config); // ← Register generated mappers
var mapper = config.CreateMapper();
See examples/HyperMapper.Examples.CodeGen for a complete working example.
Dependency Injection Registration
using HyperMapper;
using HyperMapper.Generated; // For CodeGen Mode
services.AddSingleton<IMapper>(sp =>
{
var loggerFactory = sp.GetService<ILoggerFactory>();
var config = new MapperConfiguration(cfg =>
{
cfg.AddProfile<MyMappingProfile>();
}, loggerFactory);
// Optional: Register compile-time generated mappers for maximum performance
HyperMapperGeneratedRegistry.Initialize(config);
config.AssertConfigurationIsValid();
return config.CreateMapper();
});
Quick Start
Runtime Mode (AutoMapper-Compatible)
Perfect for rapid development and 100% AutoMapper compatibility:
using HyperMapper;
// 1. Define your classes
public class User
{
public int Id { get; set; }
public string FirstName { get; set; }
public string LastName { get; set; }
public DateTime BirthDate { get; set; }
}
public class UserDto
{
public int Id { get; set; }
public string FullName { get; set; }
public int Age { get; set; }
}
// 2. Create a Profile
public class UserProfile : Profile
{
public UserProfile()
{
CreateMap<User, UserDto>()
.ForMember(d => d.FullName, opt => opt.MapFrom(s =>
$"{s.FirstName} {s.LastName}"))
.ForMember(d => d.Age, opt => opt.MapFrom(s =>
CalculateAge(s.BirthDate)));
}
private static int CalculateAge(DateTime birthDate)
{
var today = DateTime.Today;
var age = today.Year - birthDate.Year;
if (birthDate.Date > today.AddYears(-age)) age--;
return age;
}
}
// 3. Configure and use
var config = new MapperConfiguration(cfg =>
{
cfg.AddProfile<UserProfile>();
});
var mapper = config.CreateMapper();
var user = new User
{
Id = 1,
FirstName = "John",
LastName = "Doe",
BirthDate = new DateTime(1990, 5, 15)
};
var userDto = mapper.Map<UserDto>(user);
// userDto.FullName = "John Doe"
// userDto.Age = 34 (calculated)
Performance: ~147ns per mapping
CodeGen Mode (Source Generators)
For production applications requiring maximum performance:
using HyperMapper;
using HyperMapper.Generated;
// 1. Same Profile class as Runtime Mode
public class UserProfile : Profile
{
public UserProfile()
{
CreateMap<User, UserDto>()
.ForMember(d => d.FullName, opt => opt.MapFrom(s =>
$"{s.FirstName} {s.LastName}"))
.ForMember(d => d.Age, opt => opt.MapFrom(s =>
CalculateAge(s.BirthDate)));
}
}
// 2. Configure with generated mappers
var config = new MapperConfiguration(cfg =>
{
cfg.AddProfile<UserProfile>();
});
// CRITICAL: Register generated mappers
HyperMapperGeneratedRegistry.Initialize(config);
var mapper = config.CreateMapper();
// 3. Usage is identical
var userDto = mapper.Map<User, UserDto>(user); // ✅ Use typed API for best performance
Performance: ~32ns per mapping (4.6x faster than Runtime!)
What happens at compile-time:
- Source Generator analyzes
UserProfile - Generates optimized C# mapping methods
- Creates
HyperMapperGeneratedRegistrywith all mappers - Zero reflection, zero warm-up time
Two Usage Modes
HyperMapper offers two complementary approaches to object mapping:
Runtime Mode
AutoMapper-compatible dynamic configuration
- Uses the familiar AutoMapper API
- Configuration happens at application startup
- Execution plans compiled to IL at runtime
- Full support for dynamic scenarios
- Performance: ~147ns per simple mapping
Best for:
- Rapid prototyping and development
- Dynamic type resolution
- Complex custom converters with runtime dependencies
- Migration from AutoMapper (zero code changes)
CodeGen Mode
Compile-time code generation via Source Generators
- Analyzes
Profileclasses at compile-time - Generates optimized C# mapping methods
- Zero reflection, zero runtime overhead
- Compile-time error detection
- Performance: ~32ns per simple mapping (4.6x faster than Runtime!)
Best for:
- Production applications (recommended)
- Performance-critical paths
- AOT/Native compilation scenarios
- Early error detection
Runtime vs CodeGen Comparison
| Aspect | Runtime Mode | CodeGen Mode |
|---|---|---|
| Configuration | MapperConfiguration at runtime | Profile classes analyzed at compile-time |
| Performance | Fast (~147ns) | Ultra-fast (~32ns, 4.6x faster!) |
| First Call | ~1-5ms warm-up | ~32ns (no warm-up) |
| Error Detection | Runtime exceptions | Compile-time errors |
| Debugging | Expression Trees | Plain C# code |
| AOT/Native | Partial support | Full support |
| Dynamic Types | Full support | Limited (open generics only) |
| BeforeMap/AfterMap | ✅ Supported | ❌ Not supported |
| Custom Converters | ✅ Full support | ⚠️ Limited support |
| Migration Effort | Zero (100% AutoMapper compatible) | Zero (same Profile classes) |
| Use Case | Development, prototyping, dynamic | Production, performance-critical |
When to Use Each Mode
Use Runtime Mode When:
- Rapid Development - Prototyping and iteration speed is priority
- Dynamic Type Resolution - Types determined at runtime
- Complex Converters - Custom
ITypeConverterwith runtime dependencies - Lifecycle Hooks - Need
BeforeMap/AfterMapfunctionality - Migration Phase - Migrating from AutoMapper with zero changes
Use CodeGen Mode When:
- Production Applications - Strongly recommended for all production deployments
- Performance Critical - Up to 4.6x faster than Runtime Mode
- AOT/Native Compilation - Using Native AOT or trimming
- Compile-Time Safety - Want errors caught at build time
- All Mapping Scenarios - CodeGen is now faster in ALL scenarios
💡 Pro Tip: Always use CodeGen Mode in production! With the correct typed API (Map<TSource, TDest>()), CodeGen is 4.6x faster than Runtime and works for all scenarios.
Migration from AutoMapper
HyperMapper is designed to be a 100% API-compatible drop-in replacement for AutoMapper.
Migration Steps
- Change the namespace - That's it!
// Before
using AutoMapper;
// After
using HyperMapper;
- Update project references - Remove AutoMapper, add HyperMapper
<PackageReference Include="AutoMapper" Version="*" />
<ProjectReference Include="../HyperMapper/src/HyperMapper/HyperMapper.csproj" />
- Run and verify - No code changes required!
Automatic Migration Script
# Replace all usings in a directory
find . -name "*.cs" -exec sed -i '' 's/using AutoMapper;/using HyperMapper;/g' {} \;
What's Compatible?
✅ Fully Compatible:
IMapperinterface with allMap<>()methodsProfilewithCreateMap<>()ForMember(),ForMember().MapFrom(),ForMember().Ignore()ReverseMap()MapperConfigurationwithAddProfile<>()ITypeConverter<TSource, TDest>IValueResolver<TSource, TDest, TMember>withMapFrom<TResolver>()(NEW in v12.0.0)ResolutionContext- Collection mapping (List, Array, IEnumerable, Dictionary, etc.)
- Enum to string conversion
- Nested object mapping
- Constructor mapping with
ConstructUsing()andForCtorParam() - Value transformations with
AddTransform<>() - Inheritance mapping with
Include()andIncludeBase() - Member flattening with
IncludeMembers() - Conditional mapping with
Condition()andPreCondition() - Null substitution with
NullSubstitute() - Path mapping with
ForPath() - Lifecycle hooks with
BeforeMap()andAfterMap() - Reference preservation with
PreserveReferences() - Depth limiting with
MaxDepth() - Assembly scanning with
AddMaps() - Mapping order control with
SetMappingOrder()(v12.1.0)
⚠️ Behavioral Differences:
- Performance: HyperMapper is 1.2-5.3x faster
- First call: HyperMapper has zero warm-up time with CodeGen
- Memory: HyperMapper uses less memory (up to 30% savings)
🚀 Performance Best Practice:
For maximum performance, use the typed API:
// ✅ RECOMMENDED - Fast Path (4.6x faster)
var result = mapper.Map<Source, Destination>(source);
// ❌ AVOID - Slow Path (uses DynamicInvoke)
var result = mapper.Map<Destination>(source);
The typed API (Map<TSource, TDest>) uses compile-time generated mappers with direct method calls (~32ns). The single-type API (Map<TDest>) requires reflection and DynamicInvoke (~147ns+).
Migration Checklist
- Replace
using AutoMapper;withusing HyperMapper;in all .cs files - Remove AutoMapper
PackageReferencefrom .csproj files - Add
ProjectReferenceto HyperMapper in .csproj files - Run build to verify compatibility
- Run tests to validate functionality
- (Optional) Enable CodeGen Mode for production performance
- (Optional) Add
HyperMapperGeneratedRegistry.Initialize(config)for 2-3x speedup
Entity Framework & Lazy Loading Considerations
⚠️ IMPORTANT: If you're using Entity Framework Core with lazy loading proxies, read this section carefully before migrating to HyperMapper.
Overview
HyperMapper's CodeGen mode generates compiled code at build-time, which fundamentally changes how navigation properties are accessed compared to AutoMapper's runtime reflection. This difference has critical implications when working with Entity Framework Core's lazy loading feature.
Key Insight: AutoMapper can trigger EF lazy loading through reflection, but HyperMapper's compiled code cannot. This means navigation properties that worked automatically with AutoMapper will be null with HyperMapper unless explicitly loaded.
The Issue: Lazy Loading Behavioral Difference
AutoMapper (Runtime Reflection)
- Uses
PropertyInfo.GetValue()to access navigation properties - EF Core proxies intercept these reflection calls
- Lazy loading is automatically triggered
- Navigation properties are loaded on-demand
- ✅ Works with lazy loading
HyperMapper (Compiled Code)
- Generates direct property access:
entity.NavigationProperty - EF Core proxies cannot intercept compiled code
- No lazy loading trigger
- Navigation properties remain
nullif not pre-loaded - ❌ Does not work with lazy loading
Example:
// With AutoMapper (lazy loading works)
CreateMap<Order, OrderDto>()
.ForMember(dest => dest.CustomerName, opt => opt.MapFrom(
src => src.Customer.Name // ✅ Customer is lazy-loaded automatically
));
// With HyperMapper (lazy loading does NOT work)
CreateMap<Order, OrderDto>()
.ForMember(dest => dest.CustomerName, opt => opt.MapFrom(
src => src.Customer.Name // ❌ Customer is NULL - NullReferenceException!
));
Why This Happens
The difference lies in how the two libraries access entity properties:
// AutoMapper (Runtime)
// 1. PropertyInfo.GetValue(entity, "Customer")
// 2. EF proxy intercepts PropertyInfo.GetValue()
// 3. Lazy loading triggered ✅
// 4. Customer entity loaded from database
// 5. Returns loaded Customer
// HyperMapper (CodeGen)
// 1. Generated code: return entity.Customer.Name;
// 2. Direct property access - no interception possible
// 3. Customer is NULL (not loaded)
// 4. NullReferenceException ❌
EF Core's lazy loading relies on method interception at runtime. Since HyperMapper generates static compiled code, there's no runtime method call to intercept.
Identifying Lazy Loading Dependencies
Before migrating to HyperMapper, identify all places where your code relies on lazy loading:
Strategy 1: Analyze Repository Queries
Look for repository methods that return IQueryable or entities without .Include():
// ❌ PROBLEM: Query without Include
public IQueryable<Order> GetFilteredOrders(OrderFilterDto filter)
{
return _context.Set<Order>()
.Where(o => o.OrderDate >= filter.FromDate);
// Navigation properties (Customer, OrderItems) will be NULL with HyperMapper
}
Strategy 2: Analyze Mapping Profiles
Search for mappings that access navigation properties:
// Look for patterns like this in your Profile classes
CreateMap<Order, OrderDetailDto>()
.ForMember(dest => dest.CustomerName, opt => opt.MapFrom(
src => src.Customer.Name // ← Customer navigation property
// ^^^^^^^^^^^^
// This MUST be pre-loaded with .Include()
))
.ForMember(dest => dest.ProductName, opt => opt.MapFrom(
src => src.OrderItems.First().Product.Name
// ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
// Nested navigation properties MUST be pre-loaded
));
Strategy 3: Grep for Navigation Properties
Use command-line tools to find all navigation property accesses:
# Find all navigation property accesses in mapping profiles
grep -r "src\." --include="*Profile.cs" . | grep -E "\.\w+Navigation|\.\w+\.\w+"
# Find repository methods that might need Include
grep -r "IQueryable<" --include="*Repository.cs" .
Solution: Explicit Eager Loading
The solution is to explicitly load all navigation properties using .Include() and .ThenInclude().
Pattern 1: Simple Include (1 level)
var orders = _context.Orders
.Include(o => o.Customer) // Load Customer navigation property
.ToList();
Pattern 2: Nested Include (multiple levels)
var orders = _context.Orders
.Include(o => o.Customer) // Level 1
.ThenInclude(c => c.Address) // Level 2
.ThenInclude(a => a.Country) // Level 3
.ToList();
Pattern 3: Multiple Branches from Same Root
var orders = _context.Orders
.Include(o => o.Customer)
.ThenInclude(c => c.Address)
.Include(o => o.Customer) // Repeat root for different branch
.ThenInclude(c => c.PreferredPaymentMethod)
.ToList();
Pattern 4: Collection Navigation Properties
var orders = _context.Orders
.Include(o => o.OrderItems) // Collection
.ThenInclude(item => item.Product) // Items in collection
.ToList();
Pattern 5: Collections + Other Properties
var orders = _context.Orders
.Include(o => o.OrderItems)
.ThenInclude(item => item.Product)
.Include(o => o.Customer)
.ThenInclude(c => c.Address)
.ToList();
Complete Before/After Example
❌ BEFORE (AutoMapper - relies on lazy loading)
// Repository
public IQueryable<Order> GetFilteredOrders(OrderFilterDto filter)
{
var query = _context.Set<Order>().AsQueryable();
if (filter.FromDate.HasValue)
query = query.Where(o => o.OrderDate >= filter.FromDate.Value);
return query; // No .Include() - relies on lazy loading
}
// Mapping Profile
CreateMap<Order, OrderDetailDto>()
.ForMember(dest => dest.CustomerName, opt => opt.MapFrom(
src => src.Customer.Name)) // AutoMapper triggers lazy loading
.ForMember(dest => dest.ShippingAddress, opt => opt.MapFrom(
src => src.Customer.Address.Street)); // Nested lazy loading
✅ AFTER (HyperMapper - explicit eager loading)
// Repository
public IQueryable<Order> GetFilteredOrders(OrderFilterDto filter)
{
// Pre-load ALL navigation properties used in mapping
var query = _context.Set<Order>()
.Include(o => o.Customer)
.ThenInclude(c => c.Address)
.Include(o => o.OrderItems)
.ThenInclude(item => item.Product)
.AsQueryable();
if (filter.FromDate.HasValue)
query = query.Where(o => o.OrderDate >= filter.FromDate.Value);
return query;
}
// Mapping Profile (unchanged)
CreateMap<Order, OrderDetailDto>()
.ForMember(dest => dest.CustomerName, opt => opt.MapFrom(
src => src.Customer.Name)) // Now works - Customer is pre-loaded
.ForMember(dest => dest.ShippingAddress, opt => opt.MapFrom(
src => src.Customer.Address.Street)); // Now works - Address is pre-loaded
Override GetByIdAsync Pattern
If you use a generic repository with GetByIdAsync, you may need to override it in specific repositories:
// Base Repository (no navigation properties)
public class Repository<TEntity> where TEntity : class
{
protected readonly DbContext _context;
public virtual async Task<TEntity?> GetByIdAsync(int id)
{
return await _context.Set<TEntity>().FindAsync(id);
}
}
// Specific Repository (with navigation properties)
public class OrderRepository : Repository<Order>, IOrderRepository
{
public OrderRepository(DbContext context) : base(context) { }
// Override to add .Include()
public override async Task<Order?> GetByIdAsync(int id)
{
return await _context.Set<Order>()
.Include(o => o.Customer)
.Include(o => o.OrderItems)
.ThenInclude(item => item.Product)
.FirstOrDefaultAsync(o => o.Id == id);
}
}
Testing Strategy
Create tests to verify that .Include() statements are working correctly:
Test Pattern 1: Verify Includes Work
[Fact]
public async Task GetFilteredOrders_LoadsAllNavigationProperties()
{
// Arrange
await using var context = CreateInMemoryContext();
SeedCompleteData(context); // Customer → Address → Order → OrderItems
var repository = new OrderRepository(context);
// Act
var query = repository.GetFilteredOrders(new OrderFilterDto());
var result = query.ToList();
// Assert
var first = result.First();
Assert.NotNull(first.Customer); // Verify navigation property loaded
Assert.NotNull(first.Customer.Address); // Verify nested property loaded
Assert.NotNull(first.OrderItems); // Verify collection loaded
Assert.NotEmpty(first.OrderItems); // Verify collection has items
}
Test Pattern 2: Negative Test (without Include)
[Fact]
public async Task Order_WithoutInclude_HasNullNavigationProperties()
{
// Arrange
var dbName = $"TestDb_{Guid.NewGuid()}";
await using var contextForSeeding = CreateInMemoryContextWithName(dbName);
SeedCompleteData(contextForSeeding);
await contextForSeeding.SaveChangesAsync();
// Act - Use NEW context for query WITHOUT Include
await using var contextForQuerying = CreateInMemoryContextWithName(dbName);
var query = contextForQuerying.Set<Order>().AsNoTracking();
var result = query.ToList();
// Assert - Demonstrates lazy loading does NOT work
Assert.Null(result.First().Customer); // NULL because not loaded
}
Test Pattern 3: Integration Test with Mapping
[Fact]
public async Task OrderToDto_WithIncludes_MapsAllProperties()
{
// Arrange
await using var context = CreateInMemoryContext();
SeedCompleteData(context);
var repository = new OrderRepository(context);
var mapper = CreateMapper();
// Act
var query = repository.GetFilteredOrders(new OrderFilterDto());
var orders = query.ToList();
var dto = mapper.Map<OrderDetailDto>(orders.First());
// Assert - DTO must have ALL nested data
Assert.Equal("John Doe", dto.CustomerName);
Assert.Equal("123 Main St", dto.ShippingAddress);
Assert.NotEmpty(dto.Items);
Assert.Equal("Widget", dto.Items.First().ProductName);
}
Best Practices
✅ DO:
- Add
.Include()in repositories, not in service layers - Test with InMemory Database to verify Include statements
- Use
AsNoTracking()for read-only queries with Include - Document navigation dependencies in code comments
- Override
GetByIdAsync()when entity-specific includes are needed
❌ DON'T:
- Don't rely on lazy loading with HyperMapper CodeGen
- Don't add
.Include()blindly - only for properties used in mapping - Don't use explicit
.Load()- prefer.Include()for performance - Don't mix lazy and eager loading - choose one strategy and stick to it
Performance Tips:
// ✅ GOOD: Include only what you need
.Include(o => o.Customer)
.ThenInclude(c => c.Address)
// ❌ BAD: Include unnecessary deep/circular data
.Include(o => o.Customer)
.ThenInclude(c => c.Address)
.ThenInclude(a => a.City) // Not used in mapping
.ThenInclude(city => city.Country) // Circular/unnecessary
Migration Checklist
When migrating from AutoMapper to HyperMapper with Entity Framework:
- Identify all repositories that return entities mapped to DTOs
- Analyze
MappingProfileclasses for navigation property accesses - Add
.Include()for all navigation properties referenced in mappings - Create tests with InMemory Database to verify Include statements
- Create negative tests to verify lazy loading doesn't work without Include
- Run integration tests end-to-end with mapping
- Verify all API endpoints return complete data
Real-World Example
Scenario: API endpoint returns DTOs with null fields after migrating to HyperMapper
Entity Relationships:
Order → Customer → Address → Country
→ OrderItems → Product → Category
Mapping Profile:
CreateMap<Order, OrderDetailDto>()
.ForMember(dest => dest.CustomerName, opt => opt.MapFrom(
src => src.Customer.Name))
.ForMember(dest => dest.ShippingCountry, opt => opt.MapFrom(
src => src.Customer.Address.Country.Name))
.ForMember(dest => dest.Items, opt => opt.MapFrom(
src => src.OrderItems));
Solution Applied:
public IQueryable<Order> GetFilteredOrders(OrderFilterDto filter)
{
var query = _context.Set<Order>()
// Include all navigation properties used in mapping
.Include(o => o.Customer)
.ThenInclude(c => c.Address)
.ThenInclude(a => a.Country)
.Include(o => o.OrderItems)
.ThenInclude(item => item.Product)
.ThenInclude(p => p.Category)
.AsQueryable();
// Apply filters...
if (filter.FromDate.HasValue)
query = query.Where(o => o.OrderDate >= filter.FromDate.Value);
return query;
}
Result: ✅ API endpoint works correctly, all DTOs fully populated
Lazy Loading Proxies Compatibility Issue
⚠️ CRITICAL: HyperMapper CodeGen mode is incompatible with Entity Framework Core's .UseLazyLoadingProxies() feature.
The Problem
When using EF Core with .UseLazyLoadingProxies(), Entity Framework creates dynamic proxy classes at runtime that intercept property access to enable lazy loading. HyperMapper's CodeGen mode generates compiled code that cannot properly access properties of these proxy classes, resulting in null values even when navigation properties are explicitly loaded with .Include().
Symptoms:
- Navigation properties are
nullin mapped DTOs - Problem occurs only with SQL Server or other real databases (not InMemory)
.Include()statements appear to be ignored- Same code works with AutoMapper
Root Cause:
// With .UseLazyLoadingProxies() enabled:
var entity = await context.Orders.Include(o => o.Customer).FirstAsync();
// EF returns a dynamic proxy class: OrderProxy (inherits from Order)
// entity.Customer is loaded ✅
// HyperMapper CodeGen generates:
// return new OrderDto { CustomerName = source.Customer.Name };
// Problem: The generated code cannot access properties of proxy classes correctly
// Result: source.Customer is null ❌ even though it was loaded
The Solution: Disable Lazy Loading Proxies
Remove .UseLazyLoadingProxies() from your DbContext configuration:
// ❌ BEFORE (doesn't work with HyperMapper CodeGen)
services.AddDbContext<AppDbContext>((serviceProvider, options) =>
{
options.UseLazyLoadingProxies(); // ← Remove this
options.UseSqlServer(connectionString);
});
// ✅ AFTER (works with HyperMapper CodeGen)
services.AddDbContext<AppDbContext>((serviceProvider, options) =>
{
// Lazy loading proxies disabled for HyperMapper CodeGen compatibility
// All navigation properties must be explicitly loaded with .Include()
options.UseSqlServer(connectionString);
});
Why This Works:
- Without lazy loading proxies, EF Core returns concrete entity classes instead of dynamic proxies
- HyperMapper's generated code can access properties of concrete classes correctly
- Explicit
.Include()ensures all navigation properties are loaded before mapping - No performance penalty - explicit loading is actually more efficient than lazy loading
Testing with Production Database
To verify the fix works with your production database, create a diagnostic test:
[Fact(Skip = "Diagnostic - connects to production database")]
public async Task ProductionDatabase_VerifyNavigationProperties()
{
// Create DbContext WITHOUT lazy loading proxies
var options = new DbContextOptionsBuilder<AppDbContext>()
.UseSqlServer("your-production-connection-string")
// .UseLazyLoadingProxies() // ← Disabled
.Options;
await using var context = new AppDbContext(options);
var mapper = CreateMapper();
// Load entities with Include
var entities = await context.Orders
.Include(o => o.Customer)
.ThenInclude(c => c.Address)
.Take(3)
.ToListAsync();
// Map to DTOs
var dtos = mapper.Map<List<OrderDto>>(entities);
// Verify navigation properties are populated
Assert.NotNull(dtos.First().Customer);
Assert.NotNull(dtos.First().Customer.Address);
}
Migration Checklist
When disabling lazy loading proxies:
- Remove
.UseLazyLoadingProxies()from all DbContext configurations - Verify all repository methods use explicit
.Include()for navigation properties - Add
.Include()statements for any missing navigation properties - Create integration tests with real database to verify Include statements work
- Update all environments (Development, Staging, Production)
- Restart application after deployment to clear EF Core cached plans
Reference: See Real-World Example section above for complete repository patterns with explicit Include.
Runtime Mode Documentation
Runtime Mode uses the AutoMapper-compatible API for dynamic configuration at runtime.
Basic Configuration
var config = new MapperConfiguration(cfg =>
{
// Add profiles
cfg.AddProfile<UserProfile>();
cfg.AddProfile<OrderProfile>();
// Or scan assemblies
cfg.AddMaps(Assembly.GetExecutingAssembly());
});
// Validate configuration
config.AssertConfigurationIsValid();
// Create mapper
var mapper = config.CreateMapper();
Profile Creation
public class MappingProfile : Profile
{
public MappingProfile()
{
// Simple mapping
CreateMap<Source, Destination>();
// With custom configuration
CreateMap<Order, OrderDto>()
.ForMember(d => d.Total, opt => opt.MapFrom(s =>
s.Items.Sum(i => i.Price)))
.ForMember(d => d.ItemCount, opt => opt.MapFrom(s =>
s.Items.Count))
.ForMember(d => d.InternalCode, opt => opt.Ignore());
// Bidirectional mapping
CreateMap<Entity, EntityDto>().ReverseMap();
}
}
Member Configuration
CreateMap<User, UserDto>()
// Map from expression
.ForMember(d => d.FullName, opt => opt.MapFrom(s =>
$"{s.FirstName} {s.LastName}"))
// Map from destination-dependent expression
.ForMember(d => d.UpdatedName, opt => opt.MapFrom((s, d) =>
d.UpdatedName ?? s.Name))
// Ignore property
.ForMember(d => d.InternalId, opt => opt.Ignore())
// Null substitute
.ForMember(d => d.Name, opt => opt.NullSubstitute("N/A"))
// Pre-condition (only map if condition is true)
.ForMember(d => d.SecretData, opt =>
{
opt.PreCondition(s => s.IsAuthorized);
opt.MapFrom(s => s.SecretData);
})
// Post-condition (set value only if condition is true)
.ForMember(d => d.Status, opt =>
{
opt.MapFrom(s => s.Status);
opt.Condition((s, d, val) => val != null);
});
Type-Level Configuration
CreateMap<Source, Dest>()
// Type-level transformations (e.g., trim all strings)
.AddTransform<string>(s => s?.Trim())
// Custom converter
.ConvertUsing(s => new Dest { Value = s.Value * 2 })
// Or use ITypeConverter
.ConvertUsing<CustomConverter>()
// Before/After map hooks
.BeforeMap((s, d) => { /* pre-processing */ })
.AfterMap((s, d) => { /* post-processing */ })
// Max depth for circular references
.MaxDepth(2)
// Preserve object references
.PreserveReferences();
Constructor Mapping
public class Destination
{
public int Id { get; }
public string Name { get; }
public Destination(int id, string name)
{
Id = id;
Name = name;
}
}
CreateMap<Source, Destination>()
.ConstructUsing(s => new Destination(s.Id, s.Name))
// Or map individual constructor parameters
.ForCtorParam("id", opt => opt.MapFrom(s => s.Identifier))
.ForCtorParam("name", opt => opt.MapFrom(s => s.FullName));
Inheritance Mapping
CreateMap<Person, PersonDto>()
.Include<Employee, EmployeeDto>()
.Include<Customer, CustomerDto>();
CreateMap<Employee, EmployeeDto>()
.IncludeBase<Person, PersonDto>();
CreateMap<Customer, CustomerDto>()
.IncludeBase<Person, PersonDto>();
Flattening with IncludeMembers
public class Order
{
public int Id { get; set; }
public Address ShippingAddress { get; set; }
}
public class Address
{
public string Street { get; set; }
public string City { get; set; }
}
public class OrderDto
{
public int Id { get; set; }
public string Street { get; set; } // Flattened from ShippingAddress
public string City { get; set; } // Flattened from ShippingAddress
}
CreateMap<Order, OrderDto>()
.IncludeMembers(s => s.ShippingAddress);
CreateMap<Address, OrderDto>();
Usage Examples
// Simple map
var dto = mapper.Map<UserDto>(user);
// Map with explicit types
var dto = mapper.Map<User, UserDto>(user);
// Map to existing object (update)
var existingDto = new UserDto { Id = 1 };
mapper.Map(user, existingDto);
// Map collections
var dtos = mapper.Map<List<UserDto>>(users);
var dtoArray = mapper.Map<UserDto[]>(users);
var dtoEnumerable = mapper.Map<IEnumerable<UserDto>>(users);
// Map with runtime types
object source = user;
var dto = mapper.Map(source, typeof(User), typeof(UserDto));
See full Runtime Mode example: examples/HyperMapper.Examples.Runtime
CodeGen Mode Documentation
CodeGen Mode uses Roslyn Source Generators to analyze your Profile classes at compile-time and generate optimized C# mapping methods.
How Source Generators Work
During compilation, HyperMapper's Source Generator:
- Scans for Profile classes - Finds all classes inheriting from
HyperMapper.Profile - Analyzes CreateMap calls - Extracts source and destination types
- Parses ForMember configurations - Extracts
MapFrom,Ignore, and other member configurations - Generates C# code - Creates static mapper methods with explicit property assignments
- Creates a Registry - Generates
HyperMapperGeneratedRegistryto register all mappers
┌─────────────────────────────────────────────────────────────────┐
│ COMPILE-TIME │
├─────────────────────────────────────────────────────────────────┤
│ 1. Source Generator analyzes your Profile classes │
│ 2. Extracts CreateMap<A,B>() and ForMember() calls │
│ 3. Generates .g.cs files with explicit mapping code │
│ │
│ // Auto-generated: UserProfileGeneratedMappers.g.cs │
│ public static UserDto MapUserToUserDto(User source) { │
│ return new UserDto { │
│ Id = source.Id, │
│ FullName = $"{source.FirstName} {source.LastName}" │
│ }; │
│ } │
└─────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────┐
│ RUNTIME │
├─────────────────────────────────────────────────────────────────┤
│ 1. App Start → No reflection needed │
│ 2. Map() → ~44ns (code already compiled!) │
└─────────────────────────────────────────────────────────────────┘
Enabling CodeGen Mode
Step 1: Configure .csproj
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<EmitCompilerGeneratedFiles>true</EmitCompilerGeneratedFiles>
<CompilerGeneratedFilesOutputPath>$(BaseIntermediateOutputPath)\Generated</CompilerGeneratedFilesOutputPath>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="../HyperMapper/src/HyperMapper/HyperMapper.csproj"
OutputItemType="Analyzer"
ReferenceOutputAssembly="true" />
</ItemGroup>
</Project>
Step 2: Create Profile (Same as Runtime)
using HyperMapper;
public class UserProfile : Profile
{
public UserProfile()
{
CreateMap<User, UserDto>()
.ForMember(d => d.FullName, opt => opt.MapFrom(s =>
$"{s.FirstName} {s.LastName}"));
}
}
Step 3: Register Generated Mappers
using HyperMapper;
using HyperMapper.Generated;
var config = new MapperConfiguration(cfg =>
{
cfg.AddProfile<UserProfile>();
});
// CRITICAL: Register generated mappers for maximum performance
HyperMapperGeneratedRegistry.Initialize(config);
config.AssertConfigurationIsValid();
var mapper = config.CreateMapper();
Generated Code Structure
For each Profile, the Source Generator creates:
1. Mapper Methods File ({ProfileName}GeneratedMappers.g.cs)
// Auto-generated: UserProfileGeneratedMappers.g.cs
#nullable enable
namespace MyApp.Profiles;
[global::System.CodeDom.Compiler.GeneratedCode("HyperMapper.SourceGenerator", "12.0.0")]
internal static class UserProfileGeneratedMappers
{
/// <summary>
/// Maps User to UserDto
/// </summary>
[return: global::System.Diagnostics.CodeAnalysis.NotNullIfNotNull(nameof(source))]
public static UserDto? MapUserToUserDto(User? source)
{
if (source is null) return null;
return new UserDto
{
Id = source.Id,
// MapFrom expression inlined
FullName = $"{source.FirstName} {source.LastName}",
};
}
/// <summary>
/// Maps IEnumerable<User> to List<UserDto>
/// </summary>
public static List<UserDto> MapUserToUserDtoList(IEnumerable<User>? source)
{
if (source is null) return new();
var sourceCollection = source as ICollection<User> ?? source.ToList();
var result = new List<UserDto>(sourceCollection.Count);
foreach (var item in sourceCollection)
{
result.Add(MapUserToUserDto(item)!);
}
return result;
}
}
2. Registry File (HyperMapperGeneratedRegistry.g.cs)
// Auto-generated: HyperMapperGeneratedRegistry.g.cs
#nullable enable
namespace HyperMapper.Generated;
[global::System.CodeDom.Compiler.GeneratedCode("HyperMapper.SourceGenerator", "12.0.0")]
public static class HyperMapperGeneratedRegistry
{
private static bool _initialized;
public static void Initialize(HyperMapper.MapperConfiguration config)
{
if (_initialized) return;
_initialized = true;
// User -> UserDto
config.RegisterGeneratedPlan<User, UserDto>(
UserProfileGeneratedMappers.MapUserToUserDto);
}
}
Viewing Generated Code
After building with EmitCompilerGeneratedFiles enabled:
cd obj/Generated/HyperMapper.SourceGenerator/HyperMapper.SourceGenerator.MapperGenerator/
# View generated mappers
cat UserProfileGeneratedMappers.g.cs
# View registry
cat HyperMapperGeneratedRegistry.g.cs
Supported Scenarios
| Scenario | Support | Example |
|---|---|---|
| Simple properties | ✅ | Id = source.Id |
| Nested objects | ✅ | Address = MapAddressToAddressDto(source.Address) |
| Collections | ✅ | Items = source.Items?.Select(MapItem).ToList() |
| Enum ↔ String | ✅ | Status = source.Status.ToString() |
| Nullable conversions | ✅ | Value = source.Value ?? default |
| ForMember/MapFrom | ✅ | FullName = $"{source.First} {source.Last}" |
| ForMember/Ignore | ✅ | Property skipped in generation |
| String interpolation | ✅ | $"{source.Street}, {source.City}" |
| Arithmetic | ✅ | Total = source.Qty * source.Price |
| Flattening | ✅ | AddressStreet = source.Address?.Street |
| Struct mapping | ✅ | Point → PointDto |
| PreCondition | ✅ | if (source.IsActive) dest.Value = ... |
| Lambda Converters | ✅ | ConvertUsing(s => new Dest {...}) inlined |
| Open Generics | ✅ | Box<T> → BoxDto<T> |
Compile-Time Diagnostics
| Code | Severity | Description | Resolution |
|---|---|---|---|
| LMAP001 | Error | Destination type lacks parameterless constructor | Add parameterless constructor |
| LMAP002 | Warning | Unmapped destination property | Use ForMember(..., opt => opt.Ignore()) |
| LMAP003 | Error | Incompatible property types | Add explicit MapFrom or converter |
| LMAP007 | Info | Struct mapping generated | Informational |
| LMAP008 | Info | PreCondition compiled at build-time | Informational |
Best Practices
1. Always Register Generated Mappers
// ✅ Good - explicit registration
HyperMapperGeneratedRegistry.Initialize(config);
// ⚠️ Works but not optimal
// (no explicit registration)
2. Keep Profiles Simple for Best Generation
// ✅ Good - generates clean code
CreateMap<Source, Dest>()
.ForMember(d => d.Name, opt => opt.MapFrom(s => s.FullName));
// ⚠️ Falls back to runtime - complex converter
CreateMap<Source, Dest>()
.ConvertUsing(new ComplexConverter());
3. Enable Generated Files During Development
<PropertyGroup Condition="'$(Configuration)' == 'Debug'">
<EmitCompilerGeneratedFiles>true</EmitCompilerGeneratedFiles>
</PropertyGroup>
Troubleshooting
Generated Code Not Being Used
Symptoms: Performance similar to Runtime Mode
Solutions:
- Ensure
HyperMapperGeneratedRegistry.Initialize(config)is called - Verify Profile inherits from
HyperMapper.Profile - Rebuild:
dotnet clean && dotnet build
Generated Files Not Visible
Solutions:
- Enable
EmitCompilerGeneratedFilesin.csproj - Restart IDE
- Check
obj/Generated/directory
See full CodeGen Mode example: examples/HyperMapper.Examples.CodeGen
CodeGen Mode Known Limitations
While CodeGen Mode provides significant performance improvements (up to 4.6x faster than Runtime), there are some limitations due to the compile-time nature of Source Generators:
❌ Not Supported in CodeGen Mode
Open Generic CreateMap with Type arguments
// ❌ NOT SUPPORTED in CodeGen CreateMap(typeof(IPagedList<>), typeof(PagedListDto<>)) .ConvertUsing(typeof(PagedListConverter<,>)); // ✅ WORKAROUND - Use generic constraints CreateMap<IPagedList<T>, PagedListDto<T>>() .ConvertUsing(s => new PagedListDto<T> { Items = s.Items });BeforeMap/AfterMap hooks (Runtime only)
// ❌ NOT SUPPORTED in CodeGen CreateMap<Source, Dest>() .BeforeMap((s, d) => Console.WriteLine("Mapping")) .AfterMap((s, d) => d.Validate()); // ✅ WORKAROUND - Use Runtime Mode for these mappings
⚠️ Workarounds
For Open Generics: Create specific mappings for each type combination:
// Instead of: CreateMap(typeof(List<>), typeof(ListDto<>))
// Do:
CreateMap<List<User>, ListDto<UserDto>>();
CreateMap<List<Product>, ListDto<ProductDto>>();
✅ Fully Supported Features
All other AutoMapper features work in CodeGen Mode:
- ✅ Class-based ITypeConverter (NEW in v12.0.0)
// ✅ NOW SUPPORTED in CodeGen! CreateMap<Geometry, GeometryPointDto>() .ConvertUsing(new GeometryConverter()); - ✅ ForMember with MapFrom (including nested properties)
- ✅ Complex LINQ expressions in MapFrom (NEW in v12.0.0)
// ✅ NOW SUPPORTED - Where().Select() with type conversion .ForMember(d => d.Tags, opt => opt.MapFrom(src => src.PostazioneTags.Where(pt => pt.Tag != null).Select(pt => pt.Tag!))) - ✅ Condition and PreCondition
- ✅ ConstructUsing with lambda
- ✅ ForCtorParam
- ✅ ForPath
- ✅ NullSubstitute
- ✅ Include/IncludeBase (polymorphic mapping)
- ✅ IncludeMembers (flattening from nested objects)
- ✅ AddTransform (type-level transformations)
- ✅ Collections (all types: Array, List, HashSet, Dictionary, etc.)
- ✅ Flattening (AddressStreet → Address.Street)
- ✅ ReverseMap
- ✅ Value converters
- ✅ Nullable type handling (int? → int with ?? operator)
💡 Best Practice
Start with CodeGen Mode for all mappings, and only fall back to Runtime Mode for the rare cases that require ITypeConverter instances or runtime hooks.
Advanced Features
Type Transformations
Apply transformations to all properties of a specific type:
CreateMap<Source, Dest>()
// Trim all strings
.AddTransform<string>(s => s?.Trim())
// Round all decimals to 2 places
.AddTransform<decimal>(d => Math.Round(d, 2));
Destination-Dependent Mapping
Map based on both source and destination values:
CreateMap<Source, Dest>()
.ForMember(d => d.Name, opt => opt.MapFrom((src, dest) =>
dest.Name ?? src.Name)); // Keep dest.Name if already set
Path Mapping
Map to deep nested properties:
CreateMap<Source, Dest>()
.ForPath(d => d.Address.Street, opt => opt.MapFrom(s => s.FullAddress));
Circular Reference Handling
CreateMap<Category, CategoryDto>()
.MaxDepth(3) // Limit recursion depth
.PreserveReferences(); // Maintain object references
Assembly Scanning with AutoMap Attribute
[AutoMap(typeof(UserDto))]
public class User
{
public int Id { get; set; }
public string Name { get; set; }
}
// In configuration
cfg.AddMaps(Assembly.GetExecutingAssembly());
Value Resolvers (v12.0.0)
Custom value resolvers provide reusable, testable mapping logic for complex member transformations. The IValueResolver interface is 100% compatible with AutoMapper.
Interface Definition
public interface IValueResolver<in TSource, in TDestination, TDestMember>
{
TDestMember Resolve(TSource source, TDestination destination,
TDestMember destMember, ResolutionContext context);
}
Basic Usage
// 1. Define a resolver
public class FullNameResolver : IValueResolver<User, UserDto, string>
{
public string Resolve(User source, UserDto destination,
string destMember, ResolutionContext context)
{
return $"{source.FirstName} {source.LastName}";
}
}
// 2. Use in Profile
public class UserProfile : Profile
{
public UserProfile()
{
CreateMap<User, UserDto>()
.ForMember(d => d.FullName, opt => opt.MapFrom<FullNameResolver>());
}
}
Instance-Based Registration
You can also provide a pre-instantiated resolver:
var resolver = new CurrencyFormatter("USD");
CreateMap<Order, OrderDto>()
.ForMember(d => d.Total, opt => opt.MapFrom(resolver));
Dependency Injection Support
Use ConstructServicesUsing to integrate with your DI container:
var config = new MapperConfiguration(cfg =>
{
cfg.ConstructServicesUsing(type => serviceProvider.GetService(type)!);
cfg.AddProfile<UserProfile>();
});
Now resolvers can have constructor dependencies injected:
public class PricingResolver : IValueResolver<Order, OrderDto, decimal>
{
private readonly IPricingService _pricing;
public PricingResolver(IPricingService pricing)
{
_pricing = pricing;
}
public decimal Resolve(Order source, OrderDto destination,
decimal destMember, ResolutionContext context)
{
return _pricing.Calculate(source.Items);
}
}
Nested Mapping via Context
Access the mapper through the resolution context to perform nested mappings:
public class CustomerResolver : IValueResolver<Order, OrderDto, CustomerInfo>
{
public CustomerInfo Resolve(Order source, OrderDto dest,
CustomerInfo member, ResolutionContext context)
{
return context.Mapper.Map<CustomerInfo>(source.Customer);
}
}
Combining with PreCondition
Value resolvers work seamlessly with PreCondition:
CreateMap<Source, Dest>()
.ForMember(d => d.Value, opt =>
{
opt.PreCondition(src => src.ShouldMap);
opt.MapFrom<MyValueResolver>();
});
CodeGen Mode Support
Value resolvers are fully supported in both Runtime and CodeGen modes. In CodeGen mode, the resolver is instantiated via Activator.CreateInstance() and the generated code calls the Resolve method directly.
Note: In CodeGen mode, the ResolutionContext parameter may be null. Resolvers that depend on context.Mapper for nested mappings will fall back to Runtime execution.
Source Generator Enhancements (v12.1.x)
Version 12.1.x introduces several improvements to the Source Generator for better compatibility with real-world codebases.
External Assembly Type Support (v12.0.2)
Types from external assemblies (NuGet packages, referenced projects) are now fully supported in mapping expressions. The Source Generator correctly resolves fully-qualified type names:
// Example: Using EntityFramework types
using Microsoft.EntityFrameworkCore;
public class EntitySource
{
public EntityState State { get; set; }
}
public class EntityDto
{
public EntityState MappedState { get; set; }
public string StateDescription { get; set; }
}
// Profile
CreateMap<EntitySource, EntityDto>()
.ForMember(d => d.MappedState, opt => opt.MapFrom(s => s.State))
.ForMember(d => d.StateDescription, opt => opt.MapFrom(s => s.State.ToString()));
Ambiguous Static Class Resolution (v12.1.0)
Common static classes like Path, File, Math, Convert, etc. are automatically qualified to prevent CS0104 ambiguity errors when other libraries define types with the same names:
// These expressions work correctly even if your codebase has a "Path" class
CreateMap<FileSource, FileDto>()
.ForMember(d => d.Extension, opt => opt.MapFrom(s => Path.GetExtension(s.FilePath)))
.ForMember(d => d.FileName, opt => opt.MapFrom(s => Path.GetFileName(s.FilePath)))
.ForMember(d => d.Rounded, opt => opt.MapFrom(s => Math.Round(s.Value, 2)))
.ForMember(d => d.IntValue, opt => opt.MapFrom(s => Convert.ToInt32(s.StringValue)));
Automatically qualified classes:
Path→global::System.IO.PathFile→global::System.IO.FileDirectory→global::System.IO.DirectoryMath→global::System.MathConvert→global::System.ConvertEncoding→global::System.Text.EncodingEnvironment→global::System.Environment
Base Class Property Resolution (v12.1.0)
Properties from base classes are now correctly resolved in MapFrom expressions:
public class BaseEntity
{
public int Id { get; set; }
public DateTime CreatedAt { get; set; }
}
public class DerivedEntity : BaseEntity
{
public string Name { get; set; }
}
public class EntityDto
{
public int EntityId { get; set; }
public string CreatedDate { get; set; }
public string Name { get; set; }
}
// Base class properties (Id, CreatedAt) are correctly resolved
CreateMap<DerivedEntity, EntityDto>()
.ForMember(d => d.EntityId, opt => opt.MapFrom(s => s.Id))
.ForMember(d => d.CreatedDate, opt => opt.MapFrom(s => s.CreatedAt.ToString("yyyy-MM-dd")));
Standalone Lambda Parameter Support (v12.1.0)
Lambda expressions with standalone parameters (not just property access) are now handled correctly:
CreateMap<Source, Dest>()
// Ternary expressions with parameter
.ForMember(d => d.NameOrDefault, opt => opt.MapFrom(s =>
string.IsNullOrEmpty(s.Name) ? "Default" : s.Name))
// Null coalescing
.ForMember(d => d.Value, opt => opt.MapFrom(s => s.NullableInt ?? 0))
// Null conditional with nested object
.ForMember(d => d.InnerName, opt => opt.MapFrom(s =>
s.Inner == null ? "N/A" : s.Inner.Name));
Mapping Order Control (v12.1.0)
Control the sequence in which properties are mapped using SetMappingOrder(). This is essential when destination property setters have side effects or dependencies on other properties.
When to Use SetMappingOrder
Use SetMappingOrder() when:
- Dependent property setters: One property's setter modifies another property
- Initialization sequences: Properties must be set in a specific order for validation
- Side-effect management: Control execution order when setters have observable effects
Execution Order Rules
- Properties without
SetMappingOrderexecute first (default behavior) - Properties with explicit order execute in ascending order (-500 before 0 before 600)
- Properties with the same order value maintain
ForMember()definition sequence
Example 1: Dependent Property Setters
The primary use case is when a destination property setter has a side effect that modifies another property:
public class Destination
{
private string one;
public string One
{
get => one;
set
{
one = value;
Two = value; // Side effect: also sets Two
}
}
public string Two { get; set; }
}
// Configure mapping order to preserve independent values
CreateMap<Source, Destination>()
.ForMember(d => d.One, opt =>
{
opt.MapFrom(s => s.First);
opt.SetMappingOrder(-500); // Execute first
})
.ForMember(d => d.Two, opt =>
{
opt.MapFrom(s => s.Second);
opt.SetMappingOrder(600); // Execute after One, preserving independent value
});
var result = mapper.Map<Destination>(source);
// result.One = "first"
// result.Two = "second" (not overwritten by One's setter)
Without SetMappingOrder, the mapping order is undefined, and One's setter might overwrite Two's value.
Example 2: Property Accumulation
When a property accumulates or concatenates values:
public class AccumulatingDestination
{
private string id = "";
public string ID
{
get => id;
set => id = string.Concat(ID, value); // Accumulates values
}
}
CreateMap<Source, AccumulatingDestination>()
.ForMember(d => d.ID, opt =>
{
opt.MapFrom(s => s.ClientID);
opt.SetMappingOrder(-1000); // Map first to establish base value
});
Example 3: Mixed Ordering
Combining null order, negative values, and positive values:
CreateMap<Source, Destination>()
.ForMember(d => d.Priority, opt =>
{
opt.MapFrom(s => s.PriorityValue);
// No SetMappingOrder - maps first (null order)
})
.ForMember(d => d.Name, opt =>
{
opt.MapFrom(s => s.FullName);
opt.SetMappingOrder(-50); // Maps second
})
.ForMember(d => d.Description, opt =>
{
opt.MapFrom(s => s.Desc);
opt.SetMappingOrder(0); // Maps third
})
.ForMember(d => d.Status, opt =>
{
opt.MapFrom(s => s.CurrentStatus);
opt.SetMappingOrder(100); // Maps last
});
// Execution order: Priority (null) → Name (-50) → Description (0) → Status (100)
Compatibility with Other Features
SetMappingOrder works seamlessly with:
- Conditions: Order is respected, conditions are still evaluated
- PreConditions: Member positioned in sequence according to order
- Destination-dependent mappings:
MapFrom((src, dest) => ...)respects order - Execution plans: Order preserved in compiled expression trees
AutoMapper Compatibility
HyperMapper's SetMappingOrder is 100% compatible with AutoMapper:
- Same API signature:
void SetMappingOrder(int mappingOrder) - Same execution order rules (null order first, then ascending)
- Same behavior with inheritance and other features
Examples
Two complete example applications are available:
Runtime Mode Example
Demonstrates the AutoMapper-compatible Runtime Mode:
- Basic configuration with
MapperConfiguration - Profile creation with computed properties
- Nested object mapping
- Collection mapping
- Enum to string conversion
- Bidirectional mapping with
ReverseMap() - Performance measurement
Run it:
cd examples/HyperMapper.Examples.Runtime
dotnet run
CodeGen Mode Example
Demonstrates Source Generator CodeGen Mode:
- .csproj configuration for code generation
- Struct mapping at compile-time
- PreCondition compiled to if-statements
- Viewing generated .g.cs files
- Compile-time diagnostics
- Performance comparison with Runtime Mode
Run it:
cd examples/HyperMapper.Examples.CodeGen
dotnet build # Generate code
dotnet run # Run example
API Reference
IMapper
Main interface for performing mappings:
public interface IMapper
{
// Map to new object
TDestination Map<TDestination>(object source);
TDestination Map<TSource, TDestination>(TSource source);
// Map to existing object
TDestination Map<TSource, TDestination>(TSource source, TDestination destination);
// Map with runtime types
object Map(object source, Type sourceType, Type destinationType);
object Map(object source, object destination, Type sourceType, Type destinationType);
}
Profile
Base class for defining mapping configurations:
public abstract class Profile
{
protected IMappingExpression<TSource, TDestination> CreateMap<TSource, TDestination>();
protected IMappingExpressionBase CreateMap(Type sourceType, Type destinationType);
}
MapperConfiguration
Class for configuring the mapper:
public class MapperConfiguration
{
public MapperConfiguration(Action<IMapperConfigurationExpression> configure);
public MapperConfiguration(Action<IMapperConfigurationExpression> configure, ILoggerFactory? loggerFactory);
public void RegisterGeneratedPlan<TSource, TDest>(Func<TSource?, TDest?>? generatedMapper);
public void AssertConfigurationIsValid();
public IMapper CreateMapper();
}
IMappingExpression
Fluent API for configuring mappings:
public interface IMappingExpression<TSource, TDestination>
{
// Member configuration
IMappingExpression<TSource, TDestination> ForMember<TMember>(
Expression<Func<TDestination, TMember>> destinationMember,
Action<IMemberConfigurationExpression<TSource, TDestination, TMember>> memberOptions);
// Path configuration
IMappingExpression<TSource, TDestination> ForPath<TMember>(
Expression<Func<TDestination, TMember>> destinationMember,
Action<IPathConfigurationExpression<TSource, TDestination, TMember>> memberOptions);
// Constructor parameter
IMappingExpression<TSource, TDestination> ForCtorParam(
string ctorParamName,
Action<ICtorParamConfigurationExpression<TSource>> paramOptions);
// Type-level configuration
IMappingExpression<TSource, TDestination> AddTransform<TValue>(Expression<Func<TValue, TValue>> transformer);
IMappingExpression<TSource, TDestination> ConvertUsing(Func<TSource, TDestination> converter);
IMappingExpression<TSource, TDestination> ConvertUsing<TTypeConverter>()
where TTypeConverter : ITypeConverter<TSource, TDestination>;
IMappingExpression<TSource, TDestination> ConstructUsing(Func<TSource, TDestination> constructor);
// Lifecycle hooks
IMappingExpression<TSource, TDestination> BeforeMap(Action<TSource, TDestination> beforeFunction);
IMappingExpression<TSource, TDestination> AfterMap(Action<TSource, TDestination> afterFunction);
// Other
IMappingExpression<TSource, TDestination> MaxDepth(int depth);
IMappingExpression<TSource, TDestination> PreserveReferences();
IMappingExpression<TSource, TDestination> IncludeMembers(params Expression<Func<TSource, object>>[] memberExpressions);
IMappingExpression<TDestination, TSource> ReverseMap();
}
IMemberConfigurationExpression
Member-level configuration options within ForMember():
public interface IMemberConfigurationExpression<TSource, TDestination, TMember>
{
// Value mapping
void MapFrom<TSourceMember>(Expression<Func<TSource, TSourceMember>> sourceMember);
void MapFrom<TSourceMember>(Func<TSource, TDestination, TSourceMember> resolver);
void MapFrom(string sourceMemberName);
// Value Resolvers (v12.0.0)
void MapFrom<TValueResolver>()
where TValueResolver : IValueResolver<TSource, TDestination, TMember>;
void MapFrom(IValueResolver<TSource, TDestination, TMember> resolver);
// Conditions
void PreCondition(Func<TSource, bool> condition);
void Condition(Func<TSource, TDestination, TMember, bool> condition);
// Ignore and substitute
void Ignore();
void NullSubstitute(TMember nullSubstitute);
// Other
void UseDestinationValue();
void SetMappingOrder(int mappingOrder);
}
IValueResolver (v12.0.0)
Interface for custom value resolution:
public interface IValueResolver<in TSource, in TDestination, TDestMember>
{
TDestMember Resolve(TSource source, TDestination destination,
TDestMember destMember, ResolutionContext context);
}
Testing and Coverage
HyperMapper is extensively tested to ensure reliability and compatibility:
- 856 total tests (756 unit + 100 integration)
- 100% pass rate
- 82.4% code coverage (90.1% method coverage)
Test categories:
- Basic mapping (simple objects, collections, nested)
- Configuration (profiles, validation)
- Advanced features (ForPath, IncludeMembers, MaxDepth, PreserveReferences)
- Source Generator (CodeGen compilation, diagnostics)
- Performance (benchmarks, memory allocation)
Run tests:
cd tests/HyperMapper.Tests
dotnet test
Benchmarks
Comprehensive performance benchmarks comparing HyperMapper (Runtime & CodeGen modes), AutoMapper, and manual mapping across different scenarios.
Environment
- BenchmarkDotNet: v0.14.0
- OS: macOS 26.2 (Darwin 25.2.0)
- CPU: Apple M2 Pro (10 cores)
- .NET: 8.0.23 (Arm64 RyuJIT AdvSIMD)
1. Collection Mapping Performance
Mapping collections of simple objects (CollectionItemSource → CollectionItemDestination).
| Size | Method | Mean | Error | Ratio vs Baseline | Allocated |
|---|---|---|---|---|---|
| Small (10 items) | |||||
| Manual | 301 ns | ± 22 ns | baseline | 616 B | |
| HyperMapper Runtime | 597 ns | ± 49 ns | +98% | 744 B | |
| HyperMapper CodeGen | 607 ns | ± 70 ns | +102% | 744 B | |
| AutoMapper | 601 ns | ± 87 ns | +100% | 808 B | |
| Medium (100 items) | |||||
| Manual | 2,583 ns | ± 219 ns | baseline | 5,656 B | |
| HyperMapper CodeGen | 2,100 ns | ± 150 ns | -19% ✅ | 5,784 B | |
| HyperMapper Runtime | 3,098 ns | ± 310 ns | +20% | 5,784 B | |
| AutoMapper | 3,771 ns | ± 559 ns | +46% | 6,992 B | |
| Large (1000 items) | |||||
| Manual | 18,914 ns | ± 3,020 ns | baseline | 56,056 B | |
| HyperMapper CodeGen | 29,935 ns | ± 2,959 ns | +58% | 56,184 B | |
| HyperMapper Runtime | 34,548 ns | ± 15,580 ns | +83% | 56,184 B | |
| AutoMapper | 37,402 ns | ± 2,516 ns | +98% | 64,600 B |
Key Insights:
- CodeGen is 19% faster than manual on medium collections (100 items)
- CodeGen is 1.6x faster than Runtime on large collections (1000 items)
- AutoMapper allocates 15% more memory than HyperMapper on large collections
- CodeGen performance advantage increases with collection size
2. Complex Object Mapping Performance
Mapping complex objects with nullable properties, enums, DateTime, nested objects, and collections.
| Scenario | Method | Mean | Error | Ratio vs Baseline | Allocated |
|---|---|---|---|---|---|
| Full Object (all properties set) | |||||
| Manual | 161 ns | ± 9 ns | baseline | 264 B | |
| HyperMapper CodeGen | 149 ns | ± 10 ns | -7% ✅ | 184 B | |
| HyperMapper Runtime | 295 ns | ± 25 ns | +83% | 264 B | |
| AutoMapper | 370 ns | ± 41 ns | +130% | 272 B | |
| Sparse Object (with nulls) | |||||
| Manual | 86 ns | ± 16 ns | baseline | 168 B | |
| HyperMapper CodeGen | 85 ns | ± 6 ns | -1% ✅ | 136 B | |
| HyperMapper Runtime | 200 ns | ± 18 ns | +133% | 168 B | |
| AutoMapper | 246 ns | ± 21 ns | +187% | 168 B |
Key Insights:
- CodeGen is 7% faster than manual on full complex objects
- CodeGen allocates 30% less memory than manual mapping (184B vs 264B)
- CodeGen is 2.5x faster than AutoMapper on complex objects
- CodeGen is 2x faster than Runtime Mode across all complex scenarios
3. Flattening Performance
Flattening nested objects (ModelObject with Sub, Sub2, SubWithExtraName) to flat DTO.
| Method | Mean | Error | Ratio vs Baseline | Allocated |
|---|---|---|---|---|
| Manual | 51 ns | ± 6 ns | baseline | 56 B |
| HyperMapper CodeGen | 75 ns | ± 7 ns | +48% ✅ | 56 B |
| HyperMapper Runtime | 161 ns | ± 36 ns | +216% | 56 B |
| AutoMapper | 202 ns | ± 30 ns | +298% | 56 B |
Key Insights:
- 🎉 CodeGen is now 2.1x faster than Runtime! (75ns vs 161ns)
- 49x improvement from API fix (was 1,634ns with wrong API)
- CodeGen is only 1.5x slower than manual (vs 4.4x for Runtime)
- Use typed API
Map<TSource, TDest>()for best CodeGen performance
4. Deep Nesting Performance
Mapping 10 levels of nested objects (DeepLevel1 → DeepLevel10).
| Method | Mean | Error | Ratio vs Baseline | Allocated |
|---|---|---|---|---|
| Manual | 230 ns | ± 34 ns | baseline | 328 B |
| HyperMapper CodeGen | 255 ns | ± 20 ns | +11% ✅ | 328 B |
| HyperMapper Runtime | 343 ns | ± 33 ns | +49% | 328 B |
| AutoMapper | 365 ns | ± 22 ns | +59% | 328 B |
Key Insights:
- 🎉 CodeGen is now 1.3x faster than Runtime! (255ns vs 343ns)
- 2.7x improvement from API fix (was 699ns with wrong API)
- CodeGen is only 1.1x slower than manual (excellent!)
- Navigation expression optimizations contribute to performance gain
Performance Summary
CodeGen Mode (Recommended for ALL scenarios):
- ✅ Simple mapping - 4.6x faster than Runtime (32ns vs 147ns)
- ✅ Flattening - 2.1x faster than Runtime (75ns vs 161ns)
- ✅ Deep nesting - 1.3x faster than Runtime (255ns vs 343ns)
- ✅ Collection mapping - 1.6x faster than Runtime on large collections
- ✅ Complex objects - 2x faster than Runtime, 7% faster than manual
- ✅ Memory efficiency - 30% less allocation on complex objects
API Usage Best Practices:
// ✅ CORRECT - Fast Path (uses GeneratedMapperCache with typed delegates)
var result = mapper.Map<Source, Destination>(source); // ~32ns
// ❌ AVOID - Slow Path (uses DynamicInvoke with ~100-300ns overhead)
var result = mapper.Map<Destination>(source); // ~147ns+ (4.6x slower!)
Overall:
- HyperMapper CodeGen: Best performance in ALL scenarios - use in production
- HyperMapper Runtime: Good for development and prototyping
- AutoMapper: Compatible API, but 1.2-2.5x slower than HyperMapper
HyperMapper vs AutoMapper - Detailed Comparison
For teams considering migration from AutoMapper, here's a direct comparison with HyperMapper modes:
| Scenario | HyperMapper Runtime | HyperMapper CodeGen | AutoMapper | CodeGen vs AutoMapper |
|---|---|---|---|---|
| Simple Mapping | 147 ns | 32 ns | 170 ns | 5.3x faster ✅ |
| Collection Small (10) | 597 ns | 607 ns | 601 ns | +1% |
| Collection Medium (100) | 3,098 ns | 2,100 ns | 3,771 ns | 1.8x faster ✅ |
| Collection Large (1000) | 34,548 ns | 29,935 ns | 37,402 ns | 1.2x faster ✅ |
| Complex Object Full | 295 ns | 149 ns | 370 ns | 2.5x faster ✅ |
| Complex Object Sparse | 200 ns | 85 ns | 246 ns | 2.9x faster ✅ |
| Flattening | 161 ns | 75 ns | 202 ns | 2.7x faster ✅ |
| Deep Nesting (10 levels) | 343 ns | 255 ns | 365 ns | 1.4x faster ✅ |
Key Findings:
- ✅ HyperMapper CodeGen wins in 7 out of 8 scenarios
- ✅ Average 2.5x faster than AutoMapper with CodeGen Mode
- ✅ Up to 5.3x faster on simple mappings
- ✅ Use typed API
Map<TSource, TDest>()for best performance - ✅ Same memory footprint or less (30% reduction on complex objects)
Migration Recommendation: HyperMapper is a drop-in replacement for AutoMapper with significantly better performance:
- Change
using AutoMapper;tousing HyperMapper; - Add Source Generator reference for CodeGen Mode
- Use typed API:
mapper.Map<Source, Dest>(source)instead ofmapper.Map<Dest>(source) - Enjoy 2.5x average speed improvement!
Running Benchmarks
cd benchmarks/HyperMapper.Benchmarks
dotnet run -c Release
Run specific benchmark categories:
# Collection benchmarks only
dotnet run -c Release --filter "*Collection*"
# Complex object benchmarks only
dotnet run -c Release --filter "*ComplexObject*"
# Flattening benchmarks only
dotnet run -c Release --filter "*Flattening*"
# Deep nesting benchmarks only
dotnet run -c Release --filter "*DeepNesting*"
Architecture
HyperMapper uses a hybrid architecture combining runtime execution plans with optional compile-time code generation:
Runtime Path
- Configuration →
MapperConfigurationanalyzesProfileclasses - Plan Building →
ExecutionPlanBuildercompiles Expression Trees to IL - Execution → Generic static cache provides fast typed delegates
- Performance → ~147ns per simple mapping
CodeGen Path
- Compile-Time → Source Generator analyzes
Profileclasses - Generation → Creates optimized C# mapping methods
- Registration →
HyperMapperGeneratedRegistry.Initialize() - Execution → Direct method calls via
GeneratedMapperCache<TSource, TDest>, zero reflection - Performance → ~32ns per simple mapping (4.6x faster than Runtime!)
Key Components
- Mapper - Main
IMapperimplementation - TypeMap - Metadata for source→destination mappings
- ExecutionPlanBuilder - Compiles Expression Trees to IL
- MappingCodeBuilder - Generates C# code from TypeMaps
- MapperGenerator - Roslyn
IIncrementalGeneratorimplementation
License
Specify your license here
Contributing
Contributions are welcome! Please feel free to submit issues or pull requests.
Support
For questions, issues, or feature requests, please open an issue on GitHub.
Made with ❤️ for high-performance object mapping
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net8.0 is compatible. net8.0-android was computed. net8.0-browser was computed. net8.0-ios was computed. net8.0-maccatalyst was computed. net8.0-macos was computed. net8.0-tvos was computed. net8.0-windows was computed. net9.0 was computed. net9.0-android was computed. net9.0-browser was computed. net9.0-ios was computed. net9.0-maccatalyst was computed. net9.0-macos was computed. net9.0-tvos was computed. net9.0-windows was computed. net10.0 was computed. net10.0-android was computed. net10.0-browser was computed. net10.0-ios was computed. net10.0-maccatalyst was computed. net10.0-macos was computed. net10.0-tvos was computed. net10.0-windows was computed. |
-
net8.0
- Microsoft.Extensions.Logging.Abstractions (>= 9.0.0)
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.