fix: items propagation, DynamicInvoke wrapping, thread safety#83
Merged
github-actions[bot] merged 1 commit intomainfrom Mar 26, 2026
Merged
fix: items propagation, DynamicInvoke wrapping, thread safety#83github-actions[bot] merged 1 commit intomainfrom
github-actions[bot] merged 1 commit intomainfrom
Conversation
…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>
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 | 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) 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.
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 toMap<S,D>(source)which dropped theitemsdictionary. Resolvers that depend onctx.Itemswould 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 rawTargetInvocationException— no type context, confusing stack trace.After: Wraps in
MappingExceptionwith 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:
GetOrAddwith factory delegate ensures only one thread compiles per type pair.Test plan
🤖 Generated with Claude Code