fix: ForMember with Condition but no MapFrom silently drops property#110
Merged
cloud-hai-vo merged 1 commit intomainfrom Apr 20, 2026
Merged
Conversation
…perty BuildPropertyAction returned null when SourceMemberName was null (no explicit MapFrom), but the destination property was already marked as processed — so the convention mapping loop skipped it too, leaving the property with zero actions and never writing it. Added a convention fallback: when SourceMemberName is null, look up the source property by the destination property name and build the action with the full propMap (Condition/PreCondition/NullSubstitute) applied. Covers all ForMember-only-guards patterns that AutoMapper handles by same-name convention. Co-Authored-By: Claude Sonnet 4.6 <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 | 2.156 μs | 0.7835 μs | 0.0429 μs | 2.107 μs | 2.175 μs | 2.186 μs | 1.00 | 0.02 | 1 | 0.5264 | 0.0153 | 8.65 KB | 1.00 |
| EggMapper | 1.958 μs | 0.4181 μs | 0.0229 μs | 1.944 μs | 1.946 μs | 1.985 μs | 0.91 | 0.02 | 1 | 0.5264 | 0.0153 | 8.65 KB | 1.00 |
| AutoMapper | 2.586 μs | 0.5888 μs | 0.0323 μs | 2.549 μs | 2.602 μs | 2.608 μs | 1.20 | 0.02 | 1 | 0.6065 | 0.0191 | 9.95 KB | 1.15 |
| Mapster | 2.203 μs | 0.4159 μs | 0.0228 μs | 2.188 μs | 2.193 μs | 2.229 μs | 1.02 | 0.02 | 1 | 0.5264 | 0.0153 | 8.65 KB | 1.00 |
| MapperlyMap | 2.169 μs | 0.6345 μs | 0.0348 μs | 2.136 μs | 2.167 μs | 2.205 μs | 1.01 | 0.02 | 1 | 0.5264 | 0.0153 | 8.65 KB | 1.00 |
| AgileMapper | 2.788 μs | 0.4835 μs | 0.0265 μs | 2.765 μs | 2.782 μs | 2.817 μs | 1.29 | 0.02 | 1 | 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 | 6.041 μs | 3.2172 μs | 0.1763 μs | 5.902 μs | 5.981 μs | 6.239 μs | 1.00 | 0.04 | 1 | 1.6708 | 0.0916 | 27.4 KB | 1.00 |
| EggMapper | 6.502 μs | 1.2983 μs | 0.0712 μs | 6.429 μs | 6.506 μs | 6.571 μs | 1.08 | 0.03 | 1 | 1.6708 | 0.0916 | 27.4 KB | 1.00 |
| AutoMapper | 7.859 μs | 2.7778 μs | 0.1523 μs | 7.687 μs | 7.912 μs | 7.977 μs | 1.30 | 0.04 | 1 | 1.7548 | 0.1068 | 28.7 KB | 1.05 |
| Mapster | 6.770 μs | 1.1219 μs | 0.0615 μs | 6.706 μs | 6.776 μs | 6.829 μs | 1.12 | 0.03 | 1 | 1.6708 | 0.0916 | 27.4 KB | 1.00 |
| MapperlyMap | 5.967 μs | 0.2874 μs | 0.0158 μs | 5.949 μs | 5.976 μs | 5.977 μs | 0.99 | 0.02 | 1 | 1.6785 | 0.0992 | 27.42 KB | 1.00 |
| AgileMapper | 5.968 μs | 0.2413 μs | 0.0132 μs | 5.954 μs | 5.972 μs | 5.979 μ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 | 20.13 μs | 0.929 μs | 0.051 μs | 20.08 μs | 20.14 μs | 20.18 μs | 1.00 | 0.00 | 1 | 5.2490 | 1.3123 | 85.99 KB | 1.00 |
| EggMapper | 20.07 μs | 6.875 μs | 0.377 μs | 19.68 μs | 20.10 μs | 20.43 μs | 1.00 | 0.02 | 1 | 5.2490 | 1.3123 | 85.99 KB | 1.00 |
| AutoMapper | 24.58 μs | 18.953 μs | 1.039 μs | 23.38 μs | 25.07 μs | 25.28 μs | 1.22 | 0.04 | 1 | 5.7678 | 1.4343 | 94.34 KB | 1.10 |
| Mapster | 20.87 μs | 2.926 μs | 0.160 μs | 20.71 μs | 20.88 μs | 21.03 μs | 1.04 | 0.01 | 1 | 5.2490 | 1.3123 | 85.99 KB | 1.00 |
| MapperlyMap | 20.89 μs | 3.917 μs | 0.215 μs | 20.65 μs | 20.97 μs | 21.05 μs | 1.04 | 0.01 | 1 | 5.2490 | 1.2817 | 86.02 KB | 1.00 |
| AgileMapper | 21.99 μs | 1.622 μs | 0.089 μs | 21.89 μs | 22.04 μs | 22.05 μs | 1.09 | 0.00 | 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,391.462 μs | 1,197.0117 μs | 65.6123 μs | 1,324.765 μs | 1,393.688 μs | 1,455.933 μs | 1.001 | 0.06 | 3 | 3.9063 | 1.9531 | 95.23 KB | 1.00 |
| AutoMapperStartup | 413.470 μs | 639.0771 μs | 35.0300 μs | 374.842 μs | 422.391 μs | 443.177 μs | 0.298 | 0.03 | 2 | 5.8594 | - | 103.9 KB | 1.09 |
| MapsterStartup | 2.488 μs | 0.5001 μs | 0.0274 μs | 2.457 μs | 2.500 μs | 2.508 μ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.304 ms | 2.049 ms | 0.1123 ms | 1.180 ms | 1.336 ms | 1.398 ms | 1.01 | 0.11 | 1 | 5.8594 | - | 95.58 KB | 1.00 |
| AutoMapper | 4.152 ms | 11.417 ms | 0.6258 ms | 3.578 ms | 4.059 ms | 4.819 ms | 3.20 | 0.49 | 2 | 15.6250 | 7.8125 | 310.26 KB | 3.25 |
| Mapster | 4.361 ms | 5.436 ms | 0.2980 ms | 4.027 ms | 4.460 ms | 4.598 ms | 3.36 | 0.33 | 2 | 46.8750 | 15.6250 | 769.01 KB | 8.05 |
📝 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.
Summary
.ForMember(d => d.Name, opt => opt.Condition(...))without an explicitMapFromsilently dropped the property instead of mapping it by convention.BuildPropertyActionreturnednullwhenSourceMemberNamewasnull, but the destination property was already added toprocessedDestProps— so the convention mapping loop also skipped it, leaving zero actions for that property.BuildPropertyActionbefore the finalreturn null. WhenSourceMemberNameis null, EggMapper now looks up the source property by destination property name and builds the action with the fullpropMap(Condition/PreCondition/NullSubstitute) applied — matching AutoMapper's behavior.Impact
Any
ForMembercall that configures only guards (Condition,PreCondition,NullSubstitute, or combinations) without an explicitMapFromwas silently a no-op. With this fix those properties are correctly mapped with the guards applied.Test plan
Condition_without_MapFrom_still_maps_by_convention— property is written when condition is trueCondition_without_MapFrom_skips_when_false— existing destination value preserved when condition is falseCondition_without_MapFrom_maps_into_existing_destination— regression case:Map(source, destination)path🤖 Generated with Claude Code