Skip to content

fix: items propagation, DynamicInvoke wrapping, thread safety#83

Merged
github-actions[bot] merged 1 commit intomainfrom
fix/review-round-fixes
Mar 26, 2026
Merged

fix: items propagation, DynamicInvoke wrapping, thread safety#83
github-actions[bot] merged 1 commit intomainfrom
fix/review-round-fixes

Conversation

@cloud-hai-vo
Copy link
Copy Markdown
Contributor

Context

Fixes from 3-round deep code review (Round 1: core mapper, Round 2: expression builder, Round 3: config + DI + tests).

Fixes

1. MapWithItems loses items dictionary on fallback (HIGH)

Before: MapWithItems<S,D>() fell back to Map<S,D>(source) which dropped the items dictionary. Resolvers that depend on ctx.Items would silently receive null.

After: Falls back to MapInternal() which preserves items through base-type walk, interface walk, and collection auto-mapping.

2. PatchSlow DynamicInvoke leaks TargetInvocationException (MEDIUM)

Before: Patch<Derived, Dest>() using base-type fallback threw raw TargetInvocationException — no type context, confusing stack trace.

After: Wraps in MappingException with readable runtime type names.

3. Concurrent open generic compilation waste (MEDIUM)

Before: Multiple threads calling Map<ApiResponse<Order>, ApiResponseDto<OrderDto>>() simultaneously would each independently compile the same delegate. Only one result stored; rest discarded.

After: GetOrAdd with factory delegate ensures only one thread compiles per type pair.

Test plan

  • 348 tests pass across net8.0, net9.0, net10.0
  • Existing thread safety tests still pass
  • Existing open generic tests still pass

🤖 Generated with Claude Code

…thread safety

Fixes from 3-round deep code review:

1. MapWithItems fallback now preserves items dictionary — was calling
   Map<S,D>(source) which dropped the items, now routes through
   MapInternal which propagates items to resolvers

2. PatchSlow base-type DynamicInvoke now wraps TargetInvocationException
   in MappingException with readable type names — was leaking raw
   TargetInvocationException to callers

3. TryGetOrCompileOpenGenericMap now uses GetOrAdd with factory delegate
   ensuring only one thread compiles per type pair — was allowing
   multiple threads to independently compile the same generic pair

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@github-actions github-actions Bot enabled auto-merge (squash) March 26, 2026 23:45
@github-actions github-actions Bot merged commit 29d9f7b into main Mar 26, 2026
5 checks passed
@github-actions
Copy link
Copy Markdown
Contributor

📊 Benchmark Results

