The fastest object mapper for .NET — with safety features no other mapper has.
Disclaimer: This project is an independent implementation and is not affiliated with AutoMapper, Mapster, PanoramicData.Mapper, or any other mapping library.
You've been here before. Your API maps User to UserDto ten thousand times per second, and one day someone adds a Parent property that points back to itself. The stack overflows. Production goes down. Nobody knows why.
Or maybe it's subtler — a mapping library that phones home with telemetry you never asked for. Or one that only supports .NET 8+, leaving your legacy services stranded.
Mapture was built for teams who are tired of compromise. It's the only .NET mapper that is simultaneously:
- Fastest — benchmarked faster than Mapster, AutoMapper, and PanoramicData.Mapper
- Safest — cycle detection and max-depth enforcement catch infinite recursion before it happens
- Broadest — runs on .NET Framework 4.8, .NET Standard 2.0, .NET 8, and .NET 10
- Cleanest — zero telemetry, zero surprises, familiar API, 30-minute migration from AutoMapper
Measured with BenchmarkDotNet on .NET 10.0, mapping a simple object with one nested child. All libraries configured with equivalent mappings, run on the same hardware:
| Rank | Method | Mean | vs Manual | Allocated |
|---|---|---|---|---|
| 🥇 | Manual Mapping | ~17 ns | baseline | 96 B |
| 🥈 | Mapture | ~25 ns | 1.5x | 96 B |
| 🥉 | Mapster | ~27 ns | 1.6x | 96 B |
| 4 | AutoMapper | ~68 ns | 4.0x | 96 B |
| 5 | PanoramicData.Mapper | ~283 ns | 16.9x | 272 B |
How? Mapture compiles expression trees at configuration time and caches the compiled delegates per type pair. Acyclic type graphs get a zero-overhead fast path that skips all cycle/depth tracking. The result: your mapping function runs almost as fast as code you'd write by hand.
dotnet add package Mapture
dotnet add package Mapture.Extensions.DependencyInjectionusing Mapture;
public class UserProfile : Profile
{
public UserProfile()
{
CreateMap<User, UserDto>()
.ForMember(d => d.FullName, opt => opt.MapFrom(s => s.FirstName + " " + s.LastName))
.Ignore(d => d.InternalId);
}
}using Mapture.Extensions.DependencyInjection;
builder.Services.AddMapture(typeof(Program).Assembly);public class UsersController : ControllerBase
{
private readonly IMapper _mapper;
public UsersController(IMapper mapper) => _mapper = mapper;
[HttpGet]
public IActionResult Get()
{
var users = _userService.GetAll();
return Ok(users.Select(u => _mapper.Map<User, UserDto>(u)));
}
}That's it. Convention-based matching handles properties with the same name. Custom mappings, ignores, and reverse maps are all one fluent call away.
| Feature | Mapture | AutoMapper | Mapster | PanoramicData.Mapper |
|---|---|---|---|---|
| Performance rank | 🥈 | 4th | 🥉 | 5th |
| Convention-based mapping | ✅ | ✅ | ✅ | ✅ |
| Profile system | ✅ | ✅ | ❌ | ✅ |
| ForMember / Ignore | ✅ | ✅ | ✅ | ✅ |
| ReverseMap | ✅ | ✅ | ✅ | ✅ |
| Nested object mapping | ✅ | ✅ | ✅ | ✅ |
| Collection mapping | ✅ | ✅ | ✅ | ✅ |
IEnumerable<TSrc> → List<TDest> |
✅ | ✅ | ✅ | |
Non-nullable → nullable coercion (int→int?, bool→bool?, etc.) |
✅ | ✅ | ✅ | |
Numeric widening / narrowing (int→long, long→int, etc.) |
✅ | ✅ | ✅ | |
In-place update (Map(src, dest) with mismatched nullability) |
✅ | ✅ | ✅ | |
| Enum ↔ string / numeric coercion | ✅ | ✅ | ✅ | |
Cycle detection |
✅ | ❌ | ❌ | ❌ |
| Max depth enforcement | ✅ | ❌ | ❌ | ✅ |
| ConvertUsing | ✅ | ✅ | ✅ | ✅ |
| BeforeMap / AfterMap | ✅ | ✅ | ✅ | ✅ |
| ConstructUsing | ✅ | ✅ | ✅ | ✅ |
| Condition | ✅ | ✅ | ✅ | ✅ |
| MapFrom (Expression) | ✅ | ✅ | ✅ | ✅ |
| MapFrom (string source-member name) | ✅ | ✅ | ❌ | ✅ |
ResolveUsing (Func<TSource, TMember>) |
✅ | ✅ | ✅ | |
| ResolveUsing with destination access | ✅ | ✅ | ❌ | |
| NullSubstitute | ✅ | ✅ | ✅ | ❌ |
| UseValue (constant) | ✅ | ✅ | ✅ | ✅ |
| Configuration validation | ✅ | ✅ | ❌ | ✅ |
| DI integration | ✅ | ✅ | ✅ | ✅ |
| No forced telemetry | ✅ | ❌ | ✅ | ✅ |
| .NET Framework 4.8 | ✅ | ✅ | ✅ | ❌ |
| .NET Standard 2.0 | ✅ | ❌ | ✅ | ❌ |
| .NET 8 / .NET 10 | ✅ | ✅ | ✅ |
Objects that reference themselves (directly or indirectly) are the #1 cause of
StackOverflowException in mapping libraries. Mapture detects cycles at runtime
and breaks them safely:
var node = new Node { Id = 1 };
node.Parent = node; // Circular reference!
var dto = mapper.Map<Node, NodeDto>(node);
// dto.Parent is null — cycle safely broken, no stack overflowEven without cycles, deeply nested object graphs can exhaust the stack. Mapture caps recursion at a configurable depth:
services.AddMapture(typeof(Program).Assembly, options =>
{
options.MaxDepth = 5; // Stop mapping beyond 5 levels deep
});Mapture analyzes your type graph at configuration time. Types that cannot have
cycles (no self-referencing paths) get a zero-overhead fast path — no HashSet, no
depth counter, no per-call allocation. You get safety where you need it and raw speed
everywhere else.
Catch unmapped properties at startup — not at 3am in production:
var config = new MapperConfiguration(cfg =>
{
cfg.CreateMap<User, UserDto>();
});
config.AssertConfigurationIsValid();
// Throws MaptureException if any destination property has no source and isn't ignored// Strongly-typed expression (preferred, optimized into the compiled delegate)
CreateMap<User, UserDto>()
.ForMember(d => d.FullName,
opt => opt.MapFrom(s => s.First + " " + s.Last));
// By source-member name (matches AutoMapper's string overload)
CreateMap<User, UserDto>()
.ForMember(d => d.DisplayName, opt => opt.MapFrom("FirstName"));Use when a closure or runtime state prevents authoring an Expression:
CreateMap<User, UserDto>()
.ForMember(d => d.Token, opt => opt.ResolveUsing(s => _tokenService.For(s.Id)));
// Or with access to the current destination value
CreateMap<User, UserDto>()
.ForMember(d => d.AuditTrail, opt => opt.ResolveUsing((s, d) => d.AuditTrail + ";" + s.LastLogin));CreateMap<User, UserDto>()
.ForMember(d => d.NickName, opt =>
{
opt.MapFrom(s => s.NickName);
opt.NullSubstitute("(anonymous)");
});CreateMap<User, UserDto>()
.Ignore(d => d.InternalSecret);CreateMap<User, UserDto>().ReverseMap();
// Now both User→UserDto and UserDto→User workCreateMap<User, UserDto>()
.ConvertUsing(src => new UserDto
{
Id = src.Id,
Name = src.Name.ToUpperInvariant()
});CreateMap<User, UserDto>()
.AfterMap((src, dest) => dest.Name = dest.Name.Trim());CreateMap<User, UserDto>()
.ConstructUsing(src => new UserDto { Id = src.Id * 10 });CreateMap<User, UserDto>()
.ForMember(d => d.Age, opt =>
{
opt.MapFrom(s => s.Age);
opt.Condition(s => s.Age > 0);
});No more ForMember boilerplate just because the destination type is wider:
CreateMap<Order, OrderDto>(); // That's it.
// Source -> Dest property type coercion handled automatically:
// int -> int? / long / long? / decimal / decimal? / double / double?
// long -> int / int? (with overflow check)
// bool -> bool?
// DateTime -> DateTime? / DateTimeOffset?
// decimal -> decimal?
// string -> Enum (Enum.Parse)
// int -> Enum (cast)
// Enum -> string / intIf CreateMap<Src, Dest>() is registered, Mapture automatically projects any
IEnumerable<Src> into List<Dest>, Dest[], IList<Dest>, ICollection<Dest>,
or IEnumerable<Dest> — no extra .ToList() calls required:
IEnumerable<User> users = repo.QueryUsers();
List<UserDto> dtos = mapper.Map<IEnumerable<User>, List<UserDto>>(users);Use the two-parameter overload to update an existing destination instance — including when source/destination types differ in nullability:
var dto = new OrderDto();
mapper.Map(order, dto); // Coerces int->int?, bool->bool?, etc. on the flyCreateMap<User, UserDto>()
.ForMember(d => d.Source, opt => opt.UseValue("API"));Mapture uses the same API patterns. Most migrations take under 30 minutes:
dotnet remove package AutoMapper
dotnet remove package AutoMapper.Extensions.Microsoft.DependencyInjection
dotnet add package Mapture
dotnet add package Mapture.Extensions.DependencyInjection| Find | Replace |
|---|---|
using AutoMapper; |
using Mapture; |
using AutoMapper.Extensions.Microsoft.DependencyInjection; |
using Mapture.Extensions.DependencyInjection; |
// Before
services.AddAutoMapper(typeof(Startup));
// After
services.AddMapture(typeof(Startup));Most mappings work identically. The same Profile, CreateMap, ForMember, Ignore,
and ReverseMap patterns are supported.
services.AddMapture(typeof(Program).Assembly, options =>
{
options.CompatibilityMode = true; // Extra AutoMapper compat behaviors
options.MaxDepth = 10; // Prevent infinite recursion (default: 10)
options.EnableCycleDetection = true; // Detect circular references (default: true)
options.EnableDebugTracing = false; // Debug logging (default: false)
});Mapture.slnx
├── src/
│ ├── Mapture.Core # Core mapping engine
│ ├── Mapture.Compatibility # AutoMapper compatibility layer
│ ├── Mapture.Extensions.DependencyInjection # DI registration extensions
│ └── Mapture.Benchmarks # BenchmarkDotNet performance tests
├── tests/
│ ├── Mapture.Tests # 37 unit tests × 3 frameworks = 111
│ └── Mapture.CompatibilityTests # 3 compat tests × 3 frameworks = 9
├── enterprise/
│ └── Mapture.Enterprise # Metrics & audit (optional)
├── samples/
│ └── Mapture.Sample.Api # Sample ASP.NET Core Web API
└── docs/
└── index.html # Full documentation site
- Configuration time — You define mappings via Profiles. Mapture builds a type map dictionary.
- First map call — Mapture compiles an expression tree for the type pair into a native delegate. For acyclic types, this delegate is pure property assignment — no reflection, no dictionary lookups, no allocations beyond the destination object.
- Subsequent calls — The compiled delegate is cached in a thread-static slot. The hot path is: null check → read cached delegate → call. That's it.
For cyclic types (detected at configuration time), Mapture wraps the delegate with cycle detection and depth tracking. You never need to think about it — the engine picks the right path automatically.
Optional enterprise features available in Mapture.Enterprise:
- Mapping metrics — execution counts and latency tracking
- Audit trail — field-level lineage tracking
- No forced telemetry — opt-in only, you control what's collected
MIT License. See LICENSE for details.
Disclaimer: This project is an independent implementation and is not affiliated with AutoMapper, Mapster, or any other object mapping library. Common API patterns (IMapper, Profile, CreateMap) are standard industry conventions used for migration compatibility.