Skip to content
CØDE N!NJΔ edited this page Oct 9, 2025 · 3 revisions

TurboMapper - Complete Guide

Table of Contents

  1. Introduction
  2. Getting Started
  3. Basic Usage
  4. Advanced Features
  5. Mapping Modules
  6. Dependency Injection
  7. Performance Considerations
  8. API Reference
  9. Best Practices
  10. Troubleshooting

Introduction

TurboMapper is a lightweight, high-performance object mapper for .NET that provides both shallow and deep mapping capabilities. It serves as a free alternative to AutoMapper with a simple, intuitive API.

Key Features

  • Automatic Property Mapping: Maps properties by name automatically
  • Custom Property Mapping: Define explicit mappings between different property names
  • Nested Object Support: Handles deep object hierarchies seamlessly
  • Type Conversion: Automatic conversion between compatible types
  • Mapping Modules: Organize mappings in reusable modules
  • Dependency Injection: First-class support for Microsoft.Extensions.DependencyInjection
  • Thread-Safe: Safe for concurrent operations
  • Multi-Framework Support: Compatible with .NET Framework 4.6.2, .NET Standard 2.0/2.1, and .NET 9.0

Installation

dotnet add package TurboMapper

Getting Started

Quick Start Example

using TurboMapper;
using Microsoft.Extensions.DependencyInjection;

// Setup
var services = new ServiceCollection();
services.AddTurboMapper();
var serviceProvider = services.BuildServiceProvider();
var mapper = serviceProvider.GetService<IMapper>();

// Define models
public class Source
{
    public string Name { get; set; }
    public int Age { get; set; }
}

public class Target
{
    public string Name { get; set; }
    public int Age { get; set; }
}

// Map objects
var source = new Source { Name = "John Doe", Age = 30 };
var target = mapper.Map<Source, Target>(source);

Basic Usage

Default Name-Based Mapping

TurboMapper automatically maps properties with matching names:

public class UserDTO
{
    public string FirstName { get; set; }
    public string LastName { get; set; }
    public int Age { get; set; }
}

public class UserEntity
{
    public string FirstName { get; set; }
    public string LastName { get; set; }
    public int Age { get; set; }
}

var dto = new UserDTO 
{ 
    FirstName = "Jane", 
    LastName = "Smith", 
    Age = 28 
};

var entity = mapper.Map<UserDTO, UserEntity>(dto);
// entity.FirstName = "Jane"
// entity.LastName = "Smith"
// entity.Age = 28

Nested Object Mapping

TurboMapper automatically handles nested objects:

public class Address
{
    public string Street { get; set; }
    public string City { get; set; }
    public string ZipCode { get; set; }
}

public class Person
{
    public string Name { get; set; }
    public Address Address { get; set; }
}

var source = new Person
{
    Name = "John",
    Address = new Address
    {
        Street = "123 Main St",
        City = "Boston",
        ZipCode = "02101"
    }
};

var target = mapper.Map<Person, Person>(source);
// All nested properties are mapped automatically

Type Conversion

TurboMapper handles common type conversions:

public class Source
{
    public int Age { get; set; }
    public string Status { get; set; }
}

public class Target
{
    public string Age { get; set; }  // int to string
    public StatusEnum Status { get; set; }  // string to enum
}

public enum StatusEnum { Active, Inactive }

var source = new Source { Age = 25, Status = "Active" };
var target = mapper.Map<Source, Target>(source);
// target.Age = "25"
// target.Status = StatusEnum.Active

Null Handling

TurboMapper gracefully handles null values:

// Null source returns null
Source source = null;
var target = mapper.Map<Source, Target>(source); // returns null

// Null nested objects are preserved
var person = new Person { Name = "John", Address = null };
var mapped = mapper.Map<Person, Person>(person);
// mapped.Address is null

Advanced Features

Explicit Property Mapping

Configure custom mappings without modules:

using TurboMapper.Impl;