Generated: 2026-03-26 23:52 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 14.26 ns 0.462 ns 0.025 ns 14.24 ns 14.26 ns 14.29 ns 1.00 0.00 1 0.0048 80 B 1.00
EggMapper 24.52 ns 1.248 ns 0.068 ns 24.46 ns 24.51 ns 24.60 ns 1.72 0.00 2 0.0048 80 B 1.00
AutoMapper 82.17 ns 2.144 ns 0.118 ns 82.08 ns 82.12 ns 82.30 ns 5.76 0.01 3 0.0048 80 B 1.00
Mapster 26.84 ns 0.838 ns 0.046 ns 26.79 ns 26.87 ns 26.87 ns 1.88 0.00 2 0.0048 80 B 1.00
MapperlyMap 14.09 ns 0.678 ns 0.037 ns 14.05 ns 14.09 ns 14.13 ns 0.99 0.00 1 0.0048 80 B 1.00
AgileMapper 498.71 ns 27.792 ns 1.523 ns 497.27 ns 498.55 ns 500.30 ns 34.97 0.11 4 0.0200 344 B 4.30
EggMapperGenerator 14.61 ns 0.779 ns 0.043 ns 14.56 ns 14.63 ns 14.63 ns 1.02 0.00 1 0.0048 80 B 1.00
EggMapperClassMapper 14.40 ns 1.324 ns 0.073 ns 14.33 ns 14.39 ns 14.48 ns 1.01 0.00 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 18.33 ns 6.044 ns 0.331 ns 18.09 ns 18.19 ns 18.71 ns 1.00 0.02 1 0.0048 80 B 1.00
EggMap 28.71 ns 3.175 ns 0.174 ns 28.58 ns 28.64 ns 28.90 ns 1.57 0.03 3 0.0048 80 B 1.00
AutoMapper 87.24 ns 3.666 ns 0.201 ns 87.06 ns 87.20 ns 87.45 ns 4.76 0.07 5 0.0048 80 B 1.00
Mapster 35.19 ns 6.358 ns 0.348 ns 34.90 ns 35.09 ns 35.57 ns 1.92 0.03 4 0.0048 80 B 1.00
MapperlyMap 23.50 ns 2.154 ns 0.118 ns 23.36 ns 23.56 ns 23.57 ns 1.28 0.02 2 0.0062 104 B 1.30
AgileMapper 514.05 ns 47.868 ns 2.624 ns 511.92 ns 513.24 ns 516.98 ns 28.05 0.45 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 51.38 ns 0.789 ns 0.043 ns 51.34 ns 51.39 ns 51.42 ns 1.00 0.00 1 0.0162 272 B 1.00
EggMapper 68.33 ns 7.621 ns 0.418 ns 68.07 ns 68.11 ns 68.82 ns 1.33 0.01 2 0.0162 272 B 1.00
AutoMapper 117.68 ns 5.301 ns 0.291 ns 117.39 ns 117.68 ns 117.97 ns 2.29 0.01 3 0.0162 272 B 1.00
Mapster 65.09 ns 3.467 ns 0.190 ns 64.92 ns 65.05 ns 65.29 ns 1.27 0.00 2 0.0162 272 B 1.00
MapperlyMap 47.14 ns 2.076 ns 0.114 ns 47.02 ns 47.15 ns 47.25 ns 0.92 0.00 1 0.0162 272 B 1.00
AgileMapper 499.92 ns 25.457 ns 1.395 ns 498.89 ns 499.38 ns 501.51 ns 9.73 0.02 4 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 66.69 ns 6.415 ns 0.352 ns 66.47 ns 66.50 ns 67.09 ns 1.00 0.01 1 0.0191 320 B 1.00
EggMapper 86.74 ns 0.776 ns 0.043 ns 86.69 ns 86.74 ns 86.78 ns 1.30 0.01 2 0.0191 320 B 1.00
AutoMapper 150.44 ns 7.631 ns 0.418 ns 150.10 ns 150.32 ns 150.91 ns 2.26 0.01 3 0.0196 328 B 1.02
Mapster 85.78 ns 2.001 ns 0.110 ns 85.66 ns 85.80 ns 85.87 ns 1.29 0.01 2 0.0191 320 B 1.00
MapperlyMap 68.40 ns 3.233 ns 0.177 ns 68.25 ns 68.35 ns 68.59 ns 1.03 0.01 1 0.0191 320 B 1.00
AgileMapper 573.91 ns 7.609 ns 0.417 ns 573.44 ns 574.04 ns 574.24 ns 8.61 0.04 4 0.0315 528 B 1.65

🟠 Collection — 100-item List<T>

Method Mean Error StdDev Min Median Max Ratio Rank Gen0 Gen1 Allocated Alloc Ratio
Manual 1.644 μs 0.0214 μs 0.0012 μs 1.643 μs 1.645 μs 1.645 μs 1.00 1 0.5283 0.0172 8.65 KB 1.00
EggMapper 1.682 μs 0.0687 μs 0.0038 μs 1.678 μs 1.683 μs 1.686 μs 1.02 1 0.5283 0.0172 8.65 KB 1.00
AutoMapper 2.249 μs 0.0717 μs 0.0039 μs 2.245 μs 2.250 μs 2.253 μs 1.37 2 0.6065 0.0191 9.95 KB 1.15
Mapster 1.677 μs 0.1059 μs 0.0058 μs 1.671 μs 1.677 μs 1.683 μs 1.02 1 0.5283 0.0172 8.65 KB 1.00
MapperlyMap 1.719 μs 0.4494 μs 0.0246 μs 1.704 μs 1.705 μs 1.747 μs 1.05 1 0.5283 0.0172 8.65 KB 1.00
AgileMapper 2.430 μs 0.1199 μs 0.0066 μs 2.423 μs 2.433 μs 2.435 μs 1.48 2 0.5417 0.0153 8.91 KB 1.03

