-
Notifications
You must be signed in to change notification settings - Fork 0
Home
- Introduction
- Getting Started
- Basic Usage
- Advanced Features
- Mapping Modules
- Dependency Injection
- Performance Considerations
- API Reference
- Best Practices
- Troubleshooting
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.
- 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
dotnet add package TurboMapperusing 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);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 = 28TurboMapper 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 automaticallyTurboMapper 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.ActiveTurboMapper 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 nullConfigure 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 = 30Map 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);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);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 provide a clean, reusable way to organize mapping configurations.
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);
};
}
}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);
};
}
}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
};
}
}Mapping modules are automatically discovered and registered when using dependency injection:
services.AddTurboMapper();
// All mapping modules in loaded assemblies are automatically registeredusing Microsoft.Extensions.DependencyInjection;
using TurboMapper;
var services = new ServiceCollection();
services.AddTurboMapper();
var serviceProvider = services.BuildServiceProvider();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// 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();
}
}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
}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);
});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));
}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]);
}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
}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
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
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
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
internal class PropertyMapping
{
public string SourcePropertyPath { get; set; }
public string TargetPropertyPath { get; set; }
}Always register TurboMapper with DI for automatic module discovery:
services.AddTurboMapper();Group related mappings in modules:
// UserMappings.cs
internal class UserDTOMappingModule : MappingModule<UserDTO, UserEntity>
{
// ...
}
internal class UserEntityMappingModule : MappingModule<UserEntity, UserDTO>
{
// ...
}Unless you need strict control, enable default mapping:
public MyMappingModule() : base(enableDefaultMapping: true)
{
// Only specify differences from default
}Always check for null in business logic when needed:
var entity = mapper.Map<DTO, Entity>(dto);
if (entity?.Address == null)
{
entity.Address = new Address();
}Leverage lambda expressions for compile-time safety:
config.ForMember(dest => dest.FullName, src => src.FirstName)
.ForMember(dest => dest.Years, src => src.Age);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);
}Keep object hierarchies reasonable for performance:
// Good - 2-3 levels
User.Address.City
// Potentially slow - 5+ levels
User.Profile.Settings.Preferences.Display.ThemeDon'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
}Problem: Properties with matching names aren't mapping.
Solutions:
- Ensure properties have public getters and setters
- Check property names match exactly (case-sensitive)
- Verify target property has a setter (
setaccessor)
// 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; }
}Problem: Nested objects aren't being created.
Solutions:
- Check if source nested object is null
- Ensure nested class has parameterless constructor
- Verify property types match or are compatible
public class Address
{
// Must have parameterless constructor
public Address() { }
public string Street { get; set; }
}Problem: Automatic type conversion not working.
Solutions:
- Check if types are compatible for conversion
- Implement explicit mapping for complex conversions
- 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 handlingProblem: Mapping module not being discovered.
Solutions:
- Ensure module class is not abstract
- Make module class internal or public (not private)
- Verify assembly is loaded in AppDomain
- 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>
{
// ...
}Problem: Mapping is slow with large datasets.
Solutions:
- Reuse mapper instance (singleton)
- Configure mappings once, not repeatedly
- Use Parallel processing for large collections
- Profile to identify specific bottlenecks
// Efficient approach
var mapper = serviceProvider.GetService<IMapper>();
var results = sources
.AsParallel()
.Select(s => mapper.Map<Source, Target>(s))
.ToList();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;
}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");
}Verify assemblies containing modules are loaded:
var assemblies = AppDomain.CurrentDomain.GetAssemblies();
foreach (var assembly in assemblies)
{
Console.WriteLine($"Loaded: {assembly.FullName}");
}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;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();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);
};
}
}- Initial release
- Default name-based mapping
- Custom property mapping
- Nested object support
- Type conversion
- Mapping modules
- Dependency injection integration
- Multi-framework support
TurboMapper is licensed under the terms specified in the LICENSE file.
For issues, questions, or contributions:
- GitHub: https://github.com/CodeShayk/TurboMapper
- Wiki: https://github.com/CodeShayk/TurboMapper/wiki
Contributions are welcome! Please submit pull requests or open issues on GitHub.