MapsterMapper / Mapster

A fast, fun and stimulating object to object Mapper
MIT License
4.32k stars 329 forks source link

Can't use conditional mapping with different types of source field #673

Open GTmAster opened 8 months ago

GTmAster commented 8 months ago

For example there are 2 classes:

public class Source
{
    public int? IntValue { get; set; }
    public string StringValue { get; set; } = "Some Value";
}

public class Destination
{
    public string Mapped { get; set; } = string.Empty;
}

The task is put in Mapped string representation of IntValue if it's not null, otherwise use StringValue. I assume configuration should look like (according to Mapster docs):

TypeAdapterConfig.GlobalSettings
    .NewConfig<Source, Destination>()
    .Map(dest => dest.Mapped, src => src.IntValue, src => src.IntValue.HasValue)
    .Map(dest => dest.Mapped, src => src.StringValue);

But it gives runtime exception:

Mapster.CompileException: Error while compiling
source=Source
destination=Destination
type=Map
 ---> System.InvalidOperationException: No coercion operator is defined between types 'System.Nullable`1[System.Int32]' and 'System.String'.

Is it intended behavior or it's a bug? How to perform conditional mapping in given case Mapster way, without slapping .AfterMapping() and do it with custom code?

stagep commented 8 months ago

The boolean conditional will be evaluated at runtime so the mapping when the int has a value needs to be defined to create a string from a nullable int.

TypeAdapterConfig.GlobalSettings
    .NewConfig<Source, Destination>()
    .Map(dest => dest.Mapped, src => src.IntValue.Value.ToString(), src => src.IntValue.HasValue)
    .Map(dest => dest.Mapped, src => src.StringValue);
GTmAster commented 8 months ago

Sad it's not possible. Because in real life it's not just int, string and .ToString().... it's a big hierarchy of POCO classes with their own async mapping configs with DI.

I was thinking that this config would be roughly compiled into something like:

if (conditionFunc1(src)) {
      destExpr.SetValue(srcExpr1.GetValue().Adapt<TDest>())
      return;
}
destExpr.SetValue(srcExpr2.GetValue().Adapt<TDest>())

But seems like it's not. guess no other way that put everything into .AfterMappingAsync():

.AfterMappingAsync(async (src, dest) => {
     var mapper = MapContext.Current.GetService<IAsyncMapper>();
     if (src => src.IntValue.HasValue) 
     {
           dest.Mapped = await mapper.AdaptToTypeAsync<string>(src.IntValue);
           return;
     }
     dest.Mapped = await mapper.AdaptToTypeAsync<string>(src.StringValue);
})