Mapperly Implementation
Riok.Mapperly is a high-performance source generator for object mapping that creates mapping code at compile time. The Arbiter framework supports Mapperly as an alternative to the built-in MapperBase implementation, providing excellent performance with minimal runtime overhead.
Overview
Mapperly generates mapping code using source generators, which means:
- Zero runtime reflection: All mapping code is generated at compile time
- Excellent performance: Generated code is optimized and fast
- Type safety: Compile-time validation of mappings
- IntelliSense support: Generated methods are available in IDE
- AOT compatibility: Works with Native AOT compilation
Installation
Add the Mapperly package to your project:
<PackageReference Include="Riok.Mapperly" Version="4.2.1" ExcludeAssets="runtime" PrivateAssets="all" />
Note: The ExcludeAssets="runtime" PrivateAssets="all" attributes ensure that Mapperly is only used during compilation and doesn't add runtime dependencies.
Basic Implementation
Creating a Mapperly Mapper
To create a Mapperly mapper that integrates with the Arbiter framework, implement the IMapper<TSource, TDestination> interface:
using System.Diagnostics.CodeAnalysis;
using Arbiter.CommandQuery.Definitions;
using Riok.Mapperly.Abstractions;
[Mapper]
public partial class PriorityMapperly : IMapper<Priority, PriorityReadModel>
{
[return: NotNullIfNotNull(nameof(source))]
public partial PriorityReadModel? Map(Priority? source);
public partial void Map(Priority source, PriorityReadModel destination);
public partial IQueryable<PriorityReadModel> ProjectTo(IQueryable<Priority> source);
}
Key Components:
[Mapper]Attribute: Tells Mapperly to generate mapping code for this classpartialClass: Required for source generators to add generated codepartialMethods: Mapperly will generate the implementation for these methodsIMapper<TSource, TDestination>: Ensures compatibility with Arbiter's mapping system
Generated Code
Mapperly will generate efficient mapping implementations. For example:
// Generated by Mapperly
public partial PriorityReadModel? Map(Priority? source)
{
if (source is null)
return default;
var target = new PriorityReadModel()
{
Id = source.Id,
Name = source.Name,
Description = source.Description,
DisplayOrder = source.DisplayOrder,
IsActive = source.IsActive,
Created = source.Created,
CreatedBy = source.CreatedBy,
Updated = source.Updated,
UpdatedBy = source.UpdatedBy,
};
return target;
}
Advanced Configuration
Ignoring Properties
Use Mapperly attributes to control mapping behavior:
[Mapper]
public partial class UserMapper : IMapper<User, UserDto>
{
[MapperIgnoreTarget(nameof(UserDto.CalculatedField))]
[MapperIgnoreSource(nameof(User.InternalProperty))]
[return: NotNullIfNotNull(nameof(source))]
public partial UserDto? Map(User? source);
[MapperIgnoreTarget(nameof(UserDto.CalculatedField))]
[MapperIgnoreSource(nameof(User.InternalProperty))]
public partial void Map(User source, UserDto destination);
public partial IQueryable<UserDto> ProjectTo(IQueryable<User> source);
}
Property Mapping Configuration
For complex mapping scenarios, you can configure property mappings:
[Mapper]
public partial class OrderMapper : IMapper<Order, OrderDto>
{
[MapProperty(nameof(Order.Customer.Name), nameof(OrderDto.CustomerName))]
[MapProperty(nameof(Order.Items.Count), nameof(OrderDto.ItemCount))]
[return: NotNullIfNotNull(nameof(source))]
public partial OrderDto? Map(Order? source);
[MapProperty(nameof(Order.Customer.Name), nameof(OrderDto.CustomerName))]
[MapProperty(nameof(Order.Items.Count), nameof(OrderDto.ItemCount))]
public partial void Map(Order source, OrderDto destination);
public partial IQueryable<OrderDto> ProjectTo(IQueryable<Order> source);
}
Custom Mapping Methods
You can provide custom mapping logic for specific properties:
[Mapper]
public partial class UserMapper : IMapper<User, UserDto>
{
[return: NotNullIfNotNull(nameof(source))]
public partial UserDto? Map(User? source);
public partial void Map(User source, UserDto destination);
public partial IQueryable<UserDto> ProjectTo(IQueryable<User> source);
// Custom mapping method for complex transformations
private string MapFullName(User user) => $"{user.FirstName} {user.LastName}";
// Custom date formatting
private string FormatDate(DateTime date) => date.ToString("yyyy-MM-dd");
}
Registration and Usage
Service Registration
Register Mapperly mappers in your dependency injection container:
// The [RegisterSingleton] attribute handles registration automatically
// Or register manually:
services.AddSingleton<IMapper<User, UserDto>, UserMapper>();
services.AddSingleton<IMapper<UserDto, User>, UserDtoMapper>();
// Register the ServiceProviderMapper for generic access
services.AddSingleton<IMapper, ServiceProviderMapper>();
Usage in Services
Use Mapperly mappers the same way as other Arbiter mappers:
public class UserService
{
private readonly IMapper<User, UserDto> _userMapper;
public UserService(IMapper<User, UserDto> userMapper)
{
_userMapper = userMapper;
}
public UserDto GetUserDto(User user)
{
return _userMapper.Map(user);
}
public async Task<List<UserDto>> GetUserDtosAsync()
{
return await _dbContext.Users
.ProjectTo(_userMapper)
.ToListAsync();
}
}
Performance Benefits
Mapperly provides excellent performance characteristics:
Benchmark Results
Based on the Arbiter framework benchmarks:
| Method | Mean | Ratio | Allocated |
|---|---|---|---|
| Manual Mapping | 15.2 ns | 1.00 | - |
| Mapperly | 16.8 ns | 1.11 | - |
| Arbiter MapperBase | 24.3 ns | 1.60 | - |
| AutoMapper | 89.4 ns | 5.88 | 80 B |
Why Mapperly is Fast
- No Reflection: All mapping code is generated at compile time
- Optimized Code: Generated code is hand-optimized equivalent
- No Boxing: Value types are handled efficiently
- Minimal Allocations: Only allocates the destination object
- Inlined Operations: Simple mappings are inlined by the JIT compiler
Comparison with MapperBase
| Aspect | Mapperly | MapperBase |
|---|---|---|
| Performance | Excellent (compile-time generated) | Very Good (compiled expressions) |
| Setup Complexity | Low (attributes only) | Medium (expression writing) |
| Flexibility | Good (attributes and custom methods) | Excellent (full expression control) |
| Compile-time Safety | Excellent | Good |
| Query Translation | Good (basic LINQ support) | Excellent (full expression support) |
| AOT Compatibility | Excellent | Limited (requires reflection) |
| Code Generation | Built-in source generator | Template-based |
Best Practices
Use Specific Interfaces
// ✅ Preferred - Direct injection of specific mapper
public UserService(IMapper<User, UserDto> userMapper) { }
// ❌ Avoid unless you need generic mapping
public UserService(IMapper genericMapper) { }
Use Meaningful Mapper Names
// ✅ Clear and descriptive
public partial class UserToUserDtoMapper : IMapper<User, UserDto>
// ❌ Generic and unclear
public partial class UserMapper : IMapper<User, UserDto>