var mapper = new Mapper();
var mappings = new List<PropertyMapping>
{
    new PropertyMapping
    {
        SourcePropertyPath = "FirstName",
        TargetPropertyPath = "FullName"
    },
    new PropertyMapping
    {
        SourcePropertyPath = "Age",
        TargetPropertyPath = "Years"
    }
};

mapper.CreateMap<UserSource, UserTarget>(mappings);

var source = new UserSource { FirstName = "John", Age = 30 };
var target = mapper.Map<UserSource, UserTarget>(source);
// target.FullName = "John"
// target.Years = 30

Nested Property Flattening

Map nested properties to flat structure:

public class Source
{
    public string Name { get; set; }
    public Address Address { get; set; }
}

public class FlatTarget
{
    public string Name { get; set; }
    public string Street { get; set; }
    public string City { get; set; }
}

var mappings = new List<PropertyMapping>
{
    new PropertyMapping
    {
        SourcePropertyPath = "Address.Street",
        TargetPropertyPath = "Street"
    },
    new PropertyMapping
    {
        SourcePropertyPath = "Address.City",
        TargetPropertyPath = "City"
    }
};

mapper.CreateMap<Source, FlatTarget>(mappings);

Nested Property Mapping

Map flat structure to nested objects:

public class TargetWithNested
{
    public string Name { get; set; }
    public AddressConfig Address { get; set; }
}

public class AddressConfig
{
    public string StreetName { get; set; }
    public string Location { get; set; }
}

var mappings = new List<PropertyMapping>
{
    new PropertyMapping
    {
        SourcePropertyPath = "Address.Street",
        TargetPropertyPath = "Address.StreetName"
    },
    new PropertyMapping
    {
        SourcePropertyPath = "Address.City",
        TargetPropertyPath = "Address.Location"
    }
};

mapper.CreateMap<Source, TargetWithNested>(mappings);

Disabling Default Mapping

Control which properties get mapped:

var mappings = new List<PropertyMapping>
{
    new PropertyMapping
    {
        SourcePropertyPath = "FirstName",
        TargetPropertyPath = "Name"
    }
};

// Only map explicitly defined properties
mapper.CreateMap<Source, Target>(mappings, enableDefaultMapping: false);

Mapping Modules

Mapping modules provide a clean, reusable way to organize mapping configurations.

Creating a Mapping Module

using TurboMapper;
using System;

internal class UserMappingModule : MappingModule<UserSource, UserTarget>
{
    public UserMappingModule() : base(enableDefaultMapping: true)
    {
    }

    public override Action<IMappingExpression<UserSource, UserTarget>> CreateMappings()
    {
        return config =>
        {
            config.ForMember(dest => dest.Name, src => src.FirstName)
                  .ForMember(dest => dest.Years, src => src.Age);
        };
    }
}

Module with Nested Mappings

internal class AddressMappingModule : MappingModule<AddressSource, AddressTarget>
{
    public AddressMappingModule() : base(enableDefaultMapping: true)
    {
    }

    public override Action<IMappingExpression<AddressSource, AddressTarget>> CreateMappings()
    {
        return config =>
        {
            config.ForMember(dest => dest.Address.Street, src => src.Address.StreetName)
                  .ForMember(dest => dest.Address.Location, src => src.Address.City);
        };
    }
}

Module Without Default Mapping

When you want complete control over what gets mapped:

internal class StrictMappingModule : MappingModule<Source, Target>
{
    public StrictMappingModule() : base(enableDefaultMapping: false)
    {
    }

    public override Action<IMappingExpression<Source, Target>> CreateMappings()
    {
        return config =>
        {
            config.ForMember(dest => dest.Name, src => src.FirstName)
                  .ForMember(dest => dest.Years, src => src.Age);
            // Only these properties will be mapped
        };
    }
}

Module Auto-Discovery

Mapping modules are automatically discovered and registered when using dependency injection:

