Skip to content

armanbul/SwiftMapping

Repository files navigation

SwiftMapping

CI NuGet NuGet Downloads License: MIT

High-performance, source-generated, AOT-compatible object mapper for .NET.

🌐 Türkçe dokümantasyon için tıklayın

SwiftMapping is a modern alternative to AutoMapper that combines two mapping engines:

  • Source Generator (compile-time): Roslyn incremental generator produces zero-reflection mapping code at build time — fully AOT and trimming compatible.
  • Fluent API (runtime): Expression-tree compiled delegates with for-loop optimized collection mapping — no LINQ overhead, pre-allocated lists, near hand-written performance.

Free and open source (MIT). No license key required.


Table of Contents


Installation

dotnet add package SwiftMapping

For projects that only need marker interfaces and attributes (e.g., shared contract libraries):

dotnet add package SwiftMapping.Contracts

Quick Start

1. Define your models

public class User
{
    public int Id { get; set; }
    public string Name { get; set; }
    public string Email { get; set; }
}

public class UserDto
{
    public int Id { get; set; }
    public string Name { get; set; }
    public string Email { get; set; }
}

2. Create a mapping profile

Option A — Attribute-Based (AOT-compatible, zero reflection)

[MapperProfile]
[CreateMap(typeof(User), typeof(UserDto), ReverseMap = true)]
public class AppProfile { }

Option B — Fluent API (full feature set)

public class AppProfile : SwiftMapping.Core.Profile
{
    public AppProfile()
    {
        CreateMap<User, UserDto>()
            .ReverseMap();
    }
}

3. Register in DI

// Attribute-based profiles are auto-discovered
services.AddSwiftMapping();

// Or add fluent profiles explicitly
services.AddSwiftMapping(cfg =>
{
    cfg.AddProfile<AppProfile>();
});

4. Map objects

public class UserService
{
    private readonly IMapper _mapper;

    public UserService(IMapper mapper) => _mapper = mapper;

    public UserDto GetUser(User user)
    {
        return _mapper.Map<User, UserDto>(user);
    }
}

Performance Benchmarks

Benchmarked against AutoMapper 13.0.1 using BenchmarkDotNet v0.14.0.

Environment: .NET 10.0.5, X64 RyuJIT AVX2, Windows 11 Job: ShortRun (3 iterations, 1 launch, 3 warmup)

Scenario AutoMapper SwiftMapping Speedup AM Alloc SM Alloc
Simple (5 properties) 29.78 ns 12.65 ns 2.4x faster 48 B 88 B
Flattening (nested → flat) 27.42 ns 11.29 ns 2.4x faster 32 B 72 B
Large Object (20 properties) 37.53 ns 19.02 ns 2.0x faster 152 B 192 B
Complex Nested (Order + Customer + Address + 3 Items) 88.75 ns 63.43 ns 1.4x faster 568 B 600 B
Collection (100 items) 957.90 ns 624.92 ns 1.5x faster 6,992 B 5,696 B
Batch (10,000 items) 309,961 ns 123,127 ns 2.5x faster 480,000 B 880,000 B

Transparency notes:

  • Both mappers used their recommended typed API paths for fairest comparison.
  • AutoMapper used Map<TDestination>(source), SwiftMapping used Map<TSource, TDestination>(source).
  • SwiftMapping allocates slightly more for simple objects (88 B vs 48 B) due to destination object construction differences, but achieves significantly lower latency.
  • Collection and Complex scenarios show SwiftMapping uses less memory thanks to pre-allocated lists.
  • Batch scenario shows SwiftMapping uses more total memory but completes 2.5x faster — a throughput-latency tradeoff.
  • Results vary by hardware and runtime version. Run your own benchmarks with the included SwiftMapping.Benchmarks project.
Raw BenchmarkDotNet Output
BenchmarkDotNet v0.14.0, Windows 11 (10.0.26200.8117)
.NET SDK 10.0.201
  [Host]   : .NET 10.0.5 (10.0.526.15411), X64 RyuJIT AVX2
  ShortRun : .NET 10.0.5 (10.0.526.15411), X64 RyuJIT AVX2

