MapsterMapper / MapsterMapper/Mapster

Conditional mapping with nested adapters fails for one type but works for another identical pattern

Open
#926 9 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

configuration problem
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

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. 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

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.