Skip to content

feat: same-type auto-mapping — T → T without CreateMap#89

Merged
cloud-hai-vo merged 1 commit intomainfrom
feat/same-type-auto-map
Mar 27, 2026
Merged

feat: same-type auto-mapping — T → T without CreateMap#89
cloud-hai-vo merged 1 commit intomainfrom
feat/same-type-auto-map

Conversation

@cloud-hai-vo
Copy link
Copy Markdown
Contributor

Problem

In DSP, code like mapper.Map<DeliveryProvider, DeliveryProvider>(provider) throws No mapping configured because there's no explicit CreateMap<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 ExpressionBuilder infrastructure as explicit maps — same performance after first call.

// No CreateMap needed — just works
var copy = mapper.Map<DeliveryProvider, DeliveryProvider>(provider);
copy.Id == provider.Id    // true
copy != provider          // true (new instance)

How it works

  1. MapSlow / MapInternal exhaust all registered maps (frozen, open generic, base-type, interface)
  2. Before throwing, check if sourceType == destType
  3. If yes, create a synthetic TypeMap and compile via ExpressionBuilder
  4. Cache in ConcurrentDictionary with GetOrAdd (thread-safe, one compilation per type)
  5. Subsequent calls hit 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

  • 355 tests pass across net8.0, net9.0, net10.0
  • 7 new tests: property copy, single-type-arg, null source, nested objects, list elements, fast cache, explicit precedence

🤖 Generated with Claude Code

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>
@cloud-hai-vo cloud-hai-vo enabled auto-merge (squash) March 27, 2026 03:57
@cloud-hai-vo cloud-hai-vo merged commit a00d309 into main Mar 27, 2026
5 checks passed
@cloud-hai-vo cloud-hai-vo deleted the feat/same-type-auto-map branch March 27, 2026 03:58
@github-actions
Copy link
Copy Markdown
Contributor

📊 Benchmark Results

Generated: 2026-03-27 04:04 UTC  ·  Download full artifacts

Column guide:
Mean = average execution time  ·  Error = half of 99.9 % confidence interval  ·  StdDev = standard deviation  ·  Min / Median / Max = statistical range  ·  Ratio = vs Manual baseline (lower = closer to hand-written speed)  ·  RatioSD = ratio std dev  ·  Rank = 1 is fastest  ·  Gen0/1/2 = GC collections per 1 000 ops  ·  Allocated = managed heap per operation  ·  Alloc Ratio = allocation ratio vs baseline

🔵 Flat Mapping — 10-property object

Method Mean Error StdDev Min Median Max Ratio RatioSD Rank Gen0 Allocated Alloc Ratio
Manual 16.82 ns 0.705 ns 0.039 ns 16.77 ns 16.82 ns 16.85 ns 1.00 0.00 1 0.0048 80 B 1.00
EggMapper 27.27 ns 2.500 ns 0.137 ns 27.11 ns 27.35 ns 27.35 ns 1.62 0.01 2 0.0048 80 B 1.00
AutoMapper 86.90 ns 3.536 ns 0.194 ns 86.67 ns 87.00 ns 87.01 ns 5.17 0.01 3 0.0048 80 B 1.00
Mapster 29.41 ns 5.039 ns 0.276 ns 29.21 ns 29.30 ns 29.73 ns 1.75 0.01 2 0.0048 80 B 1.00
MapperlyMap 16.56 ns 1.392 ns 0.076 ns 16.49 ns 16.55 ns 16.64 ns 0.98 0.00 1 0.0048 80 B 1.00
AgileMapper 505.50 ns 25.659 ns 1.406 ns 503.93 ns 505.93 ns 506.64 ns 30.06 0.09 4 0.0200 344 B 4.30
EggMapperGenerator 16.70 ns 4.110 ns 0.225 ns 16.47 ns 16.70 ns 16.92 ns 0.99 0.01 1 0.0048 80 B 1.00
EggMapperClassMapper 16.28 ns 2.956 ns 0.162 ns 16.11 ns 16.29 ns 16.43 ns 0.97 0.01 1 0.0048 80 B 1.00