Job=ShortRun  IterationCount=3  LaunchCount=1  WarmupCount=3

| Type                       | Method      | Mean          | Error         | Rank | Gen0    | Gen1   | Allocated |
|--------------------------- |------------ |--------------:|--------------:|-----:|--------:|-------:|----------:|
| BatchMappingBenchmark      | AutoMapper  | 309,960.90 ns | 46,839.088 ns |   10 | 25.3906 |      - |  480000 B |
| BatchMappingBenchmark      | SwiftMapping | 123,126.57 ns | 10,150.128 ns |    9 | 46.6309 |      - |  880000 B |
| CollectionMappingBenchmark | AutoMapper  |     957.90 ns |    681.049 ns |    8 |  0.3700 | 0.0076 |    6992 B |
| CollectionMappingBenchmark | SwiftMapping |     624.92 ns |    189.552 ns |    7 |  0.3023 | 0.0057 |    5696 B |
| ComplexMappingBenchmark    | AutoMapper  |      88.75 ns |      4.005 ns |    6 |  0.0302 |      - |     568 B |
| ComplexMappingBenchmark    | SwiftMapping |      63.43 ns |     59.496 ns |    5 |  0.0318 |      - |     600 B |
| FlatteningBenchmark        | AutoMapper  |      27.42 ns |      3.083 ns |    3 |  0.0017 |      - |      32 B |
| FlatteningBenchmark        | SwiftMapping |      11.29 ns |      4.648 ns |    1 |  0.0038 |      - |      72 B |
| LargeObjectBenchmark       | AutoMapper  |      37.53 ns |      5.847 ns |    4 |  0.0080 |      - |     152 B |
| LargeObjectBenchmark       | SwiftMapping |      19.02 ns |      5.116 ns |    2 |  0.0102 |      - |     192 B |
| SimpleMappingBenchmark     | AutoMapper  |      29.78 ns |     25.670 ns |    3 |  0.0025 |      - |      48 B |
| SimpleMappingBenchmark     | SwiftMapping |      12.65 ns |      5.801 ns |    1 |  0.0046 |      - |      88 B |

Mapping Approaches

1. Attribute-Based (Source Generated)

Best for: AOT deployments, trimming, minimal runtime overhead.

[MapperProfile]
[CreateMap(typeof(Order), typeof(OrderDto))]
[CreateMap(typeof(Customer), typeof(CustomerDto), ReverseMap = true)]
[IgnoreProperty(typeof(Order), typeof(OrderDto), nameof(OrderDto.InternalCode))]
[MapFrom(typeof(Order), typeof(OrderDto), nameof(OrderDto.FullName), "FirstName + \" \" + LastName")]
[UseConverter(typeof(Order), typeof(OrderDto), typeof(OrderConverter))]
[IncludeDerived(typeof(Order), typeof(OrderDto), typeof(PriorityOrder), typeof(PriorityOrderDto))]
[MapIgnoreNull(SourceType = typeof(Order), DestinationType = typeof(OrderDto))]
public class OrderProfile { }

The source generator produces GeneratedMapper.g.cs at compile time with direct property assignments — no reflection, no expression trees.

Compile-time diagnostics:

Code Description
SMAP001 Profile has no mappings
SMAP002 Source property not found
SMAP003 Destination property not writable
SMAP004 Unmapped destination property
SMAP005 Property type mismatch
SMAP006 Nullable to non-nullable mapping
SMAP007 Per-mapping generation timing
SMAP008 Total generation timing

2. Fluent API (Runtime Profiles)

Best for: complex mapping logic, dynamic configurations, full feature access.

public class OrderProfile : SwiftMapping.Core.Profile
{
    public OrderProfile()
    {
        CreateMap<Order, OrderDto>()
            .ForMember(d => d.FullName, opt => opt.MapFrom(s => $"{s.FirstName} {s.LastName}"))
            .ForMember(d => d.InternalCode, opt => opt.Ignore())
            .AfterMap((src, dst) => dst.MappedAt = DateTime.UtcNow)
            .ReverseMap();

        CreateMap<Customer, CustomerDto>();
        CreateMap<OrderItem, OrderItemDto>();
    }
}

Core Features

Property Mapping

CreateMap<Source, Dest>()
    // Map from a different property
    .ForMember(d => d.FullName, opt => opt.MapFrom(s => s.FirstName + " " + s.LastName))

    // Map from a nested property
    .ForMember(d => d.City, opt => opt.MapFrom(s => s.Address.City))

    // Use a constant value
    .ForMember(d => d.Source, opt => opt.UseValue("SystemA"))

    // Ignore a property
    .ForMember(d => d.Secret, opt => opt.Ignore())

    // Null substitution
    .ForMember(d => d.Name, opt => opt.NullSubstitute("Unknown"));

Nested Object Mapping

Nested objects are mapped automatically when their mapping is configured:

CreateMap<Order, OrderDto>();
CreateMap<Customer, CustomerDto>();
CreateMap<Address, AddressDto>();

var dto = mapper.Map<Order, OrderDto>(order);
// dto.Customer → mapped as CustomerDto
// dto.ShippingAddress → mapped as AddressDto

Collection Mapping

Collections are mapped automatically — List<T>, IEnumerable<T>, arrays, and ICollection<T>:

CreateMap<OrderItem, OrderItemDto>();

// Map a list directly
var dtos = mapper.Map<List<OrderItem>, List<OrderItemDto>>(items);

// Nested collections are mapped automatically
CreateMap<Order, OrderDto>();
// Order.Items (List<OrderItem>) → OrderDto.Items (List<OrderItemDto>)

Flattening & Unflattening

SwiftMapping automatically flattens nested properties by name convention:

public class Order { public Customer Customer { get; set; } }
public class Customer { public string Name { get; set; } }
public class OrderDto { public string CustomerName { get; set; } }  // Flattened!

CreateMap<Order, OrderDto>();  // CustomerName ← Customer.Name (automatic)

Unflattening works with ReverseMap():

CreateMap<Order, OrderDto>().ReverseMap();
// OrderDto.CustomerName → Order.Customer.Name

Reverse Mapping

CreateMap<User, UserDto>().ReverseMap();

var dto  = mapper.Map<User, UserDto>(user);
var back = mapper.Map<UserDto, User>(dto);

Custom Type Converters

Inline converter:

CreateMap<string, DateTime>()
    .ConvertUsing(s => DateTime.Parse(s));

DI-resolved converter:

public class MoneyConverter : ITypeConverter<decimal, Money>
{
    public Money Convert(decimal source) => new Money(source, "USD");
}

CreateMap<decimal, Money>()
    .ConvertUsing<MoneyConverter>();

Custom Value Resolvers

public class FullNameResolver : IValueResolver<User, UserDto, string>
{
    public string Resolve(User source, UserDto destination, ResolutionContext context)
    {
        return $"{source.FirstName} {source.LastName}";
    }
}

CreateMap<User, UserDto>()
    .ForMember(d => d.FullName, opt => opt.MapFrom<FullNameResolver>());

Conditions & PreConditions

CreateMap<User, UserDto>()
    // Only map if condition is true
    .ForMember(d => d.Email, opt => opt.Condition(src => src.IsEmailPublic))

    // Pre-condition checked before property access
    .ForMember(d => d.Address, opt => opt.PreCondition(src => src.HasAddress))

    // 3-argument condition: access source, destination, and current value
    .ForMember(d => d.Name, opt =>
        opt.Condition((src, dst, currentValue) => currentValue != null));

Before/After Map Hooks

Inline hooks:

CreateMap<Order, OrderDto>()
    .BeforeMap((src, dst) => Console.WriteLine($"Mapping order {src.Id}"))
    .AfterMap((src, dst) => dst.MappedAt = DateTime.UtcNow);

DI-resolved hooks:

public class AuditAction : IMappingAction<Order, OrderDto>
{
    private readonly IAuditService _audit;
    public AuditAction(IAuditService audit) => _audit = audit;

