Table of Contents

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:

  1. [Mapper] Attribute: Tells Mapperly to generate mapping code for this class
  2. partial Class: Required for source generators to add generated code
  3. partial Methods: Mapperly will generate the implementation for these methods
  4. IMapper<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

  1. No Reflection: All mapping code is generated at compile time
  2. Optimized Code: Generated code is hand-optimized equivalent
  3. No Boxing: Value types are handled efficiently
  4. Minimal Allocations: Only allocates the destination object
  5. 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>