🟠 Collection — 100-item List<T>

Method Mean Error StdDev Min Median Max Ratio Rank Gen0 Gen1 Allocated Alloc Ratio
Manual 5.074 μs 0.3246 μs 0.0178 μs 5.056 μs 5.075 μs 5.092 μs 1.00 1 1.6708 0.0916 27.4 KB 1.00
EggMapper 5.571 μs 0.4137 μs 0.0227 μs 5.556 μs 5.559 μs 5.597 μs 1.10 1 1.6708 0.0916 27.4 KB 1.00
AutoMapper 6.285 μs 0.0963 μs 0.0053 μs 6.280 μs 6.286 μs 6.291 μs 1.24 1 1.7548 0.1068 28.7 KB 1.05
Mapster 5.588 μs 0.1242 μs 0.0068 μs 5.582 μs 5.585 μs 5.595 μs 1.10 1 1.6708 0.0916 27.4 KB 1.00
MapperlyMap 5.115 μs 0.1515 μs 0.0083 μs 5.107 μs 5.115 μs 5.123 μs 1.01 1 1.6785 0.0992 27.42 KB 1.00
AgileMapper 5.017 μs 0.3602 μs 0.0197 μs 4.995 μs 5.023 μs 5.032 μs 0.99 1 1.0223 0.0610 16.72 KB 0.61

🟠 Collection — 100-item List<T>

Method Mean Error StdDev Min Median Max Ratio Rank Gen0 Gen1 Allocated Alloc Ratio
Manual 16.27 μs 2.867 μs 0.157 μs 16.09 μs 16.33 μs 16.39 μs 1.00 1 5.2490 1.3123 85.99 KB 1.00
EggMapper 16.42 μs 2.854 μs 0.156 μs 16.24 μs 16.50 μs 16.52 μs 1.01 1 5.2490 1.3123 85.99 KB 1.00
AutoMapper 20.87 μs 1.616 μs 0.089 μs 20.77 μs 20.89 μs 20.94 μs 1.28 1 5.7678 1.4343 94.34 KB 1.10
Mapster 16.78 μs 0.212 μs 0.012 μs 16.77 μs 16.78 μs 16.79 μs 1.03 1 5.2490 1.3123 85.99 KB 1.00
MapperlyMap 17.97 μs 0.623 μs 0.034 μs 17.94 μs 17.95 μs 18.01 μs 1.10 1 5.2490 1.2817 86.02 KB 1.00
AgileMapper 19.73 μs 1.308 μs 0.072 μs 19.65 μs 19.76 μs 19.78 μs 1.21 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,307.479 μs 1,671.8997 μs 91.6425 μs 1,210.844 μs 1,318.452 μs 1,393.141 μs 1.003 0.09 3 3.9063 1.9531 95.29 KB 1.00
AutoMapperStartup 439.799 μs 658.3421 μs 36.0860 μs 410.643 μs 428.597 μs 480.158 μs 0.337 0.03 2 5.8594 - 104.16 KB 1.09
MapsterStartup 2.388 μs 0.2922 μs 0.0160 μs 2.377 μs 2.379 μs 2.406 μ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.286 ms 1.744 ms 0.0956 ms 1.225 ms 1.237 ms 1.396 ms 1.00 0.09 1 5.8594 3.9063 95.43 KB 1.00
AutoMapper 4.366 ms 11.433 ms 0.6267 ms 3.738 ms 4.368 ms 4.992 ms 3.41 0.47 2 15.6250 7.8125 310.51 KB 3.25
Mapster 4.081 ms 6.573 ms 0.3603 ms 3.670 ms 4.231 ms 4.342 ms 3.19 0.31 2 39.0625 15.6250 756.28 KB 7.92

📝 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