A zero-dependency, high-performance object mapper for .NET. 2x faster than AutoMapper with zero configuration.
$ dotnet add package MapsicleMapsicle is a high-performance, modular object mapping ecosystem for .NET. Choose only what you need:
| Package | Purpose | Dependencies |
|---|---|---|
| Mapsicle | Zero-config mapping | None |
| Mapsicle.Fluent | Fluent configuration + Profiles | Mapsicle |
| Mapsicle.EntityFramework | EF Core ProjectTo<T>() | Mapsicle.Fluent |
| Mapsicle.Validation | FluentValidation integration | Mapsicle.Fluent |
| Mapsicle.NamingConventions | Naming convention support | Mapsicle.Fluent |
| Mapsicle.Json | JSON serialization integration | Mapsicle.Fluent |
| Mapsicle.AspNetCore | ASP.NET Core Minimal API helpers | Mapsicle.Validation |
| Mapsicle.Caching | Memory/Distributed cache support | Mapsicle.Fluent |
| Mapsicle.Audit | Change tracking/diff detection | Mapsicle.Fluent |
| Mapsicle.DataAnnotations | DataAnnotations validation | Mapsicle.Fluent |
The core
Mapsiclepackage has zero dependencies. Extension packages introduce their respective third-party dependencies (listed in the table above).
"The fastest mapping is the one you don't have to configure."
⚠️ AutoMapper is now commercial software. As of July 2025, AutoMapper requires a paid license for commercial use. Mapsicle is 100% free and MPL 2.0 licensed forever.
| Feature | Mapsicle | AutoMapper | Mapperly |
|---|---|---|---|
| License | MPL 2.0 (Free) | Commercial | MIT (Free) |
| Architecture | Runtime + Caching | Runtime + Expressions | Source Generator |
| Setup Required | None | Profiles, DI | Partial class |
| Dependencies | 0 (core) | 5+ | 0 (compile-time) |
| Compile-time Safety | Partial | No | Full |
| AOT Compatible | Partial | No | Yes |
| Circular Refs | Handled | Crash | N/A |
| Memory Bounded | LRU Option | No | N/A |
| Cache Statistics | Yes | No | N/A |
| Integrated Validation | Yes | No | No |
| ASP.NET Core Helpers | Yes | No | No |
| Feature | Mapsicle | AutoMapper | Mapperly |
|---|---|---|---|
| Convention-based mapping | ✅ | ✅ | ✅ |
Flattening (Address.City → AddressCity) | ✅ | ✅ | ✅ |
| Custom member mapping | ✅ ForMember() | ✅ ForMember() | ✅ [MapProperty] |
| Ignore members | ✅ [IgnoreMap] | ✅ Ignore() | ✅ [MapperIgnore] |
| Reverse mapping | ✅ ReverseMap() | ✅ ReverseMap() | ✅ (define both) |
| Before/After map hooks | ✅ | ✅ | ✅ |
| Type converters | ✅ CreateConverter<>() | ✅ ConvertUsing() | ✅ User methods |
| Inheritance/Polymorphism | ✅ Include<>() | ✅ Include<>() | ✅ |
| Nested object mapping | ✅ | ✅ | ✅ |
| Collection mapping | ✅ | ✅ | ✅ |
| Constructor mapping | ✅ ConstructUsing() | ✅ ConstructUsing() | ✅ (automatic) |
| Feature | Mapsicle | AutoMapper | Mapperly |
|---|---|---|---|
| Profile support | ✅ MapsicleProfile | ✅ Profile | ❌ (partial classes) |
| Fluent configuration | ✅ | ✅ | ❌ (attributes) |
| Attribute-based config | ✅ [MapFrom] | ✅ | ✅ |
| Static zero-config API | ✅ obj.MapTo<T>() | ❌ | ❌ |
| DI-friendly | ✅ IMapper | ✅ IMapper | ✅ |
| Assembly scanning | ✅ | ✅ | N/A |
| Package/Feature | Mapsicle | AutoMapper | Mapperly |
|---|---|---|---|
| EF Core ProjectTo | ✅ Mapsicle.EntityFramework | ✅ Built-in | ✅ (expressions) |
| FluentValidation | ✅ Mapsicle.Validation | ❌ | ❌ |
| DataAnnotations | ✅ Mapsicle.DataAnnotations | ❌ | ❌ |
| JSON serialization | ✅ Mapsicle.Json | ❌ | ❌ |
| ASP.NET Core | ✅ Mapsicle.AspNetCore | ❌ | ❌ |
| Caching | ✅ Mapsicle.Caching | ❌ | N/A |
| Audit/Change tracking | ✅ Mapsicle.Audit | ❌ | ❌ |
| Naming conventions | ✅ 5 conventions | ✅ Built-in | ✅ NamingStrategy |
| Convention | Mapsicle | AutoMapper | Mapperly |
|---|---|---|---|
| PascalCase | ✅ | ✅ | ✅ |
| camelCase | ✅ | ✅ | ✅ |
| snake_case | ✅ | ✅ | ✅ |
| kebab-case | ✅ | ❌ | ❌ |
| SCREAMING_SNAKE_CASE | ✅ | ❌ | ❌ |
| Aspect | Mapsicle | AutoMapper | Mapperly |
|---|---|---|---|
| First map overhead | Medium | Medium-High | None |
| Subsequent maps | Fast | Fast | Fastest |
| Memory footprint | Low-Medium | Medium | Lowest |
| Startup time impact | Low | Medium | None |
| AOT compatible | Partial | No | Yes |
| Scenario | Recommendation |
|---|---|
| Maximum performance, AOT required | Mapperly |
| Compile-time safety is critical | Mapperly |
| Quick prototyping, zero setup | Mapsicle (static API) |
| Need integrated validation | Mapsicle |
| Existing AutoMapper codebase | AutoMapper (if licensed) or migrate |
| Budget-conscious / OSS project | Mapsicle or Mapperly |
| Complex mapping configurations | AutoMapper or Mapsicle (fluent) |
| ASP.NET Core Minimal APIs | Mapsicle (AspNetCore package) |
| Need audit trail of changes | Mapsicle (Audit package) |
Mapsicle (Static - Zero Config)
var dto = user.MapTo<UserDto>();
Mapsicle (Fluent)
var config = new MapperConfiguration(cfg => cfg.CreateMap<User, UserDto>());
var mapper = config.CreateMapper();
var dto = mapper.Map<UserDto>(user);
AutoMapper
var config = new MapperConfiguration(cfg => cfg.CreateMap<User, UserDto>());
var mapper = config.CreateMapper();
var dto = mapper.Map<UserDto>(user);
Mapperly
[Mapper]
public partial class UserMapper
{
public partial UserDto ToDto(User user);
}
// Usage
var dto = new UserMapper().ToDto(user);
Features not found in AutoMapper or Mapperly:
user.MapTo<UserDto>() - no setup requiredMapWithAudit<T>()IMemoryCache/IDistributedCacheMapValidateAndReturn<T, TValidator>()MapToJson<T>(), MapFromJson<T>()using Mapsicle;
// 1. Define your types
public class User
{
public int Id { get; set; }
public string FirstName { get; set; }
public string LastName { get; set; }
public string Email { get; set; }
}
public class UserDto
{
public int Id { get; set; }
public string FirstName { get; set; }
public string LastName { get; set; }
}
// 2. Map - that's it! No configuration needed
var user = new User { Id = 1, FirstName = "John", LastName = "Doe", Email = "john@example.com" };
var dto = user.MapTo<UserDto>(); // FirstName and LastName copied automatically
// 3. Map collections
List<User> users = GetUsers();
List<UserDto> dtos = users.MapTo<UserDto>(); // Entire list mapped
Requirements: .NET Standard 2.0+ or .NET 6.0+
Installation: dotnet add package Mapsicle
Do you need EF Core query translation (ProjectTo)?
├─ YES → Install: Mapsicle + Mapsicle.Fluent + Mapsicle.EntityFramework
└─ NO
├─ Do you need post-mapping validation?
│ └─ YES → Install: Mapsicle + Mapsicle.Fluent + Mapsicle.Validation
├─ Do you need naming convention support (snake_case ↔ PascalCase)?
│ └─ YES → Install: Mapsicle + Mapsicle.Fluent + Mapsicle.NamingConventions
├─ Do you need custom mapping logic (ForMember, hooks)?
│ └─ YES → Install: Mapsicle + Mapsicle.Fluent
└─ NO → Install: Mapsicle (core only - zero config)
| Scenario | Packages Needed |
|---|---|
| Simple POCO mapping | Mapsicle |
| API DTOs with transformations | Mapsicle.Fluent |
| EF Core with SQL projection | Mapsicle.EntityFramework |
| Map + validate DTOs | Mapsicle.Validation |
| snake_case ↔ PascalCase mapping | Mapsicle.NamingConventions |
Real benchmarks on Apple M1, .NET 8.0, BenchmarkDotNet v0.13.12:
| Scenario | Manual | Mapsicle | AutoMapper | Mapperly | Winner |
|---|---|---|---|---|---|
| Single Object | 13 ns | 26 ns | 54 ns | 13 ns | ⭐ Mapsicle (2.1x faster than AutoMapper) |
| Flattening | 13 ns | 29 ns | 56 ns | 15 ns | ⭐ Mapsicle (1.9x faster than AutoMapper) |
| Collection (100) | 1.5 μs | 2.0 μs | 1.9 μs | 1.5 μs | ⭐ Mapperly (Mapsicle uses 18% less memory) |
Note: Mapperly generates code at compile-time, resulting in near-manual performance. Mapsicle is now the fastest runtime-based mapper, outperforming AutoMapper by 2.1x for single objects and 1.9x for flattening, while using less memory for collections.
| Scenario | Mapsicle | AutoMapper | Mapperly | Notes |
|---|---|---|---|---|
| Deep Nesting (15 levels) | ✅ Safe | ✅ Safe | ✅ Safe | All handle with limits |
| Circular References | ✅ Handled | ❌ Crashes | ❌ Compile error | Mapsicle wins |
| Large Collection (10K) | 4 ms | 4 ms | ~3.5 ms | Mapperly fastest |
| Parallel (1000 threads) | ✅ Thread-safe | ✅ Thread-safe | ✅ Thread-safe | All thread-safe |
| Cold Start | Medium | Slow | None | Mapperly pre-compiled |
| Optimization | Improvement | Status |
|---|---|---|
| TypedMapperCache<T,D> | Zero-allocation generic cache | ✅ NEW |
| MapTo<TSource,TDest>() | Strongly-typed mapping, no boxing | ✅ NEW |
| Skip depth tracking for simple | No overhead for flat types | ✅ NEW |
| Lock-free cache reads | Eliminates contention | ✅ |
| Collection mapper caching | +20% for collections (v1.1) | ✅ |
| PropertyInfo caching | +15% faster cold starts | ✅ |
| Primitive fast path | Skips depth tracking | ✅ |
| Cached compiled actions | No runtime reflection | ✅ |
| LRU cache option | Memory-bounded in long-run apps | ✅ |
| Collection pre-allocation | Capacity hints for known sizes | ✅ |
// Enable memory-bounded caching
Mapper.UseLruCache = true;
Mapper.MaxCacheSize = 1000; // Default
// Monitor cache performance
var stats = Mapper.CacheInfo();
Console.WriteLine($"Cache entries: {stats.Total}");
Console.WriteLine($"Hit ratio: {stats.HitRatio:P1}"); // Only when LRU enabled
Console.WriteLine($"Hits: {stats.Hits}, Misses: {stats.Misses}");
| Feature | Mapsicle (Unbounded) | Mapsicle (LRU) | AutoMapper |
|---|---|---|---|
| Memory Bounded | ❌ | ✅ | ❌ |
| Cache Statistics | Entry count only | Full stats | ❌ |
| Configurable Limit | ❌ | ✅ | ❌ |
| Lock-Free Reads | ✅ | ✅ | Partial |
✓ Core: 10,000 mappings in 19ms
✓ Fluent: 10,000 mappings in 10ms
✓ Deep nesting (10 levels): 1,000 mappings in 3ms
✓ Large collection (10,000 items): 4ms
💡 Key Insight: Mapsicle is now 2.1x faster than AutoMapper for single object mapping while maintaining zero-configuration simplicity. Both vastly outperform reflection-based approaches.
cd tests/Mapsicle.Benchmarks
dotnet run -c Release # Full suite
dotnet run -c Release -- --quick # Smoke test
dotnet run -c Release -- --edge # Edge cases only
# Core package - zero config
dotnet add package Mapsicle
# Fluent configuration + Profiles (optional)
dotnet add package Mapsicle.Fluent
# EF Core ProjectTo (optional)
dotnet add package Mapsicle.EntityFramework
# FluentValidation integration (optional)
dotnet add package Mapsicle.Validation
# Naming conventions support (optional)
dotnet add package Mapsicle.NamingConventions
# Serilog structured logging (optional)
dotnet add package Mapsicle.Serilog
# Dapper integration (optional)
dotnet add package Mapsicle.Dapper
# JSON serialization (optional)
dotnet add package Mapsicle.Json
# ASP.NET Core Minimal API helpers (optional)
dotnet add package Mapsicle.AspNetCore
# Memory/Distributed caching (optional)
dotnet add package Mapsicle.Caching
# Change tracking/audit (optional)
dotnet add package Mapsicle.Audit
# DataAnnotations validation (optional)
dotnet add package Mapsicle.DataAnnotations
using Mapsicle;
var dto = user.MapTo<UserDto>(); // Single object
List<UserDto> dtos = users.MapTo<UserDto>(); // Collection
var flat = order.MapTo<OrderFlatDto>(); // Auto-flattening
public class UserDto
{
[MapFrom("UserName")] // Map from different property
public string Name { get; set; }
[IgnoreMap] // Never mapped
public string Secret { get; set; }
}
// Cycle Detection - no more StackOverflow
Mapper.MaxDepth = 32; // Default, configurable
// Validation at startup
Mapper.AssertMappingValid<User, UserDto>();
// Logging
Mapper.Logger = Console.WriteLine;
// Memory-bounded caching (prevents memory leaks in long-running apps)
Mapper.UseLruCache = true; // Enable LRU cache
Mapper.MaxCacheSize = 1000; // Limit cache entries
// Cache statistics
var stats = Mapper.CacheInfo();
Console.WriteLine($"Hit ratio: {stats.HitRatio:P1}");
// Scoped instances with isolated caches
using var mapper = MapperFactory.Create();
var dto = mapper.MapTo<UserDto>(user); // Uses isolated cache
using Mapsicle.Fluent;
var config = new MapperConfiguration(cfg =>
{
cfg.CreateMap<User, UserDto>()
.ForMember(d => d.FullName, opt => opt.MapFrom(s => $"{s.First} {s.Last}"))
.ForMember(d => d.Password, opt => opt.Ignore())
.ForMember(d => d.Status, opt => opt.Condition(s => s.IsActive));
});
config.AssertConfigurationIsValid();
var mapper = config.CreateMapper();
// In Program.cs
services.AddMapsicle(cfg =>
{
cfg.CreateMap<User, UserDto>();
}, validateConfiguration: true);
// In your service
public class UserService(IMapper mapper)
{
public UserDto GetUser(User user) => mapper.Map<UserDto>(user);
}
cfg.CreateMap<Order, OrderDto>()
.BeforeMap((src, dest) => dest.CreatedAt = DateTime.UtcNow)
.AfterMap((src, dest) => dest.WasProcessed = true);
cfg.CreateMap<Vehicle, VehicleDto>()
.Include<Car, CarDto>()
.Include<Truck, TruckDto>();
cfg.CreateMap<Order, OrderDto>()
.ConstructUsing(src => OrderFactory.Create(src.Type));
cfg.CreateConverter<Money, decimal>(m => m.Amount);
cfg.CreateConverter<Money, string>(m => $"{m.Currency} {m.Amount}");
ProjectTo<T>() that translates to SQL—no in-memory loading!
using Mapsicle.EntityFramework;
var dtos = await _context.Users
.Where(u => u.IsActive)
.ProjectTo<UserEntity, UserDto>()
.ToListAsync();
// Flattening in SQL: Customer.Name → CustomerName
var orders = _context.Orders
.ProjectTo<OrderEntity, OrderFlatDto>()
.ToList();
// ForMember expressions are translated to SQL!
var config = new MapperConfiguration(cfg =>
{
cfg.CreateMap<Order, OrderDto>()
.ForMember(d => d.CustomerName, opt => opt.MapFrom(s => s.Customer.FirstName + " " + s.Customer.LastName))
.ForMember(d => d.Total, opt => opt.MapFrom(s => s.Lines.Sum(l => l.Quantity * l.UnitPrice)));
});
// These expressions translate to SQL queries
var orders = _context.Orders.ProjectTo<Order, OrderDto>(config).ToList();
Post-mapping validation using FluentValidation—validate DTOs immediately after mapping!
using FluentValidation;
using Mapsicle.Fluent;
using Mapsicle.Validation;
// 1. Define your validator
public class UserDtoValidator : AbstractValidator<UserDto>
{
public UserDtoValidator()
{
RuleFor(x => x.Name).NotEmpty().WithMessage("Name is required");
RuleFor(x => x.Email).NotEmpty().EmailAddress();
RuleFor(x => x.Age).GreaterThan(0).WithMessage("Age must be positive");
}
}
// 2. Map and validate in one call
var result = mapper.MapAndValidate<User, UserDto, UserDtoValidator>(user);
if (result.IsValid)
{
return Ok(result.Value); // The mapped DTO
}
else
{
return BadRequest(result.ErrorsByProperty); // { "Email": ["Valid email is required"] }
}
// Map and validate with validator type
var result = mapper.MapAndValidate<TSource, TDest, TValidator>(source);
// Map and validate with validator instance
var validator = new UserDtoValidator();
var result = mapper.MapAndValidate<UserDto>(source, validator);
// Validate an existing object
var result = dto.Validate<UserDto, UserDtoValidator>();
// Get value or throw exception
var dto = result.GetValueOrThrow(); // Throws ValidationException if invalid
result.IsValid // bool - true if validation passed
result.Value // TDest - the mapped object
result.Errors // IList<ValidationFailure> - all validation errors
result.ErrorsByProperty // IDictionary<string, string[]> - errors grouped by property
result.ValidationResult // FluentValidation.Results.ValidationResult - full result
[ApiController]
[Route("api/users")]
public class UsersController : ControllerBase
{
private readonly IMapper _mapper;
private readonly IUserRepository _repo;
public UsersController(IMapper mapper, IUserRepository repo)
{
_mapper = mapper;
_repo = repo;
}
[HttpPost]
public async Task<IActionResult> Create([FromBody] CreateUserRequest request)
{
var result = _mapper.MapAndValidate<CreateUserRequest, UserDto, UserDtoValidator>(request);
if (!result.IsValid)
{
return BadRequest(new { errors = result.ErrorsByProperty });
}
var user = await _repo.CreateAsync(result.Value);
return CreatedAtAction(nameof(GetById), new { id = user.Id }, user);
}
}
Automatic naming convention conversion—map between snake_case, PascalCase, camelCase, and kebab-case!
using Mapsicle.NamingConventions;
// Source uses snake_case (e.g., from Python API or database)
public class ApiResponse
{
public int user_id { get; set; }
public string first_name { get; set; }
public string email_address { get; set; }
}
// Destination uses PascalCase (C# convention)
public class UserDto
{
public int UserId { get; set; }
public string FirstName { get; set; }
public string EmailAddress { get; set; }
}
// Map with naming convention conversion
var dto = apiResponse.MapWithConvention<ApiResponse, UserDto>(
NamingConvention.SnakeCase,
NamingConvention.PascalCase);
// dto.UserId == apiResponse.user_id
// dto.FirstName == apiResponse.first_name
| Convention | Example | C# Property |
|---|---|---|
NamingConvention.PascalCase | UserName | Standard C# |
NamingConvention.CamelCase | userName | JavaScript/JSON |
NamingConvention.SnakeCase | user_name | Python/Ruby/SQL |
NamingConvention.KebabCase | user-name | URLs/CSS |
// Convert a single name
var snake = "UserName".ConvertName(NamingConvention.PascalCase, NamingConvention.SnakeCase);
// Result: "user_name"
var pascal = "first_name".ConvertName(NamingConvention.SnakeCase, NamingConvention.PascalCase);
// Result: "FirstName"
var camel = "OrderCount".ConvertName(NamingConvention.PascalCase, NamingConvention.CamelCase);
// Result: "orderCount"
// Combine with IMapper for convention-based mapping
var dto = mapper.MapWithConvention<ApiResponse, UserDto>(
apiResponse,
NamingConvention.SnakeCase,
NamingConvention.PascalCase);
// Check if names match across conventions
bool match = NamingConvention.NamesMatch(
"user_name", NamingConvention.SnakeCase,
"UserName", NamingConvention.PascalCase);
// Result: true
public class ExternalApiClient
{
private readonly HttpClient _http;
public async Task<UserDto> GetUserAsync(int id)
{
// External API returns snake_case JSON
var response = await _http.GetFromJsonAsync<ExternalUserResponse>($"/users/{id}");
// Convert to C# conventions
return response.MapWithConvention<ExternalUserResponse, UserDto>(
NamingConvention.SnakeCase,
NamingConvention.PascalCase);
}
}
// External API response (snake_case)
public class ExternalUserResponse
{
public int user_id { get; set; }
public string first_name { get; set; }
public string last_name { get; set; }
public string email_address { get; set; }
public DateTime created_at { get; set; }
}
// Internal DTO (PascalCase)
public class UserDto
{
public int UserId { get; set; }
public string FirstName { get; set; }
public string LastName { get; set; }
public string EmailAddress { get; set; }
public DateTime CreatedAt { get; set; }
}
Structured logging integration for enterprise diagnostics and observability.
using Mapsicle.Serilog;
using Serilog;
// Configure Serilog logger
var logger = new LoggerConfiguration()
.WriteTo.Console()
.MinimumLevel.Debug()
.CreateLogger();
// Enable Mapsicle logging
MapsicleLogging.UseSerilog(logger);
// Log individual mappings
var dto = user.MapWithLogging<User, UserDto>(logger);
// Output: [INF] Mapsicle: Mapped User -> UserDto in 0.5ms
// Log collection mappings
var dtos = users.MapCollectionWithLogging<User, UserDto>(logger);
// Output: [INF] Mapsicle: Mapped 100 User -> UserDto items in 5.2ms
// Configure slow mapping threshold (default: 100ms)
MapsicleLogging.SlowMappingThreshold = TimeSpan.FromMilliseconds(50);
// Slow mappings automatically log warnings
var dto = largeObject.MapWithLogging<Large, LargeDto>(logger);
// Output: [WRN] Mapsicle: Slow mapping detected Large -> LargeDto took 75ms
using (var scope = new MappingLoggingScope(logger, "OrderProcessing"))
{
// All mappings in this scope are logged with the operation context
var orderDto = order.MapWithLogging<Order, OrderDto>(logger);
var itemDtos = items.MapCollectionWithLogging<Item, ItemDto>(logger);
}
// Output includes: OperationName = "OrderProcessing"
Seamless integration with Dapper for mapping database query results directly to DTOs.
using Mapsicle.Dapper;
using Dapper;
// Query and map in one call
var users = connection.QueryAndMap<User, UserDto>("SELECT * FROM Users").ToList();
// With parameters
var user = connection.QuerySingleAndMap<User, UserDto>(
"SELECT * FROM Users WHERE Id = @Id",
param: new { Id = 1 });
// Async query and map
var users = await connection.QueryAndMapAsync<User, UserDto>("SELECT * FROM Users");
// Async single result
var user = await connection.QuerySingleAndMapAsync<User, UserDto>(
"SELECT * FROM Users WHERE Id = @Id",
param: new { Id = 1 });
// Use a custom mapper configuration
var config = new MapperConfiguration(cfg =>
{
cfg.CreateMap<User, UserSummaryDto>()
.ForMember(d => d.FullName, opt => opt.MapFrom(s => $"{s.FirstName} {s.LastName}"));
});
var users = connection.QueryAndMap<User, UserSummaryDto>(
"SELECT * FROM Users", config).ToList();
// Or use IMapper instance
var mapper = config.CreateMapper();
var users = connection.QueryAndMap<User, UserSummaryDto>(
"SELECT * FROM Users", mapper).ToList();
using var transaction = connection.BeginTransaction();
// Mappings work within transactions
var users = connection.QueryAndMap<User, UserDto>(
"SELECT * FROM Users WHERE Active = 1",
transaction: transaction).ToList();
transaction.Commit();
// Map existing IEnumerable from Dapper
var users = connection.Query<User>("SELECT * FROM Users");
var dtos = users.MapTo<User, UserDto>(mapper);
| AutoMapper | Mapsicle |
|---|---|
CreateMap<S,D>() | Same! |
ForMember().MapFrom() | Same! |
.Ignore() | Same! |
BeforeMap/AfterMap | Same! |
Include<Derived>() | Same! |
ConstructUsing() | Same! |
services.AddAutoMapper() | services.AddMapsicle() |
_mapper.Map<T>() | mapper.Map<T>() or obj.MapTo<T>() |
Simple mappings (no profiles) → Use core Mapsicle package
Profiles with configuration → Use Mapsicle.Fluent
EF Core ProjectTo → Use Mapsicle.EntityFramework
dotnet remove package AutoMapper
dotnet remove package AutoMapper.Extensions.Microsoft.DependencyInjection
dotnet add package Mapsicle.Fluent # Includes core
Before (AutoMapper):
public class UserProfile : Profile
{
public UserProfile()
{
CreateMap<User, UserDto>()
.ForMember(d => d.FullName, opt => opt.MapFrom(s => s.FirstName + " " + s.LastName));
}
}
After (Mapsicle):
// In Program.cs/Startup.cs
services.AddMapsicle(cfg =>
{
cfg.CreateMap<User, UserDto>()
.ForMember(d => d.FullName, opt => opt.MapFrom(s => s.FirstName + " " + s.LastName));
}, validateConfiguration: true);
Before:
services.AddAutoMapper(typeof(UserProfile).Assembly);
After:
services.AddMapsicle(cfg =>
{
cfg.CreateMap<User, UserDto>();
cfg.CreateMap<Order, OrderDto>();
// ... all your mappings
}, validateConfiguration: true);
Before:
public class UserService
{
private readonly IMapper _mapper;
public UserService(IMapper mapper) => _mapper = mapper;
public UserDto GetUser(User user) => _mapper.Map<UserDto>(user);
}
After (same interface!):
public class UserService
{
private readonly IMapper _mapper;
public UserService(IMapper mapper) => _mapper = mapper;
// Option 1: Same as AutoMapper
public UserDto GetUser(User user) => _mapper.Map<UserDto>(user);
// Option 2: Extension method (no DI needed for simple cases)
public UserDto GetUser(User user) => user.MapTo<UserDto>();
}
❌ Not Supported:
IMemberValueResolver interface - use ResolveUsing(func) insteadITypeConverter interface - use CreateConverter<T, U>() insteadMapper.MaxDepth)✅ Now Supported (via extension packages):
Mapsicle.NamingConventionsMapsicle.Validation⚠️ Behavioral Differences:
GetUnmappedProperties<T, U>() for validationSymptom: Destination properties remain default/null after mapping
Causes & Solutions:
Property name mismatch
// Problem: Source has "UserName", destination has "Name"
// Solution 1: Use [MapFrom] attribute
public class UserDto
{
[MapFrom("UserName")]
public string Name { get; set; }
}
// Solution 2: Use Fluent configuration
cfg.CreateMap<User, UserDto>()
.ForMember(d => d.Name, opt => opt.MapFrom(s => s.UserName));
Property not readable/writable
// ❌ Won't map (no setter)
public string Name { get; }
// ✅ Will map
public string Name { get; set; }
// ✅ Also works (init setter)
public string Name { get; init; }
Type incompatibility
// Check which properties can't map
var unmapped = Mapper.GetUnmappedProperties<User, UserDto>();
Console.WriteLine($"Unmapped: {string.Join(", ", unmapped)}");
Cause: Circular references exceeding MaxDepth (default 32)
Solutions:
// Solution 1: Increase depth limit
Mapper.MaxDepth = 64;
// Solution 2: Enable logging to see depth warnings
Mapper.Logger = msg => Console.WriteLine($"[Mapsicle] {msg}");
// Solution 3: Use [IgnoreMap] to break cycle
public class User
{
public int Id { get; set; }
[IgnoreMap] // Don't map back to parent
public List<Order> Orders { get; set; }
}
Symptom: Mapping 10,000+ items is slow
Solutions:
// ❌ Don't: Map items individually
foreach (var user in users)
{
dtos.Add(user.MapTo<UserDto>());
}
// ✅ Do: Map entire collection
var dtos = users.MapTo<UserDto>(); // 20% faster with cached mapper
// ✅ Do: Pre-warm cache at startup for frequently used types
new User().MapTo<UserDto>();
new Order().MapTo<OrderDto>();
Symptom: Memory usage grows over time
Cause: Unbounded cache with many dynamic type combinations
Solution:
// Enable memory-bounded LRU cache
Mapper.UseLruCache = true;
Mapper.MaxCacheSize = 1000; // Adjust based on # of unique type pairs
// Monitor cache performance
var stats = Mapper.CacheInfo();
if (stats.HitRatio < 0.8)
{
// Consider increasing cache size
Mapper.MaxCacheSize = 2000;
}
Symptom: Exception thrown or results incorrect
Common Causes:
Missing configuration
// ❌ Don't use convention mapping with complex expressions
var dtos = context.Orders.ProjectTo<Order, OrderDto>().ToList();
// ✅ Pass configuration for ForMember expressions
var config = new MapperConfiguration(cfg =>
{
cfg.CreateMap<Order, OrderDto>()
.ForMember(d => d.CustomerName, opt => opt.MapFrom(s => s.Customer.Name));
});
var dtos = context.Orders.ProjectTo<Order, OrderDto>(config).ToList();
Non-translatable expressions
// ❌ Method calls that don't translate to SQL
cfg.CreateMap<User, UserDto>()
.ForMember(d => d.Name, opt => opt.ResolveUsing(u => FormatName(u)));
// ✅ Use expressions that translate to SQL
cfg.CreateMap<User, UserDto>()
.ForMember(d => d.Name, opt => opt.MapFrom(u => u.FirstName + " " + u.LastName));
// 1. Enable verbose logging
Mapper.Logger = msg => _logger.LogDebug($"[Mapsicle] {msg}");
// 2. Validate mapping at startup
#if DEBUG
Mapper.AssertMappingValid<User, UserDto>();
#endif
// 3. Check configuration in fluent mapper
config.AssertConfigurationIsValid();
// 4. Monitor cache statistics
var stats = Mapper.CacheInfo();
_logger.LogInformation($"Cache: {stats.Total} entries, Hit ratio: {stats.HitRatio:P1}");
// 5. Use MapperFactory for isolated testing
using var mapper = MapperFactory.Create(new MapperOptions
{
MaxDepth = 16,
Logger = Console.WriteLine
});
var dto = mapper.MapTo<UserDto>(user);
❌ Not Supported:
✅ Supported via Extension Packages:
Mapsicle.NamingConventionsMapsicle.Validation⚠️ Partial Support:
Address.City ✅, Address.Street.Line1 ❌)ForMember expressions, but not ResolveUsing delegatesGetUnmappedProperties for validation)| .NET Version | Mapsicle Support |
|---|---|
| .NET 8.0 | ✅ Fully supported |
| .NET 6.0-7.0 | ✅ Via .NET Standard 2.0 |
| .NET 5.0 | ✅ Via .NET Standard 2.0 |
| .NET Core 2.0+ | ✅ Via .NET Standard 2.0 |
| .NET Framework 4.6.1+ | ✅ Via .NET Standard 2.0 |
using Mapsicle)MapTo<T>(this object source)Maps a source object to a new instance of type T.
Parameters:
source - The source object to map fromReturns:
T? - New instance of T with mapped properties, or default(T) if source is null or max depth exceededExample:
var dto = user.MapTo<UserDto>();
MapTo<T>(this IEnumerable source)Maps a collection to a List.
Parameters:
source - The source collectionReturns:
List<T> - New list with mapped items (empty if source is null)Optimization: Pre-allocates capacity if source implements ICollection
Example:
List<UserDto> dtos = users.MapTo<UserDto>();
Map<TDest>(this object source, TDest destination)Updates an existing destination object from source.
Parameters:
source - The source objectdestination - The destination object to updateReturns:
TDest - The updated destination (same instance)Example:
source.Map(existingDto); // Updates existingDto in-place
ToDictionary(this object source)Converts an object to a dictionary of property name/value pairs.
Returns:
Dictionary<string, object?> - Case-insensitive dictionaryExample:
var dict = user.ToDictionary();
MapTo<T>(this IDictionary<string, object?> source) where T : new()Maps a dictionary to an object.
Constraints:
Example:
var user = dict.MapTo<User>();
Mapper.MaxDepthint32Mapper.MaxDepth = 64;
Mapper.UseLruCacheboolfalseMapper.UseLruCache = true;
Mapper.MaxCacheSizeint1000Mapper.MaxCacheSize = 2000;
Mapper.LoggerAction<string>?nullMapper.Logger = msg => _logger.LogDebug(msg);
Mapper.ClearCache()Clears all cached mapping delegates.
Mapper.ClearCache();
Mapper.CacheInfo()MapperCacheInfo - Current cache statisticsvar stats = Mapper.CacheInfo();
Console.WriteLine($"Total: {stats.Total}, Hit Ratio: {stats.HitRatio:P1}");
Mapper.AssertMappingValid<TSource, TDest>()Validates mapping configuration. Throws InvalidOperationException if unmapped properties exist.
Mapper.AssertMappingValid<User, UserDto>();
Mapper.GetUnmappedProperties<TSource, TDest>()List<string> - Names of destination properties that cannot be mappedvar unmapped = Mapper.GetUnmappedProperties<User, UserDto>();
MapperFactory.Create(MapperOptions? options = null)Creates an isolated mapper instance with independent cache and depth tracking.
Parameters:
options - Optional configuration (MaxDepth, Logger, UseLruCache, MaxCacheSize)Returns:
IDisposable mapper instanceExample:
using var mapper = MapperFactory.Create(new MapperOptions
{
MaxDepth = 16,
UseLruCache = true,
MaxCacheSize = 100,
Logger = Console.WriteLine
});
var dto = mapper.MapTo<UserDto>(user);
using Mapsicle.Fluent)MapperConfigurationvar config = new MapperConfiguration(cfg =>
{
cfg.CreateMap<User, UserDto>()
.ForMember(d => d.FullName, opt => opt.MapFrom(s => s.FirstName + " " + s.LastName))
.ForMember(d => d.Password, opt => opt.Ignore())
.ForMember(d => d.IsActive, opt => opt.Condition(s => s.Status == "Active"))
.BeforeMap((src, dest) => Console.WriteLine("Mapping started"))
.AfterMap((src, dest) => dest.MappedAt = DateTime.UtcNow)
.Include<PowerUser, PowerUserDto>()
.ConstructUsing(src => new UserDto(src.Id))
.ReverseMap();
cfg.CreateConverter<Money, decimal>(m => m.Amount);
});
config.AssertConfigurationIsValid();
var mapper = config.CreateMapper();
ForMember<TMember>() - Configure individual member mapping
opt.MapFrom(expr) - Map from custom expressionopt.Ignore() - Don't map this memberopt.Condition(pred) - Conditional mappingopt.ResolveUsing(func) - Custom resolver functionBeforeMap(action) - Execute action before mapping
AfterMap(action) - Execute action after mapping
Include<TDerived, TDest>() - Polymorphic mapping support
ConstructUsing(factory) - Custom object construction
ReverseMap() - Create reverse mapping
CreateConverter<TSource, TDest>(converter) - Global type converter
using Mapsicle.EntityFramework)ProjectTo<TSource, TDest>(this IQueryable<TSource> query, MapperConfiguration? config = null)Translates mapping to SQL expression (executed in database).
Parameters:
query - Source EF Core queryableconfig - Optional mapper configuration for custom mappingsReturns:
IQueryable<TDest> - Queryable projectionExample:
var dtos = await context.Users
.Where(u => u.IsActive)
.ProjectTo<User, UserDto>(config)
.ToListAsync();
using Mapsicle.Validation)MapAndValidate<TDest, TValidator>(this IMapper mapper, object? source)Maps source to destination and validates using the specified validator type.
Type Parameters:
TDest - Destination typeTValidator - FluentValidation validator type (must have parameterless constructor)Returns:
MapperValidationResult<TDest> - Contains IsValid, Value, Errors, ErrorsByPropertyExample:
var result = mapper.MapAndValidate<User, UserDto, UserDtoValidator>(user);
if (result.IsValid) return result.Value;
MapAndValidate<TDest>(this IMapper mapper, object? source, IValidator<TDest> validator)Maps source to destination and validates using a provided validator instance.
Example:
var validator = new UserDtoValidator();
var result = mapper.MapAndValidate<UserDto>(user, validator);
Validate<T, TValidator>(this T value)Validates an existing object using the specified validator type.
Example:
var result = dto.Validate<UserDto, UserDtoValidator>();
using Mapsicle.NamingConventions)MapWithConvention<TSource, TDest>(this TSource source, NamingConvention sourceConvention, NamingConvention destConvention)Maps source to destination with naming convention transformation.
Parameters:
sourceConvention - The naming convention of source propertiesdestConvention - The naming convention of destination propertiesReturns:
TDest? - New instance with convention-matched propertiesExample:
var dto = apiResponse.MapWithConvention<ApiResponse, UserDto>(
NamingConvention.SnakeCase,
NamingConvention.PascalCase);
ConvertName(this string name, NamingConvention from, NamingConvention to)Converts a property name from one convention to another.
Example:
var snakeName = "UserName".ConvertName(NamingConvention.PascalCase, NamingConvention.SnakeCase);
// Result: "user_name"
NamingConvention.NamesMatch(string sourceName, NamingConvention sourceConvention, string destName, NamingConvention destConvention)Checks if two names match when their conventions are applied.
Example:
bool match = NamingConvention.NamesMatch("user_id", NamingConvention.SnakeCase, "UserId", NamingConvention.PascalCase);
// Result: true
using Mapsicle.Serilog)MapsicleLogging.UseSerilog(ILogger logger)Enables global Serilog integration for Mapsicle mapping operations.
Parameters:
logger - Serilog ILogger instanceExample:
MapsicleLogging.UseSerilog(Log.Logger);
MapWithLogging<TSource, TDest>(this TSource source, ILogger logger)Maps source to destination with timing and structured logging.
Returns:
TDest? - Mapped destination objectExample:
var dto = user.MapWithLogging<User, UserDto>(logger);
// Logs: Mapsicle: Mapped User -> UserDto in 0.5ms
MapCollectionWithLogging<TSource, TDest>(this IEnumerable<TSource> source, ILogger logger)Maps a collection with aggregated timing and logging.
Returns:
List<TDest> - List of mapped destination objectsExample:
var dtos = users.MapCollectionWithLogging<User, UserDto>(logger);
// Logs: Mapsicle: Mapped 100 User -> UserDto items in 5.2ms
MapsicleLogging.SlowMappingThresholdConfigures the threshold for slow mapping warnings.
Default: 100ms
Example:
MapsicleLogging.SlowMappingThreshold = TimeSpan.FromMilliseconds(50);
using Mapsicle.Dapper)QueryAndMap<TSource, TDest>(this IDbConnection connection, string sql, ...)Executes a SQL query and maps results to destination type.
Overloads:
QueryAndMap<TSource, TDest>(sql, param?, transaction?, commandTimeout?) - Auto-mappingQueryAndMap<TSource, TDest>(sql, MapperConfiguration, param?, ...) - With configurationQueryAndMap<TSource, TDest>(sql, IMapper, param?, ...) - With mapper instanceReturns:
IEnumerable<TDest> - Mapped resultsExample:
var users = connection.QueryAndMap<User, UserDto>("SELECT * FROM Users").ToList();
QueryAndMapAsync<TSource, TDest>(this IDbConnection connection, string sql, ...)Async version of QueryAndMap.
Example:
var users = await connection.QueryAndMapAsync<User, UserDto>("SELECT * FROM Users");
QuerySingleAndMap<TSource, TDest>(this IDbConnection connection, string sql, ...)Executes a query expecting a single result and maps it.
Returns:
TDest? - Mapped result or nullExample:
var user = connection.QuerySingleAndMap<User, UserDto>(
"SELECT * FROM Users WHERE Id = @Id", param: new { Id = 1 });
QueryFirstAndMap<TSource, TDest>(this IDbConnection connection, string sql, ...)Executes a query and maps the first result.
Returns:
TDest? - First mapped result or nullExample:
var user = connection.QueryFirstAndMap<User, UserDto>("SELECT * FROM Users ORDER BY CreatedAt DESC");
MapTo<TSource, TDest>(this IEnumerable<TSource>? source, IMapper mapper)Maps an existing collection using a provided mapper.
Returns:
List<TDest> - Mapped resultsExample:
var users = connection.Query<User>("SELECT * FROM Users");
var dtos = users.MapTo<User, UserDto>(mapper);
AddressCity → Address.City)T ↔ T?)[MapFrom] attribute[IgnoreMap] attribute.Include<>).ConstructUsing)| Package | Tests | Coverage |
|---|---|---|
| Mapsicle | 210 | Core + Stability |
| Mapsicle.Fluent | 35 | Fluent + Enterprise |
| Mapsicle.EntityFramework | 19 | EF Core |
| Mapsicle.Validation | 13 | FluentValidation |
| Mapsicle.NamingConventions | 55 | Naming Conventions |
| Mapsicle.Serilog | 22 | Serilog Logging |
| Mapsicle.Dapper | 25 | Dapper Integration |
| Mapsicle.Json | 26 | JSON Serialization |
| Mapsicle.AspNetCore | 23 | ASP.NET Core |
| Mapsicle.Caching | 21 | Caching Integration |
| Mapsicle.Audit | 26 | Change Tracking |
| Mapsicle.DataAnnotations | 24 | DataAnnotations |
| Total | 499 |
Mapsicle/
├── src/
│ ├── Mapsicle/ # Core - zero config
│ ├── Mapsicle.Fluent/ # Fluent + DI + Profiles
│ ├── Mapsicle.EntityFramework/ # EF Core ProjectTo
│ ├── Mapsicle.Validation/ # FluentValidation integration
│ ├── Mapsicle.NamingConventions/ # Naming convention support
│ ├── Mapsicle.Serilog/ # Serilog structured logging
│ ├── Mapsicle.Dapper/ # Dapper integration
│ ├── Mapsicle.Json/ # JSON serialization
│ ├── Mapsicle.AspNetCore/ # ASP.NET Core Minimal API
│ ├── Mapsicle.Caching/ # Memory/Distributed caching
│ ├── Mapsicle.Audit/ # Change tracking/diff
│ └── Mapsicle.DataAnnotations/ # DataAnnotations validation
├── tests/
│ ├── Mapsicle.Tests/
│ ├── Mapsicle.Fluent.Tests/
│ ├── Mapsicle.EntityFramework.Tests/
│ ├── Mapsicle.Validation.Tests/
│ ├── Mapsicle.NamingConventions.Tests/
│ ├── Mapsicle.Serilog.Tests/
│ ├── Mapsicle.Dapper.Tests/
│ ├── Mapsicle.Json.Tests/
│ ├── Mapsicle.AspNetCore.Tests/
│ ├── Mapsicle.Caching.Tests/
│ ├── Mapsicle.Audit.Tests/
│ ├── Mapsicle.DataAnnotations.Tests/
│ └── Mapsicle.Benchmarks/
└── examples/
└── Mapsicle.Examples/ # Working examples for all packages
dotnet run --project examples/Mapsicle.Examples
PRs welcome! Areas for contribution:
MPL 2.0 License © Arnel Isiderio Robles
Stop configuring. Start mapping.
Free forever. Zero dependencies. Pure performance.