🟡 Flattening — 2 nested objects → 8 flat properties

Method Mean Error StdDev Min Median Max Ratio RatioSD Rank Gen0 Allocated Alloc Ratio
Manual 20.03 ns 3.925 ns 0.215 ns 19.78 ns 20.12 ns 20.18 ns 1.00 0.01 1 0.0048 80 B 1.00
EggMap 30.95 ns 1.864 ns 0.102 ns 30.84 ns 30.99 ns 31.03 ns 1.55 0.02 3 0.0048 80 B 1.00
AutoMapper 96.25 ns 2.028 ns 0.111 ns 96.14 ns 96.24 ns 96.36 ns 4.81 0.05 5 0.0048 80 B 1.00
Mapster 38.15 ns 5.476 ns 0.300 ns 37.81 ns 38.27 ns 38.37 ns 1.91 0.02 4 0.0048 80 B 1.00
MapperlyMap 25.47 ns 6.762 ns 0.371 ns 25.05 ns 25.67 ns 25.70 ns 1.27 0.02 2 0.0062 104 B 1.30
AgileMapper 530.13 ns 98.290 ns 5.388 ns 526.10 ns 528.03 ns 536.25 ns 26.47 0.34 6 0.0200 344 B 4.30

🟣 Deep Mapping — 2 nested address objects

Method Mean Error StdDev Min Median Max Ratio RatioSD Rank Gen0 Allocated Alloc Ratio
Manual 59.69 ns 16.298 ns 0.893 ns 59.17 ns 59.19 ns 60.73 ns 1.00 0.02 1 0.0162 272 B 1.00
EggMapper 69.99 ns 15.189 ns 0.833 ns 69.21 ns 69.90 ns 70.87 ns 1.17 0.02 1 0.0162 272 B 1.00
AutoMapper 127.34 ns 6.103 ns 0.335 ns 127.01 ns 127.35 ns 127.68 ns 2.13 0.03 2 0.0162 272 B 1.00
Mapster 73.83 ns 5.473 ns 0.300 ns 73.54 ns 73.82 ns 74.14 ns 1.24 0.02 1 0.0162 272 B 1.00
MapperlyMap 54.46 ns 24.970 ns 1.369 ns 52.89 ns 55.17 ns 55.34 ns 0.91 0.02 1 0.0162 272 B 1.00
AgileMapper 539.20 ns 19.529 ns 1.070 ns 538.15 ns 539.16 ns 540.29 ns 9.03 0.12 3 0.0248 424 B 1.56

🟢 Complex Mapping — nested object + collection

Method Mean Error StdDev Min Median Max Ratio RatioSD Rank Gen0 Allocated Alloc Ratio
Manual 76.10 ns 13.740 ns 0.753 ns 75.23 ns 76.50 ns 76.57 ns 1.00 0.01 1 0.0191 320 B 1.00
EggMapper 100.83 ns 12.826 ns 0.703 ns 100.02 ns 101.23 ns 101.24 ns 1.33 0.01 2 0.0191 320 B 1.00
AutoMapper 169.34 ns 27.747 ns 1.521 ns 168.14 ns 168.84 ns 171.05 ns 2.23 0.03 3 0.0196 328 B 1.02
Mapster 93.13 ns 18.509 ns 1.015 ns 91.98 ns 93.55 ns 93.87 ns 1.22 0.02 2 0.0191 320 B 1.00
MapperlyMap 76.87 ns 9.278 ns 0.509 ns 76.41 ns 76.78 ns 77.42 ns 1.01 0.01 1 0.0191 320 B 1.00
AgileMapper 572.99 ns 25.598 ns 1.403 ns 572.12 ns 572.24 ns 574.61 ns 7.53 0.07 4 0.0315 528 B 1.65

🟠 Collection — 100-item List<T>

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) adds Min, Median, and Max columns.
  • 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 '*'

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant