Skip to content

arijeetganguli/Mapture

Repository files navigation

Mapture

The fastest object mapper for .NET — with safety features no other mapper has.

Build NuGet License: MIT

Disclaimer: This project is an independent implementation and is not affiliated with AutoMapper, Mapster, PanoramicData.Mapper, or any other mapping library.


The Story

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

Benchmark Results

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.


Quick Start

1. Install

dotnet add package Mapture
dotnet add package Mapture.Extensions.DependencyInjection

2. Define a Profile

using 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);
    }
}

3. Register with DI

using Mapture.Extensions.DependencyInjection;

builder.Services.AddMapture(typeof(Program).Assembly);

4. Map Objects

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 Comparison

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 (intint?, boolbool?, etc.) ⚠️
Numeric widening / narrowing (intlong, longint, 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 ⚠️ .NET 10 only

Safety Features

Cycle Detection

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 overflow

Max Depth Enforcement

Even 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
});

Smart Cycle Analysis

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.

Configuration Validation

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

Fluent API

ForMember — Custom Property Mapping

// 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"));

ResolveUsing — Runtime Resolver Delegate

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));

NullSubstitute — Default for Null Source Members

CreateMap<User, UserDto>()
    .ForMember(d => d.NickName, opt =>
    {
        opt.MapFrom(s => s.NickName);
        opt.NullSubstitute("(anonymous)");
    });

Ignore — Skip a Property

CreateMap<User, UserDto>()
    .Ignore(d => d.InternalSecret);

ReverseMap — Bidirectional Mapping

CreateMap<User, UserDto>().ReverseMap();
// Now both User→UserDto and UserDto→User work

ConvertUsing — Full Custom Conversion

CreateMap<User, UserDto>()
    .ConvertUsing(src => new UserDto
    {
        Id = src.Id,
        Name = src.Name.ToUpperInvariant()
    });

BeforeMap / AfterMap — Pre/Post Processing

CreateMap<User, UserDto>()
    .AfterMap((src, dest) => dest.Name = dest.Name.Trim());

ConstructUsing — Custom Instantiation

CreateMap<User, UserDto>()
    .ConstructUsing(src => new UserDto { Id = src.Id * 10 });

Condition — Conditional Mapping

CreateMap<User, UserDto>()
    .ForMember(d => d.Age, opt =>
    {
        opt.MapFrom(s => s.Age);
        opt.Condition(s => s.Age > 0);
    });

Automatic Type Coercion

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 / int

Collection Projection from IEnumerable<T>

If 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);

In-place Update Mapping

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 fly

UseValue — Constant Value

CreateMap<User, UserDto>()
    .ForMember(d => d.Source, opt => opt.UseValue("API"));

Migration from AutoMapper

Mapture uses the same API patterns. Most migrations take under 30 minutes:

Step 1 — Replace NuGet Packages

dotnet remove package AutoMapper
dotnet remove package AutoMapper.Extensions.Microsoft.DependencyInjection
dotnet add package Mapture
dotnet add package Mapture.Extensions.DependencyInjection

Step 2 — Find & Replace Namespaces

Find Replace
using AutoMapper; using Mapture;
using AutoMapper.Extensions.Microsoft.DependencyInjection; using Mapture.Extensions.DependencyInjection;

Step 3 — Replace DI Registration

// Before
services.AddAutoMapper(typeof(Startup));

// After
services.AddMapture(typeof(Startup));

Step 4 — Run Tests

Most mappings work identically. The same Profile, CreateMap, ForMember, Ignore, and ReverseMap patterns are supported.


Configuration Options

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)
});

Architecture

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

How the Engine Works

  1. Configuration time — You define mappings via Profiles. Mapture builds a type map dictionary.
  2. 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.
  3. 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.


Enterprise Extension

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

License

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.

About

The fastest .NET object mapper — with cycle detection and safety no other mapper has.

Topics

Resources

License

Stars

Watchers

Forks

Packages

 
 
 

Contributors

Languages