    public void Process(Order source, OrderDto destination)
    {
        _audit.Log($"Mapped order {source.Id}");
    }
}

CreateMap<Order, OrderDto>()
    .AfterMap<AuditAction>();  // Resolved from DI container

Value Transformers

Apply transformations to all properties of a given type:

CreateMap<User, UserDto>()
    .AddTransform<string>(s => s?.Trim() ?? "");  // Trim all string properties

Null Substitution

CreateMap<User, UserDto>()
    .ForMember(d => d.Name, opt => opt.NullSubstitute("N/A"))
    .ForMember(d => d.Email, opt => opt.NullSubstitute("no-email@example.com"));

Constructor Mapping

CreateMap<User, UserDto>()
    .ConstructUsing(src => new UserDto(src.Id, src.Name));

Map Onto Existing Object

var existingDto = new UserDto { Id = 1, Name = "Old Name" };
mapper.Map(user, existingDto);
// existingDto is now updated with values from user

Max Depth

Prevent infinite recursion with self-referencing types:

CreateMap<Employee, EmployeeDto>()
    .MaxDepth(3);  // Stop mapping nested Employee references after 3 levels

Advanced Features

IQueryable Projection (ProjectTo)

Generate SQL-efficient projections for Entity Framework and other LINQ providers:

var projectionProvider = serviceProvider.GetRequiredService<IProjectionProvider>();

var dtos = dbContext.Users
    .Where(u => u.IsActive)
    .ProjectTo<User, UserDto>(projectionProvider)
    .ToListAsync();

Parameterized projections:

var paramProvider = serviceProvider.GetRequiredService<IParameterizedProjectionProvider>();

var dtos = dbContext.Users
    .ProjectTo<User, UserDto>(paramProvider, new { CurrentUserId = userId });

Async Mapping

public class EnrichAction : IAsyncMappingAction<User, UserDto>
{
    private readonly IExternalService _service;
    public EnrichAction(IExternalService service) => _service = service;

    public async Task ProcessAsync(User source, UserDto destination, CancellationToken ct)
    {
        destination.ExternalData = await _service.GetDataAsync(source.Id, ct);
    }
}

// Use
var asyncMapper = serviceProvider.GetRequiredService<IAsyncMapper>();
var dto = await asyncMapper.MapAsync<User, UserDto>(user, cancellationToken);

Multi-Source Mapping

Combine multiple source objects into a single destination:

// Two sources
var dto = mapper.Map<User, Address, UserWithAddressDto>(user, address);

// Three sources
var dto = mapper.Map<User, Address, Preferences, FullProfileDto>(user, address, prefs);

// Dynamic number of sources
var dto = mapper.MapFromMultiple<FullProfileDto>(user, address, prefs);

Diff-Based Mapping

Track which properties changed between two versions:

var existingDto = mapper.Map<Order, OrderDto>(existingOrder);
var result = mapper.MapDiff(oldOrder, updatedOrder, existingDto);

if (result.HasChanges)
{
    foreach (var change in result.Changes)
    {
        Console.WriteLine($"{change.PropertyName}: {change.OldValue}{change.NewValue}");
    }
}

Patch Semantics (MapIgnoreNull)

Only map non-null values — ideal for PATCH endpoints:

CreateMap<UserPatchDto, User>()
    .MapIgnoreNull();

// Only non-null fields overwrite the target
var patch = new UserPatchDto { Name = "New Name", Email = null };
mapper.Map(patch, existingUser);
// existingUser.Name = "New Name"
// existingUser.Email = unchanged

String Template Mapping

CreateMap<User, UserDto>()
    .ForMember(d => d.DisplayName, opt => opt.MapFromTemplate("{FirstName} {LastName}"))
    .ForMember(d => d.Greeting, opt => opt.MapFromTemplate("Hello, {FirstName}!"));

Include Members (Flatten Nested)

Expose nested object properties at the top level:

