SwiftMap 1.1.0
dotnet add package SwiftMap --version 1.1.0
NuGet\Install-Package SwiftMap -Version 1.1.0
<PackageReference Include="SwiftMap" Version="1.1.0" />
<PackageVersion Include="SwiftMap" Version="1.1.0" />
<PackageReference Include="SwiftMap" />
paket add SwiftMap --version 1.1.0
#r "nuget: SwiftMap, 1.1.0"
#:package SwiftMap@1.1.0
#addin nuget:?package=SwiftMap&version=1.1.0
#tool nuget:?package=SwiftMap&version=1.1.0
<div align="right"><a href="README.pt-BR.md">π§π· PortuguΓͺs</a></div>
<div align="center">
SwiftMap
Zero-allocation, expression-tree-compiled object mapper for .NET
Convention-based property mapping backed by compiled expression trees β zero reflection at call time. Source generator mode achieves manual-mapping parity at compile time (Mapperly-style).
</div>
Why SwiftMap?
- Source generator mode β
[Mapper]partial class emits mapping bodies at compile time; zero startup cost, zero delegate dispatch, zero reflection at call time β identical to hand-written code - Runtime expression-tree mode β mappings compile once to native delegates via FastExpressionCompiler; subsequent calls are allocation-free
- Fluent, discoverable API β
ForMember,Ignore,AfterMap,ReverseMap,NullSubstitute,Condition,Patch, and more - No heavy dependencies β single NuGet package; only depends on
Microsoft.Extensions.DependencyInjection.Abstractions - Records & init-only properties β full support for C# 9+ records via primary constructor selection
- First-class DI support β drop-in
services.AddSwiftMap(...)with profile scanning
Installation
.NET CLI
dotnet add package SwiftMap
Package Manager Console
Install-Package SwiftMap
PackageReference
<PackageReference Include="SwiftMap" Version="1.0.0" />
Quick Start
Source Generator Mode (recommended β compile-time, zero overhead)
using SwiftMap;
[Mapper]
public partial class AppMapper
{
public partial PersonDto Map(Person source);
public partial OrderDto Map(Order source);
public partial ProductDto Map(Product source);
}
// Usage β no DI, no startup cost, velocity = manual code
var mapper = new AppMapper();
var dto = mapper.Map(person);
The [Mapper] attribute triggers the included Roslyn source generator. It analyses your types at compile time and emits efficient object-initializer bodies β supporting flat objects, nested objects (null-safe is-pattern), collections (for-loop, no LINQ), records (primary constructor), enums, and nullable unwrapping.
Runtime Expression-Tree Mode
// 1. Create a mapper β once at startup or via DI
var mapper = Mapper.Create(cfg =>
cfg.CreateMap<PersonSource, PersonDest>());
// 2. Map by source type inference
var dest = mapper.Map<PersonDest>(source);
// 3. Map with explicit type parameters (slightly faster β no GetType() call)
var dest = mapper.Map<PersonSource, PersonDest>(source);
// 4. Map into an existing instance
mapper.Map(source, existingDestination);
Configuration
Convention mapping β no config needed
Matching properties are wired up automatically by name (case-insensitive):
var mapper = Mapper.Create(_ => { });
var dto = mapper.Map<CustomerDto>(customer);
Fluent API
var mapper = Mapper.Create(cfg =>
cfg.CreateMap<Customer, CustomerDto>(map =>
map.ForMember(d => d.AddressCity, opt => opt.MapFrom(s => s.Address!.City))
.Ignore(d => d.InternalId)
.ForMember(d => d.Name, opt => opt.NullSubstitute("Unknown"))
.ForMember(d => d.Score, opt => opt.Condition(s => s.IsActive))
.AfterMap((src, dest) => dest.Name = dest.Name.ToUpperInvariant())));
Reverse mapping
cfg.CreateMap<OrderDto, Order>()
.ReverseMap(); // registers the inverse mapping automatically
Profiles
Organise large sets of mappings into cohesive units:
public class AppProfile : MapProfile
{
public AppProfile()
{
CreateMap<Customer, CustomerDto>()
.ForMember(d => d.AddressCity, opt => opt.MapFrom(s => s.Address!.City));
CreateMap<Order, OrderDto>().ReverseMap();
}
}
var mapper = Mapper.Create(cfg => cfg.AddProfile<AppProfile>());
Attribute-driven mapping
[MapTo(typeof(ProductDto))]
public class Product { ... }
public class TargetDto
{
[MapProperty("FullName")] // maps from a differently-named source property
public string Name { get; set; }
[IgnoreMap] // skipped during mapping
public string Secret { get; set; }
}
Dependency Injection
// Inline configuration
services.AddSwiftMap(cfg => cfg.CreateMap<Order, OrderDto>());
// Scan assemblies for MapProfile subclasses and [MapTo]/[MapFrom] attributes
services.AddSwiftMap(typeof(Program).Assembly);
HTTP PATCH β Patch Semantics
Patch applies only the non-null fields from source onto an existing destination instance, leaving everything else untouched. Designed for HTTP PATCH endpoints where the client sends only the fields it wants to change.
// Only Name was sent β Age and Email are null (not provided by client)
var updateDto = new UpdateUserDto { Name = "JoΓ£o", Age = null };
var user = await dbContext.Users.FindAsync(id); // { Name = "Maria", Age = 30, Email = "m@m.com" }
mapper.Patch(updateDto, user);
// Result: { Name = "JoΓ£o", Age = 30, Email = "m@m.com" }
// Age and Email were preserved β they were null in the dto
Works automatically by convention β no CreateMap required. For advanced scenarios:
// Skip fields that match their default value (0, false, Guid.Empty, etc.)
mapper.Patch(dto, entity, cfg => cfg.AsPatch(PatchBehavior.SkipDefaultFields));
// Register patch behavior in a profile
public class AppProfile : MapProfile
{
public AppProfile()
{
CreateMap<UpdateUserDto, User>().AsPatch();
}
}
| Source field type | Behavior |
|---|---|
string / reference type |
Skipped if null |
Nullable<T> (int?, bool?, β¦) |
Skipped if !HasValue |
Non-nullable value type (int, bool, β¦) |
Always applied (cannot be null) |
Benchmarks
Benchmarked against AutoMapper 13.0.1 and Mapster 7.4.0 on .NET 9.
BenchmarkDotNet v0.15.8 Β· Windows 11 Β· AMD Ryzen 5 3600 3.60GHz (6C/12T)
.NET SDK 9.0.312 Β· .NET 9.0.14 Β· X64 RyuJIT x86-64-v3
ShortRun: 3 warmups + 7 iterations
Simple flat object (7 properties)
| Method | Mean | Error | StdDev | Ratio | Allocated |
|---|---|---|---|---|---|
| Manual | 15.19 ns | 4.863 ns | 2.159 ns | baseline | 64 B |
| SwiftGenerated | 15.27 ns | 5.110 ns | 2.269 ns | 1.03Γ | 64 B |
| Mapster | 30.37 ns | 4.921 ns | 2.185 ns | 2.04Γ slower | 64 B |
| SwiftMap (runtime) | 39.75 ns | 8.234 ns | 3.656 ns | 2.67Γ slower | 64 B |
| AutoMapper | 89.95 ns | 17.732 ns | 7.873 ns | 6.04Γ slower | 64 B |
Nested object (parent + child)
| Method | Mean | Error | StdDev | Ratio | Allocated |
|---|---|---|---|---|---|
| Manual | 22.18 ns | 6.680 ns | 2.966 ns | baseline | 104 B |
| SwiftGenerated | 22.97 ns | 6.421 ns | 2.851 ns | 1.05Γ | 104 B |
| Mapster | 40.52 ns | 8.772 ns | 3.895 ns | 1.86Γ slower | 104 B |
| SwiftMap (runtime) | 48.76 ns | 13.197 ns | 5.859 ns | 2.23Γ slower | 104 B |
| AutoMapper | 102.62 ns | 18.835 ns | 8.363 ns | 4.70Γ slower | 104 B |
Collection mapping
| Method | Count | Mean | Error | StdDev | Ratio | Allocated |
|---|---|---|---|---|---|---|
| Manual | 100 | 1.425 Β΅s | 0.534 Β΅s | 0.237 Β΅s | baseline | 7.05 KB |
| SwiftGenerated | 100 | 1.519 Β΅s | 0.573 Β΅s | 0.254 Β΅s | 1.09Γ | 7.05 KB |
| Mapster | 100 | 2.637 Β΅s | 0.542 Β΅s | 0.241 Β΅s | 1.90Γ slower | 7.05 KB |
| SwiftMap (runtime) | 100 | 4.372 Β΅s | 0.968 Β΅s | 0.430 Β΅s | 3.14Γ slower | 7.05 KB |
| AutoMapper | 100 | 8.486 Β΅s | 3.128 Β΅s | 1.389 Β΅s | 6.10Γ slower | 7.05 KB |
| Manual | 1000 | 14.202 Β΅s | 5.038 Β΅s | 2.237 Β΅s | baseline | 70.34 KB |
| SwiftGenerated | 1000 | 15.540 Β΅s | 4.086 Β΅s | 1.814 Β΅s | 1.12Γ | 70.34 KB |
| Mapster | 1000 | 30.888 Β΅s | 7.347 Β΅s | 3.262 Β΅s | 2.23Γ slower | 70.34 KB |
| SwiftMap (runtime) | 1000 | 43.908 Β΅s | 13.642 Β΅s | 6.057 Β΅s | 3.17Γ slower | 70.34 KB |
| AutoMapper | 1000 | 91.405 Β΅s | 14.038 Β΅s | 6.233 Β΅s | 6.59Γ slower | 70.34 KB |
Record (primary constructor)
| Method | Mean | Error | StdDev | Ratio | Allocated |
|---|---|---|---|---|---|
| Manual | 11.27 ns | 4.766 ns | 2.116 ns | baseline | 48 B |
| SwiftGenerated | 12.89 ns | 4.260 ns | 1.891 ns | 1.18Γ | 48 B |
| Mapster | 30.10 ns | 6.656 ns | 2.955 ns | 2.74Γ slower | 48 B |
| SwiftMap (runtime) | 37.35 ns | 7.100 ns | 3.153 ns | 3.40Γ slower | 48 B |
| AutoMapper | 86.29 ns | 16.937 ns | 7.520 ns | 7.86Γ slower | 48 B |
At a glance
| Scenario | SwiftGenerated vs Manual | SwiftGenerated vs Mapster | SwiftGenerated vs AutoMapper |
|---|---|---|---|
| Simple object | β parity (1.03Γ) | 2.0Γ faster | 5.9Γ faster |
| Nested object | β parity (1.05Γ) | 1.8Γ faster | 4.5Γ faster |
| Collection Γ1000 | β parity (1.12Γ) | 2.0Γ faster | 5.9Γ faster |
| Record | β parity (1.18Γ) | 2.3Γ faster | 6.7Γ faster |
All measurements: same allocated memory as manual code (no overhead).
Project Structure
src/
βββ SwiftMap/ # Runtime expression-tree mapper
β βββ Mapper.cs # Entry point β Mapper.Create(...)
β βββ IMapper.cs # Public interface
β βββ MapperConfig.cs # Configuration + compiled delegate cache
β βββ TypeMapConfig.cs # Fluent API: ForMember, Ignore, AfterMap...
β βββ MapProfile.cs # Base class for profiles
β βββ Attributes/
β β βββ MapToAttribute.cs # [MapTo], [MapFrom], [IgnoreMap], [MapProperty], [Mapper]
β βββ Extensions/
β β βββ ServiceCollectionExtensions.cs # AddSwiftMap(...)
β βββ Internal/
β βββ MappingCompiler.cs # Expression tree compiler (core engine)
β βββ TypePair.cs # (Source, Destination) dictionary key
βββ SwiftMap.SourceGenerator/ # Roslyn IIncrementalGenerator
βββ MapperGenerator.cs # [Generator] entry point
βββ Models/ # MapperClassModel, MappingMethodModel, MappedPropertyModel
βββ Pipeline/
β βββ MapperModelExtractor.cs # Semantic analysis
βββ Emit/
βββ MappingBodyEmitter.cs # Code generation
βββ SourceWriter.cs # Indented string builder
Roadmap
- FastExpressionCompiler integration β
CompileFast()replacesExpression.Compile()for faster delegate creation - Source generator mode β
[Mapper]partial class emits mapping bodies at compile time; reaches manual-mapping parity - Patch semantics β
mapper.Patch(dto, entity)applies only non-null fields; built-in support for HTTP PATCH endpoints - Async mapping β
MapAsync<TDest>(source)for pipelines that need async value resolution - IQueryable projection β
ProjectTo<TDest>()for ORM query projection - NuGet release β
SwiftMapis available on nuget.org/packages/SwiftMap
Contributing
Contributions are welcome! See CONTRIBUTING.md for full guidelines.
Quick steps:
- Fork and create a feature branch (
git checkout -b feature/my-feature) - Add or update tests to cover your change
- Run the benchmarks to verify no performance regression:
dotnet run -c Release --project benchmarks/SwiftMap.Benchmarks - Open a pull request β for significant changes, open an issue first to discuss the approach
License
SwiftMap is released under the MIT License.
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net9.0 is compatible. net9.0-android was computed. net9.0-browser was computed. net9.0-ios was computed. net9.0-maccatalyst was computed. net9.0-macos was computed. net9.0-tvos was computed. net9.0-windows was computed. net10.0 was computed. net10.0-android was computed. net10.0-browser was computed. net10.0-ios was computed. net10.0-maccatalyst was computed. net10.0-macos was computed. net10.0-tvos was computed. net10.0-windows was computed. |
-
net9.0
- FastExpressionCompiler (>= 5.3.0)
- Microsoft.Extensions.DependencyInjection.Abstractions (>= 9.0.14)
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.