Polymorphic mapping with collections maps only base class properties
This might be related to #776. I have a class hierarchy and a base class DTO that combines all possible properties. When mapping a single item cast to the base class - derived properties are mapped, however, when mapping a collection of the base class items - derived properties are ignored. This doesn't seem like a consistent behavior to me and now it must be fixed with explicit map config.
var items = new List<Base>
{
new DerivedOne
{
Id = Guid.NewGuid(),
ExtraProperty = "A",
Property = "a",
},
new DerivedTwo
{
Id = Guid.NewGuid(),
Property = "b",
OtherExtraProperty = "B",
}
};
// TypeAdapterConfig<Base, BaseDto>.NewConfig()
// .Include<DerivedOne, BaseDto>()
// .Include<DerivedTwo, BaseDto>();
// Here ExtraProperty and OtherExtraProperty are not mapped unless the config above is present
var mappedResultOne= b.Adapt<BaseDto[]>();
// Here OtherExtraProperty is mapped.
var mappedResultTwo = b[1].Adapt<BaseDto>();
public abstract class Base
{
public Guid Id { get; set; }
public required string Property { get; set; }
public abstract string Type { get; protected set; }
}
public class DerivedOne : Base
{
public required string ExtraProperty { get; set; }
public override string Type { get; protected set; } = "DerivedOne";
}
public class DerivedTwo : Base
{
public required string OtherExtraProperty { get; set; }
public override string Type { get; protected set; } = "DerivedTwo";
}
public record BaseDto
{
public Guid Id { get; init; }
public required string Property { get; init; }
public string? ExtraProperty { get; init; }
public string? OtherExtraProperty { get; init; }
public required string Type { get; init; }
}
@neistow Yes, it is related. This is again the difference in type definition based on Generic declaration variable and actual runtime type .
For now I resorted to adding config explicitly, but I think replacing _mapper.Map<BaseDto[]> call with collection.Select(x => _mapper.Map<BaseDto>(x)).ToArray() would work, since non collection mapping works as expected.
If using .Include() makes it work, then I think that will also be easy to fix.
But this will most likely be a separate setting something like .PolymorphicCollection(true) or PolymorphicMapping(true). Since like Include this will activate the evaluation of each element of the collection.
Introducing options like .PolymorphicCollection(true) or PolymorphicMapping(true) seems counterintuitive to me, since regular non-collection mapping works as expected.
Changing the current behavior of regular non-collection mapping may break existing production codebases.
As a library user, I expect the library to handle such kind of mapping by default.
It took me some time to figure out that the problem was in .Map<BaseDto[]>, since I debugged and tested with regular .Map<BaseDto> because the collection I was mapping was pretty large.
Look, in your case you get exactly what you asked for.
Collection This is itself a Generic type List<T>.
By declaring a collection of a certain type List<Base> -
you declarate: "I do not expect more behavior from its elements than class Base can provide".
listBase.Adapt<List<baseDto>> where item of listBase have type is Base because listBase.GetType() == List<Base>
It's equivalent to if you said:
I want to get a collection of BaseDto elements from the collection of Base elements.
or for each element of listBase map using item.Adapt<Base,BaseDto>() And you get what you ask for.
This source.Map<BaseDto> is equal to object.Map<BaseDto>().
It's equivalent to if you said: "I don’t know what this is, make me an instance of the BaseDto class from this".
Mapster analyzes types, not the runtime state of variables in the general case.
This is the main problem ))
.PolymorphicCollection(true) or PolymorphicMapping(true).
These will be special options that will need to be activated when really needed. Because it will be the slowest mapping mode.
As a library user, I expect the library to handle such kind of mapping by default.
If you convert to json using JsonNet
var json = JsonSerializer.Serialize(items);
You will find that you get a json representation of the collection of items of type Base. So the current behavior is consistent with the basic scenario of processing a collection of items.
This option should work too.
items.Cast<object>().ToArray().Adapt<BaseDto[]>();
@neistow In your case, do all the derived of the Base class exist in the same assembly as the Base class?
@neistow In your case, do all the derived of the Base class exist in the same assembly as the Base class?
Yes, they are all in the same assembly
Hi, I'm trying a similar thing... I do have this issue but my intention is to have a collection of Base object with instances of each specific type... here I have an example with an old project but using Autompper instead
CreateMap<FieldDto, Field>()
.ForMember(dest => dest.Name,
opt => opt.MapFrom(src => src.Name.Trim()))
.IncludeAllDerived();
CreateMap<TextField, TextFieldDto>().ReverseMap();
CreateMap<NumericField, NumericFieldDto>().ReverseMap();
CreateMap<BooleanField, BooleanFieldDto>().ReverseMap();
CreateMap<EmailField, EmailFieldDto>().ReverseMap();
CreateMap<PhoneNumberField, PhoneNumberFieldDto>().ReverseMap();
CreateMap<DateField, DateFieldDto>().ReverseMap();
CreateMap<SelectField, SelectFieldDto>().ReverseMap();
CreateMap<SelectFieldOption, SelectFieldOptionDto>().ReverseMap();
CreateMap<FileField, FileFieldDto>().ReverseMap();
and my generic method returns a collection of FieldDtos BUT with instance of all types...
Translating this to Mapster it would be something like:
public class TransactionsMappingConfig : IRegister
{
public void Register(TypeAdapterConfig config)
{
// Omitting all types for explanation
config.NewConfig<FieldDto, Field>()
.Include<TextField, TextFieldDto>()
.Include<NumericField, NumericFieldDto>();
config.NewConfig<TextField, TextFieldDto>();
config.NewConfig<NumericField, NumericFieldDto>();
}
}
Using Mapster I always get a collection of FieldDtos, always base clase...
The issue happens regardless the mapping approach
- _mapper.Map<List<FieldDto>>(myDomainCollection);
- myDomainCollection.Adapt<List<FieldDto>>();
@icalderazzo You can specify the FieldDto and Field types specification.
Maybe you made a mistake, this config shouldn't work at all
config.NewConfig<FieldDto, Field>() .Include<TextField, TextFieldDto>() .Include<NumericField, NumericFieldDto>();
Or you should map List<FieldDto> to List<Field>()
Using the original example it definitely works.
TypeAdapterConfig<Base, BaseDto>.NewConfig()
.Include<DerivedOne, BaseDto>()
.Include<DerivedTwo, BaseDtoTwo>();
public record BaseDtoTwo : BaseDto
{
public string Two { get; set; }
}
var result = items.Adapt<List<BaseDto>>();
result[1].ShouldBeOfType<BaseDtoTwo>(); // it work