Conditional mapping with nested adapters fails for one type but works for another identical pattern
还没有人认领这个 Issue。
评估
调研方向
从提供的 InquiryResponseMapper.Register 配置开始,然后通过 TypeAdapterConfig.Compile() 和 Mapper 为两个 OneOf 分支重现映射。将 PlaintextCard 和 EncryptedCard 的条件映射与其已注册的嵌套适配器进行比较;完成标准是 PlaintextCard 通过其适配器完成填充,同时不破坏正常工作的 EncryptedCard 路径。
由索引模型根据 Issue 内容生成。
描述
Conditional mapping with nested adapters fails for one type but works for another identical pattern
Summary
When using conditional mapping (3-parameter .Map() overload) with OneOf types, Mapster successfully applies nested adapters for EncryptedCard but fails to apply them for PlaintextCard, even though both use the exact same mapping pattern. The PlaintextCard property is set to null instead of being mapped through the registered adapter.
Environment
- Mapster Version: 7.4.0
- Mapster.Core Version: 1.2.1
- MapsterMapper Version: 1.0.2
- .NET Version: .NET 9.0
- Project Type: ASP.NET Core Web API with gRPC
Minimal Reproduction
Source Types
// OneOf discriminated union
[GenerateOneOf]
public partial class CardOf : OneOfBase<CardWithPlainTextPan, CardWithEncryptedPan>;
public record CardWithPlainTextPan(string Pan, string ExpiryYear, string ExpiryMonth);
public record CardWithEncryptedPan(
string EncryptedCardData,
string ExpiryYear,
string ExpiryMonth);
public record AccountUpdaterInquiryResponse(InquiryStatus Status, Maybe<CardOf> Card);
Destination Types (Protobuf Generated)
// From protobuf - simplified
public sealed partial class InquiryResponse
{
public Processing.Card.V2.Card PlaintextCard { get; set; }
public Processing.Card.V2.EncryptedCard EncryptedCard { get; set; }
public InquiryStatus Status { get; set; }
public enum AccountOneofCase { None, PlaintextCard, EncryptedCard }
}
public sealed partial class Card
{
public string Pan { get; set; }
public string ExpiryYear { get; set; }
public string ExpiryMonth { get; set; }
public string Cvc { get; set; } // Optional field
}
public sealed partial class EncryptedCard
{
public string JweCompactEncryptedData { get; set; }
public string ExpiryYear { get; set; }
public string ExpiryMonth { get; set; }
public EncryptedCardContentType EncryptedDataContentType { get; set; }
}
Mapper Configuration
public class InquiryResponseMapper : IRegister
{
public void Register(TypeAdapterConfig config)
{
// Nested adapter for PlaintextCard
config.NewConfig<CardWithPlainTextPan, Processing.Card.V2.Card>()
.Map(d => d.Pan, s => s.Pan)
.Map(d => d.ExpiryMonth, s => s.ExpiryMonth)
.Map(d => d.ExpiryYear, s => s.ExpiryYear)
.Ignore(d => d.Cvc);
// Nested adapter for EncryptedCard
config.NewConfig<CardWithEncryptedPan, Processing.Card.V2.EncryptedCard>()
.Map(d => d.JweCompactEncryptedData, s => s.EncryptedCardData)
.Map(d => d.EncryptedDataContentType,
s => Processing.Card.V2.EncryptedCardContentType.JsonCardNumberWithOptCvvCamelCase)
.Map(d => d.ExpiryYear, s => s.ExpiryYear)
.Map(d => d.ExpiryMonth, s => s.ExpiryMonth);
// Main mapping with conditional logic
config.NewConfig<AccountUpdaterInquiryResponse, InquiryResponse>()
.Map(d => d.PlaintextCard, s => s.Card.Value.AsT0, s => s.Card.HasValue && s.Card.Value.IsT0)
.Map(d => d.EncryptedCard, s => s.Card.Value.AsT1, s => s.Card.HasValue && s.Card.Value.IsT1)
.Map(d => d.Status, s => ConvertInquiryStatus(s.Status));
}
}
Test Configuration
var config = new TypeAdapterConfig();
config.Default.RequireDestinationMemberSource(value: true);
new InquiryResponseMapper().Register(config);
config.Compile();
var mapper = new Mapper(config);
Expected Behavior
Both PlaintextCard and EncryptedCard should be mapped through their respective nested adapters:
- When
Card.Value.IsT0is true,PlaintextCardshould be populated viaCardWithPlainTextPan -> Processing.Card.V2.Cardadapter - When
Card.Value.IsT1is true,EncryptedCardshould be populated viaCardWithEncryptedPan -> Processing.Card.V2.EncryptedCardadapter
Actual Behavior
- ✅
EncryptedCardworks correctly - nested adapter is applied, all fields mapped - ❌
PlaintextCardisnull- nested adapter is NOT applied, even though the condition evaluates totrue
Test Results
Passed: 18 tests for EncryptedCard mapping
Failed: 8 tests for PlaintextCard mapping (NullReferenceException on line accessing PlaintextCard.Pan)
What We've Tried
- Reordering configurations - Registering PlaintextCard adapter before EncryptedCard
- Different mapping approaches:
- Ternary operator:
.Map(d => d.PlaintextCard, s => s.Card.HasValue && s.Card.Value.IsT0 ? s.Card.Value.AsT0 : null) - AfterMapping callbacks
- Manual adapter invocation functions
- Ternary operator:
- Property mapping variations:
- Changed property order to match constructor
- Explicit mapping of Cvc to empty string instead of Ignore
- Type disambiguation - Added
using Card = Processing.Card.V2.Card;alias - ForType declarations - Explicitly declaring both adapters with
config.ForType<>()
None of these approaches resolved the issue, while EncryptedCard continues to work perfectly with the exact same pattern.
Key Observations
- The problem is asymmetric - identical mapping patterns behave differently
- The destination type name
Cardis very generic, whileEncryptedCardis more specific - Source types use OneOf discriminated unions from the OneOf NuGet package
- Destination types are protobuf-generated with oneof fields
- The nested adapter IS registered and CAN be used directly, but isn't invoked through conditional mapping
RequireDestinationMemberSource(true)is satisfied (no compilation errors about unmapped members)
Workaround Needed
Is there a recommended pattern for mapping through OneOf types with nested adapters that ensures consistent behavior? Or is this a bug that needs fixing?
Additional Context
This appears to be related to how Mapster resolves and applies nested adapters when the source value comes from a conditional expression involving discriminated union types. The fact that one type works and another doesn't suggests there may be type resolution or caching issues specific to certain type names or structures.
- 主要语言
- C#
- 星标
- 5.2k
- 派生
- 411
- 平均合并
- 3 天 4 小时
- 30 天内合并 PR
- 3
环境准备
- 没有 Dockerfile 或 Docker Compose 文件
- 没有 Pull Request 模板
- 阅读贡献指南
从这里开始
- 先读完整个 Issue,再读项目的贡献指南。
- 在 Issue 下留言说明你要接手 —— 这能避免两个人做同样的事。
- Fork 仓库,在一个分支上完成修改。
- 提交 Pull Request,并在描述里引用这个 Issue 编号。
MapsterMapper/Mapster 的其他 Issue
-
.NET Matrix未关闭
难度 2/5 1-3 小时 新手友好度 72/100
MapsterMapper/Mapster#1003 ·
-
难度 3/5 1-2 天 新手友好度 58/100
MapsterMapper/Mapster#1024 · 1 条评论 ·
-
[Mapster Tool] Add support for global:: to disambiguate type names in generated mappers可能已有人在做 @DocSvartz 于 3 天前认领。 未关闭improvement
难度 3/5 1-2 天 新手友好度 65/100
MapsterMapper/Mapster#1020 · 5 条评论 ·
-
Mapster.Tool - PropertySettingBuilder.Map(member, mapFunc, targetPropertyName) method is not working as expected!可能已有人在做 关联的 PR 仍在进行中或已合并。 未关闭
难度 3/5 1-2 天 新手友好度 68/100
MapsterMapper/Mapster#1017 · 6 条评论 ·
-
难度 3/5 1-2 天 新手友好度 57/100
MapsterMapper/Mapster#1007 · 5 条评论 ·
查看 MapsterMapper/Mapster 的全部 Issue
相似的 Issue
-
[Bug]: Revit selector reads process ids 2000-2099 as years (flaky AgentClientTests.AYearOrAProcessIdSelectsTheHost)可能已有人在做 @Wurschdhaud 今天认领。 未关闭
难度 2/5 1-3 小时 新手友好度 83/100
pyrevitlabs/pyRevit#3817 ·
维护者通常 1 天内回复
-
bug effort:S P2
难度 2/5 1-3 小时 新手友好度 76/100
nightscout/nocturne#2449 ·
维护者通常 1 天内回复
-
audit:1 bug
难度 2/5 1-3 小时 新手友好度 76/100
NethermindEth/dotnet-libp2p#395 ·
维护者通常 1 天内回复
-
area: client bug
难度 2/5 1-3 小时 新手友好度 72/100
维护者通常 1 天内回复
-
bug mcp misleading small
难度 2/5 1-3 小时 新手友好度 70/100
维护者通常 1 天内回复