Skip to content

AutoMapper Migration Plan

Executive Summary

AutoMapper v15+ requires a commercial license for production use. This document evaluates open-source alternatives and provides a migration strategy for the Essert.MF project.

Status: COMPLETED (2026-03-05). All AutoMapper references have been removed. See DEBT-002 for the implementation record.

Recommendation: Migrate to Mapperly - a compile-time source generator with superior performance and active maintenance.


Table of Contents

  1. Background
  2. Current State Analysis
  3. Alternative Evaluation
  4. Recommended Solution
  5. Migration Strategy
  6. Risk Assessment
  7. Implementation Checklist

Background

The Problem

On April 16, 2025, Jimmy Bogard announced that AutoMapper would become commercial software under "Lucky Penny Software". The warning message appears in logs:

[WRN] You do not have a valid license key for the Lucky Penny software AutoMapper.
This is allowed for development and testing scenarios. If you are running in production
you are required to have a licensed version.

Options

  1. Purchase License: Continue with AutoMapper under commercial terms
  2. Stay on v14.0.0: Last free version (not recommended - security/compatibility risks)
  3. Migrate: Switch to an open-source alternative

Current State Analysis

AutoMapper Usage in Essert.MF

Metric Value
Package Version AutoMapper v15.0.1
Profile Classes 2
Total Profile Lines 872
Files Using IMapper 34
Value Object Mappings 45+
Parameter System Mappings 14
Custom Converters (ConvertUsing) 123+

Profile Details

1. DomainToEfMappingProfile (260 lines)

Purpose: Maps between domain value objects and EF infrastructure entities

Mapping Categories: - Manufacturing: ProcessId, OrderNumber, SerialNumber, ProcessState - Product: ProductId, ArticleNumber, VersionNumber, VersionId - Robot: RobotId, RobotState - WorkPieceCarrier: WpcId, WpcNumber, WpcState, Position - Quality: MeasurementId, MeasurementType, MeasurementValue, QualityResult - Common: Email, Money, DateRange

Complex Converters:

// String parsing converters
ParsePosition()       - "stationId,moduleId,positionId" format
ParseMeasurementValue() - "value|unit|decimalPlaces" format
ParseMoney()          - "amount|currency" format
ParseDateRange()      - "startDate|endDate" format
ParseProcessState()   - Enum state mapping (0-4)

Entity Aggregate Mappings: - WorkPieceCarrier (Wpc → WorkPieceCarrierEntity) - Product (Productname → ProductEntity) - Robot (MasterData → Robot)

2. ParameterMappingProfile (612 lines)

Purpose: Handles Setup/Mapping pairs for 14 parameter systems

Systems Covered:

System Description Complexity
TH Tray Handling High (Process, Shape, Position, Shift)
SC Steep Conveyor Medium
EGP Robot Gripper (Schunk) Medium
RFL Refill Logic Medium
XGX XGX Vision (Keyence) Medium
Kann Stepper Motors (KocoMotion) Medium
HP Vibration Hopper (RNA) Medium
ES Essert Shaker High (6 clip sequences)
CMMT FESTO Servo Motors Medium
CL Circulation Logic Medium
GJ Gripperjaws Low
EL Essert Lighting Low
IV3 IV3 Vision (Keyence) Low
Mfconfig Microfactory Configuration Medium

Common Patterns: - .ForMember() explicit field mappings (339+ occurrences) - Navigation properties set to .Ignore() - UID handling with special primary key logic

Injection Points

Infrastructure Layer:
├── Repositories (18 files)
│   ├── ParameterMappingRepository (base class)
│   ├── ThMappingRepository
│   ├── ScMappingRepository
│   └── ... (35 total repository files)
├── Services
└── Tests (6 files)

Alternative Evaluation

Candidates Considered

Library Type Performance License Maintenance
Mapperly Source Generator Excellent MIT Active
Mapster Runtime + Source Gen Very Good MIT Seeking Maintainers
TinyMapper Runtime Good MIT Limited
AnyMapper Runtime Good MIT Community
Manual Mapping Hand-written Optimal N/A Self-maintained

Detailed Comparison

Pros: - Compile-time code generation (no runtime reflection) - Fastest performance (338.5 ns vs AutoMapper's 1,203.9 ns) - Trimming and AOT safe - No runtime dependencies - Readable, debuggable generated code - Active development and strong community - MIT License

Cons: - Different API requires code changes - Each mapping needs explicit method definition - Learning curve for source generator patterns

Migration Effort: Medium-High

Mapster

Pros: - Similar API to AutoMapper (easier migration) - Good performance (49μs vs AutoMapper's 227μs) - Supports both runtime and compile-time generation - Simpler syntax for basic mappings

Cons: - Seeking maintainers (long-term support concern) - Less active development than Mapperly - Runtime reflection still used in standard mode

Migration Effort: Low-Medium

Performance Benchmarks

Benchmark Results (nanoseconds, lower is better):

Mapperly:       338.5 ns  ████░░░░░░░░░░░░░░░░  (Fastest)
Manual:         529.6 ns  ██████░░░░░░░░░░░░░░
AutoMapper:   1,203.9 ns  █████████████░░░░░░░
Mapster:        ~800 ns   █████████░░░░░░░░░░░  (Estimated)

Primary Recommendation: Mapperly

Rationale: 1. Performance: 3.5x faster than AutoMapper 2. Safety: Compile-time validation catches mapping errors early 3. Future-proof: AOT/trimming compatible for .NET 9+ 4. Active maintenance: Strong community and regular updates 5. Architecture fit: Aligns with hexagonal architecture principles

Alternative: Mapster (If Rapid Migration Required)

Consider Mapster only if: - Immediate migration is critical - Team bandwidth is limited - Similar API reduces training time


Migration Strategy

Phase 1: Preparation (Week 1)

1.1 Add Mapperly Package

<!-- Essert.MF.Infrastructure.csproj -->
<PackageReference Include="Riok.Mapperly" Version="4.1.1" />

1.2 Create Mapper Structure

Essert.MF.Infrastructure/
└── Mapping/
    ├── DomainToEfMappingProfile.cs      (existing - to be replaced)
    ├── ParameterMappingProfile.cs        (existing - to be replaced)
    ├── Mappers/                          (new folder)
    │   ├── Domain/
    │   │   ├── ProcessIdMapper.cs
    │   │   ├── ProductMapper.cs
    │   │   ├── WpcMapper.cs
    │   │   └── ValueObjectMappers.cs
    │   └── Parameters/
    │       ├── ThMapper.cs
    │       ├── ScMapper.cs
    │       └── ... (one per system)
    └── Converters/                       (new folder)
        ├── PositionConverter.cs
        ├── MeasurementValueConverter.cs
        └── MoneyConverter.cs

Phase 2: Value Object Mappers (Week 2)

2.1 Example: ProcessId Mapper

Before (AutoMapper):

CreateMap<long, ProcessId>().ConvertUsing(src => new ProcessId(src));
CreateMap<ProcessId, long>().ConvertUsing(src => src.Value);

After (Mapperly):

[Mapper]
public static partial class ProcessIdMapper
{
    public static ProcessId ToProcessId(long value) => new ProcessId(value);
    public static long ToLong(ProcessId processId) => processId.Value;
}

2.2 Example: Position Mapper (Complex)

Before (AutoMapper):

CreateMap<string, Position>().ConvertUsing(src => ParsePosition(src));

private static Position ParsePosition(string value)
{
    var parts = value.Split(',');
    return new Position(
        int.Parse(parts[0]),
        int.Parse(parts[1]),
        int.Parse(parts[2]));
}

After (Mapperly):

[Mapper]
public static partial class PositionMapper
{
    public static Position ToPosition(string value)
    {
        if (string.IsNullOrEmpty(value)) return Position.Empty;
        var parts = value.Split(',');
        return new Position(
            int.Parse(parts[0]),
            int.Parse(parts[1]),
            int.Parse(parts[2]));
    }

    public static string ToString(Position position)
        => $"{position.StationId},{position.ModuleId},{position.PositionId}";
}

Phase 3: Entity Mappers (Week 3)

3.1 Example: Product Mapper

Before (AutoMapper):

CreateMap<Productname, ProductEntity>()
    .ForMember(dest => dest.Uid, opt => opt.MapFrom(src => src.Uid))
    .ForMember(dest => dest.Name, opt => opt.MapFrom(src => src.Name))
    .ForMember(dest => dest.Creator, opt => opt.MapFrom(src => src.Creator));

After (Mapperly):

[Mapper]
public partial class ProductMapper
{
    [MapProperty(nameof(Productname.Uid), nameof(ProductEntity.Uid))]
    [MapProperty(nameof(Productname.Name), nameof(ProductEntity.Name))]
    [MapProperty(nameof(Productname.Creator), nameof(ProductEntity.Creator))]
    public partial ProductEntity ToEntity(Productname product);

    public partial Productname ToDomain(ProductEntity entity);
}

Phase 4: Parameter System Mappers (Weeks 4-5)

4.1 Example: TH Setup Mapper

Before (AutoMapper):

CreateMap<ThSetupDto, ThSetup>()
    .ForMember(dest => dest.Uid, opt => opt.Ignore())  // AUTO_INCREMENT
    .ForMember(dest => dest.Crc, opt => opt.Ignore())
    .ForMember(dest => dest.ThMappings, opt => opt.Ignore())
    // ... 20+ ForMember calls

After (Mapperly):

[Mapper]
public partial class ThSetupMapper
{
    [MapperIgnoreTarget(nameof(ThSetup.Uid))]
    [MapperIgnoreTarget(nameof(ThSetup.Crc))]
    [MapperIgnoreTarget(nameof(ThSetup.ThMappings))]
    public partial ThSetup ToEntity(ThSetupDto dto);

    public partial ThSetupDto ToDto(ThSetup entity);
}

Phase 5: Repository Updates (Week 6)

5.1 Update Base Repository

Before:

public abstract class ParameterMappingRepository<TSetup, TMapping>
{
    protected readonly IMapper Mapper;

    public ParameterMappingRepository(IMapper mapper)
    {
        Mapper = mapper;
    }

    protected virtual TSetup MapToEntity(TSetupDto dto)
        => Mapper.Map<TSetup>(dto);
}

After:

public abstract class ParameterMappingRepository<TSetup, TMapping>
{
    protected abstract TSetup MapToEntity(TSetupDto dto);
    protected abstract TSetupDto MapToDto(TSetup entity);
}

// Each repository implements with specific mapper
public class ThMappingRepository : ParameterMappingRepository<ThSetup, ThMapping>
{
    private readonly ThSetupMapper _setupMapper = new();

    protected override ThSetup MapToEntity(ThSetupDto dto)
        => _setupMapper.ToEntity(dto);
}

Phase 6: Testing & Cleanup (Week 7)

  1. Update all unit tests
  2. Run integration tests
  3. Remove AutoMapper package reference
  4. Delete old profile classes
  5. Performance validation

Risk Assessment

High Risk Items

Risk Mitigation
Complex converters fail to migrate Create custom converter classes for ParsePosition, ParseMoney, etc.
Navigation property handling differs Use [MapperIgnoreTarget] consistently
Runtime behavior differences Comprehensive integration testing

Medium Risk Items

Risk Mitigation
Learning curve for team Documentation and code examples
Increased code volume Accept trade-off for compile-time safety
Test coverage gaps Update DomainToEfMappingProfileTests

Low Risk Items

Risk Mitigation
Package compatibility Mapperly supports .NET 9.0
DI integration changes Mapperly supports constructor injection

Implementation Checklist

Pre-Migration

  • Review all 45+ value object mappings
  • Document all custom converters
  • Identify complex mapping patterns
  • Set up branch for migration work
  • Create test baseline

Phase 1: Setup

  • Add Riok.Mapperly package
  • Create Mappers folder structure
  • Create Converters folder structure

Phase 2: Value Objects

  • ProcessId, OrderNumber, SerialNumber
  • ProductId, ArticleNumber, VersionNumber, VersionId
  • RobotId, RobotState
  • WpcId, WpcNumber, WpcState, Position
  • MeasurementId, MeasurementType, MeasurementValue
  • QualityResult, Email, Money, DateRange

Phase 3: Entities

  • WorkPieceCarrierEntity mapper
  • ProductEntity mapper
  • Robot mapper

Phase 4: Parameter Systems

  • TH (Tray Handling) - Setup + Mapping
  • SC (Steep Conveyor) - Setup + Mapping
  • EGP (Robot Gripper) - Setup + Mapping
  • RFL (Refill Logic) - Setup + Mapping
  • XGX (XGX Vision) - Setup + Mapping
  • Kann (Stepper Motors) - Setup + Mapping
  • HP (Vibration Hopper) - Setup + Mapping
  • ES (Essert Shaker) - Setup + Mapping
  • CMMT (FESTO Servo Motors) - Setup + Mapping
  • CL (Circulation Logic) - Setup + Mapping
  • GJ (Gripperjaws) - Setup + Mapping
  • EL (Essert Lighting) - Setup + Mapping
  • IV3 (IV3 Vision) - Setup + Mapping
  • Mfconfig - Setup + Mapping

Phase 5: Repositories

  • Update ParameterMappingRepository base class
  • Update all 18 repository implementations
  • Update ServiceCollectionExtensions

Phase 6: Testing & Cleanup

  • Update DomainToEfMappingProfileTests
  • Run all integration tests
  • Run performance benchmarks
  • Remove AutoMapper package
  • Delete old profile classes
  • Update documentation

References

Documentation

Articles

Alternative Libraries


Document History

Version Date Author Changes
1.0 2026-01-22 Claude Code Initial plan created