public class Source
{
    public string Name { get; set; }
    public InnerSource Inner { get; set; }
}
public class InnerSource { public int Value { get; set; } public string Description { get; set; } }
public class Dest { public string Name { get; set; } public int Value { get; set; } public string Description { get; set; } }

CreateMap<Source, Dest>()
    .IncludeMembers(s => s.Inner);

Inheritance & Polymorphism

// Derived type mapping
CreateMap<Person, PersonDto>();
CreateDerivedMap<Person, PersonDto, Employee, EmployeeDto>();

// Or auto-discover all derived types
CreateMap<Person, PersonDto>()
    .IncludeAllDerived();

// Inherit base mapping configuration
CreateMap<Employee, EmployeeDto>()
    .IncludeBase<Person, PersonDto>();

Naming Conventions & Affixes

CreateMap<Source, Dest>()
    .SourceMemberNamingConvention(NamingConvention.CamelCase)
    .DestinationMemberNamingConvention(NamingConvention.PascalCase)
    .RecognizePrefixes("Get", "Is")         // GetName → Name
    .RecognizePostfixes("Dto")              // NameDto → Name
    .RecognizeDestinationPrefixes("Set")
    .RecognizeDestinationPostfixes("Field");

Immutable Collection Support

public class Source { public List<int> Items { get; set; } }
public class Dest { public ImmutableList<int> Items { get; set; } }

CreateMap<Source, Dest>();  // Automatic conversion to ImmutableList/ImmutableArray

Dictionary to Object Mapping

var dict = new Dictionary<string, object>
{
    ["Id"] = 42,
    ["Name"] = "John",
    ["Email"] = "john@example.com"
};

var user = mapper.Map<Dictionary<string, object>, User>(dict);

Open Generic Mappings

CreateMap(typeof(ApiResponse<>), typeof(ApiResponseDto<>));

var dto = mapper.Map<ApiResponse<User>, ApiResponseDto<UserDto>>(response);

Diagnostics & Monitoring

services.AddSwiftMapping(cfg => cfg.EnableDiagnostics());

var diagnostics = serviceProvider.GetRequiredService<IMappingDiagnostics>();

// Summary
Console.WriteLine($"Total mappings: {diagnostics.TotalMappingCount}");
Console.WriteLine($"Total time: {diagnostics.TotalElapsedMilliseconds} ms");

// Per-type statistics with percentile latency
foreach (var stat in diagnostics.GetStatsByType())
{
    Console.WriteLine($"{stat.SourceTypeName}{stat.DestinationTypeName}");
    Console.WriteLine($"  Count: {stat.Count}, Avg: {stat.AverageMs:F3} ms");
    Console.WriteLine($"  P50: {stat.P50Ms:F3}, P95: {stat.P95Ms:F3}, P99: {stat.P99Ms:F3} ms");
}

// Slowest mappings
var slowest = diagnostics.GetSlowestMappings(10);

// Real-time event stream
diagnostics.OnMappingCompleted += entry =>
{
    if (entry.ElapsedMilliseconds > 10)
        logger.LogWarning("Slow mapping: {Src}→{Dst} {Ms}ms",
            entry.SourceType.Name, entry.DestinationType.Name, entry.ElapsedMilliseconds);
};

// Export for APM integration (JSON)
string json = diagnostics.ExportAsJson();

Hot Reload

services.AddSwiftMapping(cfg =>
{
    cfg.EnableHotReload();
    cfg.AddProfile<AppProfile>();
});

// Reload at runtime
var config = serviceProvider.GetRequiredService<IMapperConfiguration>();
config.Reload(new UpdatedProfile());

config.OnReloaded += () => logger.LogInformation("Mapper configuration reloaded");

Configuration Validation

services.AddSwiftMapping(cfg =>
{
    cfg.AddProfile<AppProfile>();
    cfg.AssertConfigurationIsValid();  // Throws if unmapped properties
});

// Or inspect results
var results = cfg.Validate();
foreach (var r in results.Where(r => !r.IsValid))
{
    Console.WriteLine($"{r.SourceType.Name}{r.DestinationType.Name}:");
    foreach (var prop in r.UnmappedProperties)
        Console.WriteLine($"  Unmapped: {prop}");
}

