feat: same-type auto-mapping — T → T without CreateMap#89
Merged
cloud-hai-vo merged 1 commit intomainfrom Mar 27, 2026
Merged
Conversation
mapper.Map<DeliveryProvider, DeliveryProvider>(src) now works without any explicit CreateMap<DeliveryProvider, DeliveryProvider>() registration. A property-copy delegate is compiled on first use and cached for subsequent calls. - Auto-compiles via synthetic TypeMap using existing ExpressionBuilder - Wired into MapSlow, MapInternal, and FindElementDelegate - Explicit CreateMap<T,T> still takes precedence if registered - Skips primitives, strings, enums, value types, collections, dicts - Same-type elements in List<T> auto-mapped too - 7 new tests covering all paths Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Contributor
📊 Benchmark Results
🔵 Flat Mapping — 10-property object
🟡 Flattening — 2 nested objects → 8 flat properties
🟣 Deep Mapping — 2 nested address objects
🟢 Complex Mapping — nested object + collection
🟠 Collection — 100-item
|
| Method | Mean | Error | StdDev | Min | Median | Max | Ratio | RatioSD | Rank | Gen0 | Gen1 | Allocated | Alloc Ratio |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Manual | 1.849 μs | 0.6793 μs | 0.0372 μs | 1.814 μs | 1.843 μs | 1.888 μs | 1.00 | 0.02 | 1 | 0.5283 | 0.0172 | 8.65 KB | 1.00 |
| EggMapper | 1.895 μs | 0.1282 μs | 0.0070 μs | 1.889 μs | 1.894 μs | 1.903 μs | 1.03 | 0.02 | 1 | 0.5283 | 0.0172 | 8.65 KB | 1.00 |
| AutoMapper | 2.521 μs | 0.3284 μs | 0.0180 μs | 2.501 μs | 2.528 μs | 2.535 μs | 1.36 | 0.03 | 2 | 0.6065 | 0.0191 | 9.95 KB | 1.15 |
| Mapster | 1.910 μs | 0.2894 μs | 0.0159 μs | 1.896 μs | 1.907 μs | 1.927 μs | 1.03 | 0.02 | 1 | 0.5283 | 0.0172 | 8.65 KB | 1.00 |
| MapperlyMap | 1.928 μs | 0.6417 μs | 0.0352 μs | 1.888 μs | 1.943 μs | 1.953 μs | 1.04 | 0.02 | 1 | 0.5264 | 0.0153 | 8.65 KB | 1.00 |
| AgileMapper | 2.949 μs | 0.4470 μs | 0.0245 μs | 2.923 μs | 2.952 μs | 2.971 μs | 1.60 | 0.03 | 2 | 0.5417 | 0.0153 | 8.91 KB | 1.03 |
🟠 Collection — 100-item List<T>
| Method | Mean | Error | StdDev | Min | Median | Max | Ratio | RatioSD | Rank | Gen0 | Gen1 | Allocated | Alloc Ratio |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Manual | 5.695 μs | 1.3237 μs | 0.0726 μs | 5.625 μs | 5.690 μs | 5.770 μs | 1.00 | 0.02 | 1 | 1.6708 | 0.0916 | 27.4 KB | 1.00 |
| EggMapper | 6.265 μs | 2.0141 μs | 0.1104 μs | 6.140 μs | 6.311 μs | 6.346 μs | 1.10 | 0.02 | 1 | 1.6708 | 0.0916 | 27.4 KB | 1.00 |
| AutoMapper | 7.124 μs | 1.0014 μs | 0.0549 μs | 7.061 μs | 7.156 μs | 7.156 μs | 1.25 | 0.02 | 1 | 1.7548 | 0.1068 | 28.7 KB | 1.05 |
| Mapster | 6.358 μs | 3.0200 μs | 0.1655 μs | 6.185 μs | 6.372 μs | 6.516 μs | 1.12 | 0.03 | 1 | 1.6708 | 0.0916 | 27.4 KB | 1.00 |
| MapperlyMap | 5.796 μs | 0.8013 μs | 0.0439 μs | 5.749 μs | 5.802 μs | 5.836 μs | 1.02 | 0.01 | 1 | 1.6785 | 0.0992 | 27.42 KB | 1.00 |
| AgileMapper | 5.446 μs | 0.7086 μs | 0.0388 μs | 5.401 μs | 5.465 μs | 5.471 μs | 0.96 | 0.01 | 1 | 1.0223 | 0.0610 | 16.72 KB | 0.61 |
🟠 Collection — 100-item List<T>
| Method | Mean | Error | StdDev | Min | Median | Max | Ratio | RatioSD | Rank | Gen0 | Gen1 | Allocated | Alloc Ratio |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Manual | 18.61 μs | 9.647 μs | 0.529 μs | 18.07 μs | 18.63 μs | 19.12 μs | 1.00 | 0.03 | 1 | 5.2490 | 1.3123 | 85.99 KB | 1.00 |
| EggMapper | 18.28 μs | 6.372 μs | 0.349 μs | 17.91 μs | 18.34 μs | 18.60 μs | 0.98 | 0.03 | 1 | 5.2490 | 1.3123 | 85.99 KB | 1.00 |
| AutoMapper | 22.89 μs | 1.476 μs | 0.081 μs | 22.80 μs | 22.90 μs | 22.96 μs | 1.23 | 0.03 | 1 | 5.7678 | 1.4343 | 94.34 KB | 1.10 |
| Mapster | 18.52 μs | 8.079 μs | 0.443 μs | 18.01 μs | 18.78 μs | 18.78 μs | 1.00 | 0.03 | 1 | 5.2490 | 1.3123 | 85.99 KB | 1.00 |
| MapperlyMap | 20.04 μs | 1.552 μs | 0.085 μs | 19.98 μs | 20.00 μs | 20.14 μs | 1.08 | 0.03 | 1 | 5.2490 | 1.2817 | 86.02 KB | 1.00 |
| AgileMapper | 21.59 μs | 3.649 μs | 0.200 μs | 21.36 μs | 21.67 μs | 21.73 μs | 1.16 | 0.03 | 1 | 5.2795 | 1.3123 | 86.25 KB | 1.00 |
⚪ Startup / Configuration time
| Method | Mean | Error | StdDev | Min | Median | Max | Ratio | RatioSD | Rank | Gen0 | Gen1 | Allocated | Alloc Ratio |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| EggMapperStartup | 1,348.285 μs | 1,697.4918 μs | 93.0453 μs | 1,257.711 μs | 1,343.525 μs | 1,443.619 μs | 1.003 | 0.08 | 3 | 3.9063 | 1.9531 | 95.26 KB | 1.00 |
| AutoMapperStartup | 399.302 μs | 962.9685 μs | 52.7836 μs | 338.423 μs | 427.213 μs | 432.271 μs | 0.297 | 0.04 | 2 | 5.8594 | - | 104.34 KB | 1.10 |
| MapsterStartup | 2.655 μs | 0.4913 μs | 0.0269 μs | 2.624 μs | 2.666 μs | 2.674 μs | 0.002 | 0.00 | 1 | 0.7019 | 0.0267 | 11.51 KB | 0.12 |
EggMapper.Benchmarks.ColdStartBenchmark-report-github
| Method | Mean | Error | StdDev | Min | Median | Max | Ratio | RatioSD | Rank | Gen0 | Gen1 | Allocated | Alloc Ratio |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| EggMapper | 1.321 ms | 0.9105 ms | 0.0499 ms | 1.282 ms | 1.304 ms | 1.377 ms | 1.00 | 0.05 | 1 | 5.8594 | 3.9063 | 96.75 KB | 1.00 |
| AutoMapper | 4.361 ms | 11.2128 ms | 0.6146 ms | 3.741 ms | 4.371 ms | 4.970 ms | 3.30 | 0.42 | 2 | 15.6250 | 7.8125 | 310.99 KB | 3.21 |
| Mapster | 4.092 ms | 12.9330 ms | 0.7089 ms | 3.410 ms | 4.041 ms | 4.825 ms | 3.10 | 0.48 | 2 | 39.0625 | 15.6250 | 757.22 KB | 7.83 |
📝 Notes
- Each benchmark class is decorated with
[MemoryDiagnoser]and[RankColumn]. - The global config (see
src/EggMapper.Benchmarks/Program.cs) addsMin,Median, andMaxcolumns. - Manual is the hand-written baseline (ratio = 1.00). A ratio < 1 means faster than manual.
- Benchmarks run on GitHub-hosted runners — absolute times may vary between runs; focus on Ratio for comparisons.
- To reproduce locally:
cd src/EggMapper.Benchmarks dotnet run --configuration Release -- --filter '*'
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
In DSP, code like
mapper.Map<DeliveryProvider, DeliveryProvider>(provider)throwsNo mapping configuredbecause there's no explicitCreateMap<DeliveryProvider, DeliveryProvider>(). This is a common pattern for cloning/copying objects.Fix
When no mapping is found and source type == dest type, auto-compile a property-copy delegate on first use and cache it. Uses the same
ExpressionBuilderinfrastructure as explicit maps — same performance after first call.How it works
MapSlow/MapInternalexhaust all registered maps (frozen, open generic, base-type, interface)sourceType == destTypeTypeMapand compile viaExpressionBuilderConcurrentDictionarywithGetOrAdd(thread-safe, one compilation per type)FastCache(zero-overhead)What it skips
Primitives, strings, enums, value types, collections, dictionaries — these don't need property-copy semantics.
Precedence
Explicit
CreateMap<T, T>()always takes precedence. Auto-mapping is only used when no registration exists.Test plan
🤖 Generated with Claude Code