services.AddTurboMapper();
// All mapping modules in loaded assemblies are automatically registered

Dependency Injection

Registration

using Microsoft.Extensions.DependencyInjection;
using TurboMapper;

var services = new ServiceCollection();
services.AddTurboMapper();
var serviceProvider = services.BuildServiceProvider();

Singleton Behavior

The mapper is registered as a singleton and thread-safe:

var mapper1 = serviceProvider.GetService<IMapper>();
var mapper2 = serviceProvider.GetService<IMapper>();
// mapper1 and mapper2 are the same instance

Using in ASP.NET Core

// Startup.cs or Program.cs
public void ConfigureServices(IServiceCollection services)
{
    services.AddTurboMapper();
    services.AddControllers();
}

// In a controller
public class UserController : ControllerBase
{
    private readonly IMapper _mapper;

    public UserController(IMapper mapper)
    {
        _mapper = mapper;
    }

    [HttpPost]
    public IActionResult CreateUser(UserDTO dto)
    {
        var entity = _mapper.Map<UserDTO, UserEntity>(dto);
        // Save entity
        return Ok();
    }
}

Scoped Services

The mapper works correctly across service scopes:

using (var scope1 = serviceProvider.CreateScope())
using (var scope2 = serviceProvider.CreateScope())
{
    var mapper1 = scope1.ServiceProvider.GetService<IMapper>();
    var mapper2 = scope2.ServiceProvider.GetService<IMapper>();
    // Both resolve to the same singleton instance
}

Performance Considerations

Thread Safety

TurboMapper is fully thread-safe for concurrent mapping operations:

var sources = GetLargeDataSet();
var results = new ConcurrentBag<Target>();

Parallel.ForEach(sources, source =>
{
    var target = mapper.Map<Source, Target>(source);
    results.Add(target);
});

Batch Processing

For large datasets, process in batches:

var sources = GetLargeDataSet(); // 10,000 items
var targets = new List<Target>(sources.Count);

foreach (var source in sources)
{
    targets.Add(mapper.Map<Source, Target>(source));
}

Reusing Mapper Instance

Always reuse the same mapper instance (singleton pattern):

// Good - Reuse singleton
var mapper = serviceProvider.GetService<IMapper>();
for (int i = 0; i < 1000; i++)
{
    var result = mapper.Map<Source, Target>(sources[i]);
}

// Bad - Creating new instances
for (int i = 0; i < 1000; i++)
{
    var mapper = new Mapper(); // Don't do this
    var result = mapper.Map<Source, Target>(sources[i]);
}

Configuration Caching

Mapping configurations are cached internally, so repeated mappings are efficient:

mapper.CreateMap<Source, Target>(mappings);

// Subsequent calls use cached configuration
for (int i = 0; i < 10000; i++)
{
    mapper.Map<Source, Target>(sources[i]); // Fast
}

API Reference

IMapper Interface

public interface IMapper
{
    TTarget Map<TSource, TTarget>(TSource source);
}

Map Method

  • Parameters:
    • TSource source - The source object to map from
  • Returns: TTarget - The mapped target object
  • Returns null if: Source is null

IMappingExpression Interface

public interface IMappingExpression<TSource, TTarget>
{
    IMappingExpression<TSource, TTarget> ForMember<TValue>(
        Expression<Func<TTarget, TValue>> targetMember, 
        Expression<Func<TSource, TValue>> sourceMember);
}

ForMember Method

  • Configures custom mapping between properties
  • Supports nested property paths
  • Returns the expression for fluent chaining

MappingModule Abstract Class

public abstract class MappingModule<TSource, TTarget>
{
    protected MappingModule(bool enableDefaultMapping = true);
    
    public abstract Action<IMappingExpression<TSource, TTarget>> CreateMappings();
}

Constructor Parameters

  • enableDefaultMapping - When true, unmapped properties use name-based mapping

ServiceCollectionExtensions