Warmup / JIT Pre-compilation

Eliminate cold-start latency:

mapper.WarmUp<User, UserDto>();
mapper.WarmUp<Order, OrderDto>();

// Or warm up all configured mappings
mapper.WarmUpAll();

Dependency Injection

services.AddSwiftMapping(cfg =>
{
    // Profiles
    cfg.AddProfile<OrderProfile>();
    cfg.AddProfile(new DynamicProfile());
    cfg.AddProfilesFromAssembly(typeof(OrderProfile).Assembly);
    cfg.AddProfilesFromAssemblyContaining<OrderProfile>();

    // Converters
    cfg.AddConverter<MoneyConverter>();

    // Features
    cfg.EnableDiagnostics();
    cfg.EnableHotReload();

    // Validation
    cfg.AssertConfigurationIsValid();
});

Injectable services:

Interface Description
IMapper Synchronous object mapping
IAsyncMapper Asynchronous mapping with hooks
IProjectionProvider IQueryable projection expressions
IParameterizedProjectionProvider Parameterized projections
IMappingDiagnostics Performance diagnostics (when enabled)
IMapperConfiguration Hot reload control (when enabled)

Target Frameworks

Package Targets
SwiftMapping net10.0 · net8.0 · netstandard2.0 · net462
SwiftMapping.Contracts netstandard2.0
SwiftMapping.Generator netstandard2.0 (Roslyn analyzer)
  • net8.0+ — Full AOT and trimming compatibility
  • netstandard2.0 / net462 — Broad .NET Framework and .NET Core support

Migration from AutoMapper

SwiftMapping's API is designed to be familiar to AutoMapper users:

AutoMapper SwiftMapping Notes
cfg.CreateMap<S, D>() CreateMap<S, D>() Same
.ForMember(d => d.X, opt => opt.MapFrom(...)) Same Same
.ReverseMap() Same Same
.ConvertUsing<T>() Same Same
IMapper.Map<D>(src) IMapper.Map<S, D>(src) Typed source for fast path
mapper.ProjectTo<D>(cfg) source.ProjectTo<S, D>(provider) Provider-based
cfg.AddMaps(assembly) cfg.AddProfilesFromAssembly(asm) Explicit method name
Paid license (v14+) MIT — free No license key

Complete Feature Matrix

Feature Status
Property name matching
Flattening (Address.CityAddressCity)
Unflattening (reverse)
Nested object mapping
Collection mapping (List, Array, IEnumerable)
Immutable collections (ImmutableList<T>, ImmutableArray<T>)
Custom property mapping (ForMember / MapFrom)
Property ignoring
Conditions & PreConditions
3-argument conditions
Null substitution
Constant values (UseValue)
Custom type converters
Custom value resolvers
Constructor mapping (ConstructUsing)
Map onto existing object
Reverse mapping
Before/After map hooks
DI-resolved hooks & converters
Value transformers
Include members (flatten nested)
Max depth
Naming conventions (PascalCase / camelCase / snake_case)
Property prefix/postfix recognition
Inheritance & polymorphism
Include all derived types
Open generic mappings
Dictionary → POCO mapping
IQueryable projection (ProjectTo)
Parameterized projections
Async mapping
Multi-source mapping
Diff-based mapping
Patch semantics (MapIgnoreNull)
String template mapping
Mapping diagnostics & statistics
Percentile latency (P50/P90/P95/P99)
JSON diagnostics export
Hot reload
Configuration validation
Warmup / JIT pre-compilation
DI integration
AOT / Trim compatible
Source-generated (zero reflection)
Compile-time diagnostics
Multi-target (.NET 10, 8, Standard 2.0, Framework 4.6.2)

Support

If you find SwiftMapping useful, consider buying me a coffee:

Buy Me a Coffee

License

MIT — free and open source.

Copyright © 2026 Rev. Fr. Bedros Buldukian (Arman Buldukoglu)

About

No description, website, or topics provided.

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors