fix: AutoMapper compat — null collections, null elements, opts timing, flattening, error wrapping#73
Merged
cloud-hai-vo merged 1 commit intomainfrom Mar 25, 2026
Merged
Conversation
… 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>
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.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) 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 was referenced Mar 25, 2026
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.
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)
[]by default; EggMapper was leaving them nullList<T>,T[],HashSet<T>, and other collection types2. Null elements in MapList fast paths (#68 — High)
TryBuildCtxFreeListDelegateinlined loop now null-check elements3. opts.Items timing and BeforeMap order (#69 — High)
Itemsdictionary added toResolutionContext, threaded throughMapInternalAfterMapfires after mapping with correct destination values4. Multi-level flattening — unlimited depth (#70 — High)
dest.AddressCityName→src.Address.City.Namenow works at any depthTryGetFlattenedPropertyChainwith null guards at each reference-type levelHasFlattenedSourcealso recursive for proper ctx-free path bail-out5. ctx-free path error wrapping (#71 — Medium)
MapSlownow wraps exceptions inMappingException(was: raw NRE/InvalidCastException)MappingExceptionandMappingValidationExceptionas-isTest plan
🤖 Generated with Claude Code