MapsterMapper / MapsterMapper/Mapster
Conditional mapping with nested adapters fails for one type but works for another identical pattern
Nobody has claimed this yet.
- Dominant language
- C#
- Stars
- 5.2k
- Forks
- 410
- Avg merge
- 2d 12h
- Merged PRs (30d)
- 6
Description
# 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
```csharp
// OneOf discriminated union
[GenerateOneOf]
public partial class CardOf : OneOfBase;
public record CardWithPlainTextPan(string Pan, string ExpiryYear, string ExpiryMonth);
public record CardWithEncryptedPan(
string EncryptedCardData,
string ExpiryYear,
string ExpiryMonth);
public record AccountUpdaterInquiryResponse(InquiryStatus Status, Maybe Card);
```
### Destination Types (Protobuf Generated)
```csharp
// 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
```csharp
public class InquiryResponseMapper : IRegister
{
public void Register(TypeAdapterConfig config)
{
// Nested adapter for PlaintextCard
config.NewConfig()
.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()
.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()
.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
```csharp
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.IsT0` is true, `PlaintextCard` should be populated via `CardWithPlainTextPan -> Processing.Card.V2.Card` adapter
- When `Card.Value.IsT1` is true, `EncryptedCard` should be populated via `CardWithEncryptedPan -> Processing.Card.V2.EncryptedCard` adapter
## Actual Behavior
- ✅ `EncryptedCard` works correctly - nested adapter is applied, all fields mapped
- ❌ `PlaintextCard` is `null` - nested adapter is NOT applied, even though the condition evaluates to `true`
### Test Results
```
Passed: 18 tests for EncryptedCard mapping
Failed: 8 tests for PlaintextCard mapping (NullReferenceException on line accessing PlaintextCard.Pan)
```
## What We've Tried
1. **Reordering configurations** - Registering PlaintextCard adapter before EncryptedCard
2. **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
3. **Property mapping variations**:
- Changed property order to match constructor
- Explicit mapping of Cvc to empty string instead of Ignore
4. **Type disambiguation** - Added `using Card = Processing.Card.V2.Card;` alias
5. **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
1. The problem is **asymmetric** - identical mapping patterns behave differently
2. The destination type name `Card` is very generic, while `EncryptedCard` is more specific
3. Source types use OneOf discriminated unions from the OneOf NuGet package
4. Destination types are protobuf-generated with oneof fields
5. The nested adapter IS registered and CAN be used directly, but isn't invoked through conditional mapping
6. `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.
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start with the supplied InquiryResponseMapper.Register configuration, then reproduce the mapping through TypeAdapterConfig.Compile() and Mapper for both OneOf branches. Compare conditional mapping of PlaintextCard and EncryptedCard with their registered nested adapters; done means PlaintextCard is populated through its adapter without breaking the working EncryptedCard path.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- csharp
- Domain
- tooling
- Issue type
- Bug
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Quiet
- Clarity
- Mostly clear
- Newbie friendliness
- 48/100