Derived types: Type descriminator support
Protobuf, when using the OneOf construct, generates an enum and a <Field>OneOfCase property, which contains one of the values indicating which of the body fields is filled. This information can be used when generating the mapper with inheritance.
Proto:
message Message
{
oneof Message
{
Ping Ping = 1;
Ack Ack = 2;
}
}
Generated class (simplified):
public sealed partial class Message : IMessage<Message>
{
public Ping { get; set; }
public Ack { get; set; }
public MessageOneofCase MessageCase { get; }
public enum MessageOneofCase
{
Ping = 1,
Ack = 2,
}
}
Mapper config:
public partial class MessageMapper
{
[MapOneOfDerivedType(nameof(@Message.MessageOneofCase.Ack), typeof(AckProtocolMessage))]
[MapOneOfDerivedType(nameof(@Message.MessageOneofCase.Ping), typeof(PingProtocolMessage))]
public partial ProtocolMessage MapToProtocolMessage(Message message);
[MapOneOfDerivedType(nameof(@Message.MessageOneofCase.Ack), typeof(AckProtocolMessage))]
[MapOneOfDerivedType(nameof(@Message.MessageOneofCase.Ping), typeof(PingProtocolMessage))]
public partial Message MapToMessage(ProtocolMessage message);
}
Generated mapper:
public partial class MessageMapper
{
public partial ProtocolMessage MapToProtocolMessage(Message message)
{
return message switch
{
AckProtocolMessage ackProtocolMessage => MapToProtocolMessage(ackProtocolMessage),
PingProtocolMessage pingProtocolMessage => MapToProtocolMessage(pingProtocolMessage),
_ => throw new ArgumentOutOfRangeException();
}
}
public partial Message MapToMessage(ProtocolMessage message)
{
return message.MessageCase switch
{
Message.MessageOneofCase.Ack => MapToAckMessage(message.Ack),
Message.MessageOneofCase.Ping => MapToPingMessage(message.Ping),
_ => throw new ArgumentOutOfRangeException();
}
}
private PingProtocolMessage MapToPingMessage(Ping ping) { ... }
private AckProtocolMessage MapToAckMessage(Ack ack) { ... }
private ProtocolMessage MapToProtocolMessage(AckProtocolMessage ackProtocolMessage) { ... }
private ProtocolMessage MapToProtocolMessage(PingProtocolMessage pingProtocolMessage) { ... }
}
I realize this may seem like a "niche" situation, but gRPC and Protobuf usage is pretty widespread, so OneOf inheritance support would be a nice addition.
Thanks for raising this point! A more protobuf-independent approach could indeed be achieved using a type discriminator for mapping derived types. This would align with how System.Text.Json handles polymorphism (see docs).
For example, the Mapperly API could look like this:
public partial class MessageMapper
{
[DerivedTypeDiscriminator(nameof(Message.MessageCase))]
[MapDerivedType<AckProtocolMessage>(Message.MessageOneofCase.Ack)]
[MapDerivedType<PingProtocolMessage>(Message.MessageOneofCase.Ping)]
public partial ProtocolMessage MapToProtocolMessage(Message message);
}
This design would not only support protobuf, but could also work seamlessly with other type discriminators, making the feature more versatile and broadly applicable.
What do you think about this approach?
Hello, this feature would be amazing. Could it see some development time soon?
this is a feature we would also like to have!
Thanks for your interest in this feature! To help us better gauge demand, please use the thumbs up (👍🏻) reaction on the original post instead of commenting or consider contributing. This keeps the thread clean and helps maintainers prioritize more effectively. Thanks!