Skip to content

fix: AutoMapper compat — null collections, null elements, opts timing, flattening, error wrapping#73

Merged
cloud-hai-vo merged 1 commit intomainfrom
fix/automapper-compat-gaps
Mar 25, 2026
Merged

fix: AutoMapper compat — null collections, null elements, opts timing, flattening, error wrapping#73
cloud-hai-vo merged 1 commit intomainfrom
fix/automapper-compat-gaps

Conversation

@cloud-hai-vo
Copy link
Copy Markdown
Contributor

Closes #67, #68, #69, #70, #71

Summary

Deep review vs AutoMapper found 5 behavioral gaps. This PR fixes all of them:

1. Null collections → empty by default (#67 — Critical)

  • AutoMapper maps null source collections to [] by default; EggMapper was leaving them null
  • Now all 3 compilation paths (typed, ctx-free, flexible) emit empty collections when source is null
  • Handles List<T>, T[], HashSet<T>, and other collection types

2. Null elements in MapList fast paths (#68 — High)

  • FastCache per-item loop and TryBuildCtxFreeListDelegate inlined loop now null-check elements
  • Previously: NRE on null elements after cache warm-up (invisible in basic testing)

3. opts.Items timing and BeforeMap order (#69 — High)

  • opts callback now evaluated BEFORE mapping (was: after)
  • Items dictionary added to ResolutionContext, threaded through MapInternal
  • AfterMap fires after mapping with correct destination values

4. Multi-level flattening — unlimited depth (#70 — High)

  • dest.AddressCityNamesrc.Address.City.Name now works at any depth
  • Recursive TryGetFlattenedPropertyChain with null guards at each reference-type level
  • HasFlattenedSource also recursive for proper ctx-free path bail-out

5. ctx-free path error wrapping (#71 — Medium)

  • MapSlow now wraps exceptions in MappingException (was: raw NRE/InvalidCastException)
  • Re-throws MappingException and MappingValidationException as-is

Test plan

  • 333 unit tests pass on net8.0, net9.0, net10.0
  • All analyzer, generator, class-mapper tests pass
  • 12 new tests covering all 5 fixes + multi-level nested object mapping
  • Verify in consumer app

🤖 Generated with Claude Code

… timing, flattening, error wrapping

Closes #67, #68, #69, #70, #71

1. Null collections → empty by default (#67 CRITICAL)
   - All 3 paths (typed, ctx-free, flexible) now emit empty collections
     when source collection is null — matches AutoMapper's AllowNullCollections=false
   - Handles List<T>, T[], HashSet<T>, and other collection types
   - BuildEmptyCollectionExpr + CreateEmptyCollection helpers

2. Null elements in MapList fast paths (#68 HIGH)
   - Per-item FastCache loop null-checks elements before invoking delegate
   - TryBuildCtxFreeListDelegate inlined loop body adds null element guard
     with continue-label pattern

3. opts.Items timing and BeforeMap order (#69 HIGH)
   - opts callback now evaluated BEFORE mapping (was: after)
   - Items dictionary added to ResolutionContext, threaded through MapInternal
   - MapWithItems helper for typed Map<S,D> with items
   - AfterMap fires after mapping with correct destination values

4. Multi-level flattening — unlimited depth (#70 HIGH)
   - TryGetFlattenedPropertyChain: recursive PascalCase decomposition
     e.g., AddressCityName → src.Address.City.Name
   - TryBuildTypedFlattenedAssign: builds nested null guards for each
     reference-type intermediate in the chain
   - TryBuildFlattenedAction: runtime getter chain with null-check loop
   - HasFlattenedSource: recursive detection for ctx-free path bail-out

5. ctx-free path error wrapping (#71 MEDIUM)
   - MapSlow wraps all exceptions in MappingException (was: raw NRE)
   - Re-throws MappingException and MappingValidationException as-is

12 new tests covering all 5 fixes + multi-level nested object mapping.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@cloud-hai-vo cloud-hai-vo enabled auto-merge (squash) March 25, 2026 05:54
@cloud-hai-vo cloud-hai-vo merged commit b79ff59 into main Mar 25, 2026
5 checks passed
@cloud-hai-vo cloud-hai-vo deleted the fix/automapper-compat-gaps branch March 25, 2026 05:55
@github-actions
Copy link
Copy Markdown
Contributor

📊 Benchmark Results

Generated: 2026-03-25 06:00 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.64 ns 2.808 ns 0.154 ns 14.47 ns 14.67 ns 14.78 ns 1.00 0.01 1 0.0048 80 B 1.00
EggMapper 29.90 ns 0.439 ns 0.024 ns 29.88 ns 29.90 ns 29.93 ns 2.04 0.02 2 0.0048 80 B 1.00
AutoMapper 81.62 ns 5.521 ns 0.303 ns 81.31 ns 81.64 ns 81.91 ns 5.58 0.05 3 0.0048 80 B 1.00
Mapster 28.20 ns 5.548 ns 0.304 ns 27.87 ns 28.26 ns 28.47 ns 1.93 0.03 2 0.0048 80 B 1.00
MapperlyMap 15.22 ns 5.761 ns 0.316 ns 14.86 ns 15.39 ns 15.41 ns 1.04 0.02 1 0.0048 80 B 1.00
AgileMapper 506.75 ns 21.787 ns 1.194 ns 505.39 ns 507.26 ns 507.61 ns 34.61 0.32 4 0.0200 344 B 4.30
EggMapperGenerator 15.82 ns 1.970 ns 0.108 ns 15.70 ns 15.85 ns 15.91 ns 1.08 0.01 1 0.0048 80 B 1.00
EggMapperClassMapper 15.81 ns 15.858 ns 0.869 ns 14.81 ns 16.22 ns 16.40 ns 1.08 0.05 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 19.29 ns 2.225 ns 0.122 ns 19.19 ns 19.26 ns 19.42 ns 1.00 0.01 1 0.0048 80 B 1.00
EggMap 29.81 ns 8.962 ns 0.491 ns 29.47 ns 29.57 ns 30.37 ns 1.55 0.02 3 0.0048 80 B 1.00
AutoMapper 98.08 ns 2.281 ns 0.125 ns 97.94 ns 98.14 ns 98.17 ns 5.08 0.03 5 0.0048 80 B 1.00
Mapster 35.83 ns 3.521 ns 0.193 ns 35.62 ns 35.88 ns 36.00 ns 1.86 0.01 4 0.0048 80 B 1.00
MapperlyMap 23.46 ns 4.661 ns 0.256 ns 23.20 ns 23.46 ns 23.72 ns 1.22 0.01 2 0.0062 104 B 1.30
AgileMapper 497.55 ns 42.083 ns 2.307 ns 495.01 ns 498.11 ns 499.52 ns 25.79 0.17 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 53.26 ns 9.471 ns 0.519 ns 52.66 ns 53.50 ns 53.62 ns 1.00 0.01 1 0.0162 272 B 1.00
EggMapper 64.34 ns 16.656 ns 0.913 ns 63.33 ns 64.57 ns 65.11 ns 1.21 0.02 2 0.0162 272 B 1.00
AutoMapper 120.57 ns 11.040 ns 0.605 ns 120.13 ns 120.32 ns 121.26 ns 2.26 0.02 3 0.0162 272 B 1.00
Mapster 67.17 ns 8.258 ns 0.453 ns 66.73 ns 67.14 ns 67.63 ns 1.26 0.01 2 0.0162 272 B 1.00
MapperlyMap 50.50 ns 25.790 ns 1.414 ns 48.88 ns 51.10 ns 51.50 ns 0.95 0.02 1 0.0162 272 B 1.00
AgileMapper 503.98 ns 29.585 ns 1.622 ns 502.88 ns 503.21 ns 505.84 ns 9.46 0.08 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 69.57 ns 15.22 ns 0.835 ns 69.03 ns 69.15 ns 70.53 ns 1.00 0.01 1 0.0191 320 B 1.00
EggMapper 92.09 ns 14.39 ns 0.789 ns 91.22 ns 92.26 ns 92.77 ns 1.32 0.02 2 0.0191 320 B 1.00
AutoMapper 155.41 ns 11.48 ns 0.630 ns 154.92 ns 155.20 ns 156.12 ns 2.23 0.02 3 0.0196 328 B 1.02
Mapster 89.10 ns 16.19 ns 0.887 ns 88.24 ns 89.06 ns 90.01 ns 1.28 0.02 2 0.0191 320 B 1.00
MapperlyMap 71.39 ns 14.68 ns 0.804 ns 70.46 ns 71.81 ns 71.90 ns 1.03 0.01 1 0.0191 320 B 1.00
AgileMapper 589.98 ns 57.17 ns 3.133 ns 586.37 ns 591.72 ns 591.87 ns 8.48 0.10 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.784 μs 0.8510 μs 0.0466 μs 1.731 μs 1.802 μs 1.819 μs 1.00 0.03 1 0.5283 0.0172 8.65 KB 1.00
EggMapper 1.879 μs 0.5304 μs 0.0291 μs 1.848 μs 1.882 μs 1.906 μs 1.05 0.03 1 0.5283 0.0172 8.65 KB 1.00
AutoMapper 2.506 μs 1.2681 μs 0.0695 μs 2.430 μs 2.521 μs 2.567 μs 1.41 0.05 2 0.6065 0.0191 9.95 KB 1.15
Mapster 1.786 μs 0.4204 μs 0.0230 μs 1.763 μs 1.785 μs 1.809 μs 1.00 0.03 1 0.5283 0.0172 8.65 KB 1.00
MapperlyMap 1.813 μs 0.3873 μs 0.0212 μs 1.789 μs 1.821 μs 1.830 μs 1.02 0.03 1 0.5283 0.0172 8.65 KB 1.00
AgileMapper 2.509 μs 0.6194 μs 0.0339 μs 2.471 μs 2.521 μs 2.536 μs 1.41 0.04 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.308 μs 2.0588 μs 0.1128 μs 5.214 μs 5.277 μs 5.433 μs 1.00 0.03 1 1.6708 0.0916 27.4 KB 1.00
EggMapper 5.781 μs 1.1465 μs 0.0628 μs 5.733 μs 5.758 μs 5.852 μs 1.09 0.02 1 1.6708 0.0916 27.4 KB 1.00
AutoMapper 6.558 μs 1.2242 μs 0.0671 μs 6.482 μs 6.583 μs 6.608 μs 1.24 0.03 1 1.7548 0.1068 28.7 KB 1.05
Mapster 5.876 μs 0.8543 μs 0.0468 μs 5.823 μs 5.894 μs 5.911 μs 1.11 0.02 1 1.6708 0.0916 27.4 KB 1.00
MapperlyMap 5.388 μs 0.8693 μs 0.0476 μs 5.336 μs 5.400 μs 5.429 μs 1.02 0.02 1 1.6785 0.0992 27.42 KB 1.00
AgileMapper 5.270 μs 0.9710 μs 0.0532 μs 5.238 μs 5.240 μs 5.331 μs 0.99 0.02 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 16.63 μs 1.199 μs 0.066 μs 16.57 μs 16.61 μs 16.70 μs 1.00 0.00 1 5.2490 1.3123 85.99 KB 1.00
EggMapper 17.26 μs 4.511 μs 0.247 μs 16.98 μs 17.35 μs 17.45 μs 1.04 0.01 1 5.2490 1.3123 85.99 KB 1.00
AutoMapper 21.47 μs 2.401 μs 0.132 μs 21.35 μs 21.44 μs 21.61 μs 1.29 0.01 1 5.7678 1.4343 94.34 KB 1.10
Mapster 17.45 μs 7.002 μs 0.384 μs 17.06 μs 17.47 μs 17.83 μs 1.05 0.02 1 5.2490 1.3123 85.99 KB 1.00
MapperlyMap 18.88 μs 3.748 μs 0.205 μs 18.74 μs 18.79 μs 19.12 μs 1.14 0.01 1 5.2490 1.2817 86.02 KB 1.00
AgileMapper 21.15 μs 5.401 μs 0.296 μs 20.93 μs 21.03 μs 21.49 μs 1.27 0.02 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,279.838 μs 1,691.345 μs 92.7083 μs 1,215.840 μs 1,237.521 μs 1,386.154 μs 1.003 0.09 3 3.9063 1.9531 94.35 KB 1.00
AutoMapperStartup 368.826 μs 1,163.069 μs 63.7518 μs 320.223 μs 345.246 μs 441.008 μs 0.289 0.05 2 5.8594 - 103.92 KB 1.10
MapsterStartup 2.595 μs 2.173 μs 0.1191 μs 2.477 μs 2.594 μs 2.715 μ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.328 ms 1.740 ms 0.0954 ms 1.244 ms 1.310 ms 1.432 ms 1.00 0.09 1 5.8594 - 95.55 KB 1.00
AutoMapper 4.068 ms 9.166 ms 0.5024 ms 3.684 ms 3.883 ms 4.636 ms 3.07 0.38 2 15.6250 7.8125 310.37 KB 3.25
Mapster 4.396 ms 12.883 ms 0.7062 ms 3.630 ms 4.537 ms 5.021 ms 3.32 0.51 2 39.0625 15.6250 754.07 KB 7.89

📝 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.

fix: null source collections should map to empty collections by default

1 participant