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.
- Installation
- Quick Start
- Performance Benchmarks
- Mapping Approaches
- Core Features
- Advanced Features
- Diagnostics & Monitoring
- Hot Reload
- Configuration Validation
- Warmup / JIT Pre-compilation
- Dependency Injection
- Target Frameworks
- Migration from AutoMapper
- License
dotnet add package SwiftMappingFor projects that only need marker interfaces and attributes (e.g., shared contract libraries):
dotnet add package SwiftMapping.Contractspublic 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; }
}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();
}
}// Attribute-based profiles are auto-discovered
services.AddSwiftMapping();
// Or add fluent profiles explicitly
services.AddSwiftMapping(cfg =>
{
cfg.AddProfile<AppProfile>();
});public class UserService
{
private readonly IMapper _mapper;
public UserService(IMapper mapper) => _mapper = mapper;
public UserDto GetUser(User user)
{
return _mapper.Map<User, UserDto>(user);
}
}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 usedMap<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.Benchmarksproject.
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 |
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 |
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>();
}
}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 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 AddressDtoCollections 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>)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.NameCreateMap<User, UserDto>().ReverseMap();
var dto = mapper.Map<User, UserDto>(user);
var back = mapper.Map<UserDto, User>(dto);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>();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>());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));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 containerApply transformations to all properties of a given type:
CreateMap<User, UserDto>()
.AddTransform<string>(s => s?.Trim() ?? ""); // Trim all string propertiesCreateMap<User, UserDto>()
.ForMember(d => d.Name, opt => opt.NullSubstitute("N/A"))
.ForMember(d => d.Email, opt => opt.NullSubstitute("no-email@example.com"));CreateMap<User, UserDto>()
.ConstructUsing(src => new UserDto(src.Id, src.Name));var existingDto = new UserDto { Id = 1, Name = "Old Name" };
mapper.Map(user, existingDto);
// existingDto is now updated with values from userPrevent infinite recursion with self-referencing types:
CreateMap<Employee, EmployeeDto>()
.MaxDepth(3); // Stop mapping nested Employee references after 3 levelsGenerate 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 });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);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);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}");
}
}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 = unchangedCreateMap<User, UserDto>()
.ForMember(d => d.DisplayName, opt => opt.MapFromTemplate("{FirstName} {LastName}"))
.ForMember(d => d.Greeting, opt => opt.MapFromTemplate("Hello, {FirstName}!"));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);// 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>();CreateMap<Source, Dest>()
.SourceMemberNamingConvention(NamingConvention.CamelCase)
.DestinationMemberNamingConvention(NamingConvention.PascalCase)
.RecognizePrefixes("Get", "Is") // GetName → Name
.RecognizePostfixes("Dto") // NameDto → Name
.RecognizeDestinationPrefixes("Set")
.RecognizeDestinationPostfixes("Field");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/ImmutableArrayvar dict = new Dictionary<string, object>
{
["Id"] = 42,
["Name"] = "John",
["Email"] = "john@example.com"
};
var user = mapper.Map<Dictionary<string, object>, User>(dict);CreateMap(typeof(ApiResponse<>), typeof(ApiResponseDto<>));
var dto = mapper.Map<ApiResponse<User>, ApiResponseDto<UserDto>>(response);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();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");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}");
}Eliminate cold-start latency:
mapper.WarmUp<User, UserDto>();
mapper.WarmUp<Order, OrderDto>();
// Or warm up all configured mappings
mapper.WarmUpAll();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) |
| 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
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 |
| Feature | Status |
|---|---|
| Property name matching | ✅ |
Flattening (Address.City → AddressCity) |
✅ |
| 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) | ✅ |
If you find SwiftMapping useful, consider buying me a coffee:
MIT — free and open source.
Copyright © 2026 Rev. Fr. Bedros Buldukian (Arman Buldukoglu)