public static class ServiceCollectionExtensions
{
    public static IServiceCollection AddTurboMapper(
        this IServiceCollection services);
}

AddTurboMapper Method

  • Registers IMapper as singleton
  • Auto-discovers and registers mapping modules
  • Returns IServiceCollection for chaining

PropertyMapping Class

internal class PropertyMapping
{
    public string SourcePropertyPath { get; set; }
    public string TargetPropertyPath { get; set; }
}

Best Practices

1. Use Dependency Injection

Always register TurboMapper with DI for automatic module discovery:

services.AddTurboMapper();

2. Organize with Mapping Modules

Group related mappings in modules:

// UserMappings.cs
internal class UserDTOMappingModule : MappingModule<UserDTO, UserEntity>
{
    // ...
}

internal class UserEntityMappingModule : MappingModule<UserEntity, UserDTO>
{
    // ...
}

3. Enable Default Mapping

Unless you need strict control, enable default mapping:

public MyMappingModule() : base(enableDefaultMapping: true)
{
    // Only specify differences from default
}

4. Handle Null Values

Always check for null in business logic when needed:

var entity = mapper.Map<DTO, Entity>(dto);
if (entity?.Address == null)
{
    entity.Address = new Address();
}

5. Use Type-Safe Expressions

Leverage lambda expressions for compile-time safety:

config.ForMember(dest => dest.FullName, src => src.FirstName)
      .ForMember(dest => dest.Years, src => src.Age);

6. Test Your Mappings

Always unit test custom mappings:

[Test]
public void Map_UserDTOToEntity_MapsCorrectly()
{
    var dto = new UserDTO { FirstName = "John", Age = 30 };
    var entity = mapper.Map<UserDTO, UserEntity>(dto);
    
    Assert.AreEqual("John", entity.Name);
    Assert.AreEqual(30, entity.Years);
}

7. Avoid Deep Nesting

Keep object hierarchies reasonable for performance:

// Good - 2-3 levels
User.Address.City

// Potentially slow - 5+ levels
User.Profile.Settings.Preferences.Display.Theme

8. Reuse Mapping Configurations

Don't create mappings repeatedly:

// Good - Create once
mapper.CreateMap<Source, Target>(mappings);

// Bad - Inside loop
for (int i = 0; i < 1000; i++)
{
    mapper.CreateMap<Source, Target>(mappings); // Don't do this
}

Troubleshooting

Common Issues

Properties Not Mapping

Problem: Properties with matching names aren't mapping.

Solutions:

  1. Ensure properties have public getters and setters
  2. Check property names match exactly (case-sensitive)
  3. Verify target property has a setter (set accessor)
// Won't map - no setter
public class Target
{
    public string Name { get; } // Read-only
}

// Will map - has setter
public class Target
{
    public string Name { get; set; }
}

Nested Objects Are Null

Problem: Nested objects aren't being created.

Solutions:

  1. Check if source nested object is null
  2. Ensure nested class has parameterless constructor
  3. Verify property types match or are compatible
public class Address
{
    // Must have parameterless constructor
    public Address() { }
    
    public string Street { get; set; }
}

Type Conversion Fails

Problem: Automatic type conversion not working.

Solutions:

  1. Check if types are compatible for conversion
  2. Implement explicit mapping for complex conversions
  3. Ensure enum names match string values (case-insensitive)
// Works - Compatible types
int age = 25;
string ageStr = age.ToString(); // TurboMapper handles this

// May fail - Incompatible types
object obj = new MyClass();
int value = (int)obj; // Need explicit handling

Mapping Module Not Found

Problem: Mapping module not being discovered.

Solutions:

  1. Ensure module class is not abstract
  2. Make module class internal or public (not private)
  3. Verify assembly is loaded in AppDomain
  4. Check that AddTurboMapper() is called
// Correct - Will be discovered
internal class MyMappingModule : MappingModule<Source, Target>
{
    // ...
}

// Wrong - Won't be discovered
private class MyMappingModule : MappingModule<Source, Target>
{
    // ...
}

Performance Issues

Problem: Mapping is slow with large datasets.

Solutions:

  1. Reuse mapper instance (singleton)
  2. Configure mappings once, not repeatedly
  3. Use Parallel processing for large collections
  4. Profile to identify specific bottlenecks
// Efficient approach
var mapper = serviceProvider.GetService<IMapper>();
var results = sources
    .AsParallel()
    .Select(s => mapper.Map<Source, Target>(s))
    .ToList();

Debug Tips

Enable Detailed Exceptions

Wrap mapping calls to get better error messages:

try
{
    var result = mapper.Map<Source, Target>(source);
}
catch (Exception ex)
{
    Console.WriteLine($"Mapping failed: {ex.Message}");
    Console.WriteLine($"Stack trace: {ex.StackTrace}");
    throw;
}

Verify Configuration

Test that your configuration is registered:

var mapper = serviceProvider.GetService<IMapper>();
var source = new Source { Name = "Test" };

try
{
    var target = mapper.Map<Source, Target>(source);
    Console.WriteLine("Mapping successful");
}
catch
{
    Console.WriteLine("Mapping configuration missing");
}

Check Assembly Loading

Verify assemblies containing modules are loaded:

var assemblies = AppDomain.CurrentDomain.GetAssemblies();
foreach (var assembly in assemblies)
{
    Console.WriteLine($"Loaded: {assembly.FullName}");
}

Examples

Example 1: Simple DTO Mapping

public class CreateUserRequest
{
    public string FirstName { get; set; }
    public string LastName { get; set; }
    public string Email { get; set; }
}

public class User
{
    public string FirstName { get; set; }
    public string LastName { get; set; }
    public string Email { get; set; }
    public DateTime CreatedAt { get; set; }
}

// Mapping
var request = new CreateUserRequest
{
    FirstName = "John",
    LastName = "Doe",
    Email = "john@example.com"
};

var user = mapper.Map<CreateUserRequest, User>(request);
user.CreatedAt = DateTime.UtcNow;

Example 2: Complex Business Object

public class OrderDTO
{
    public string OrderNumber { get; set; }
    public CustomerDTO Customer { get; set; }
    public List<OrderItemDTO> Items { get; set; }
}

public class OrderEntity
{
    public string OrderNumber { get; set; }
    public CustomerEntity Customer { get; set; }
    public List<OrderItemEntity> Items { get; set; }
}

// Map with nested objects
var dto = GetOrderDTO();
var entity = mapper.Map<OrderDTO, OrderEntity>(dto);

// Map collection
entity.Items = dto.Items
    .Select(item => mapper.Map<OrderItemDTO, OrderItemEntity>(item))
    .ToList();

Example 3: Flattening Hierarchy

internal class OrderFlatteningModule : MappingModule<Order, OrderFlat>
{
    public OrderFlatteningModule() : base(true)
    {
    }

    public override Action<IMappingExpression<Order, OrderFlat>> CreateMappings()
    {
        return config =>
        {
            config.ForMember(dest => dest.CustomerName, src => src.Customer.Name)
                  .ForMember(dest => dest.CustomerEmail, src => src.Customer.Email)
                  .ForMember(dest => dest.ShippingStreet, src => src.ShippingAddress.Street)
                  .ForMember(dest => dest.ShippingCity, src => src.ShippingAddress.City);
        };
    }
}

Version History

v1.0.0

  • Initial release
  • Default name-based mapping
  • Custom property mapping
  • Nested object support
  • Type conversion
  • Mapping modules
  • Dependency injection integration
  • Multi-framework support

License

TurboMapper is licensed under the terms specified in the LICENSE file.


Support

For issues, questions, or contributions:


Contributing

Contributions are welcome! Please submit pull requests or open issues on GitHub.

Clone this wiki locally