Skip to content

Polymorphic mapping with collections maps only base class properties #793

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
neistow opened this issue Apr 15, 2025 · 11 comments
Open

Polymorphic mapping with collections maps only base class properties #793

neistow opened this issue Apr 15, 2025 · 11 comments

Comments

@neistow
Copy link

neistow commented Apr 15, 2025

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; }
}
@DocSvartz
Copy link

DocSvartz commented Apr 15, 2025

@neistow Yes, it is related.
This is again the difference in type definition based on Generic declaration variable and actual runtime type .

@neistow
Copy link
Author

neistow commented Apr 15, 2025

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.

@DocSvartz
Copy link

If using .Include() makes it work, then I think that will also be easy to fix.

@DocSvartz
Copy link

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.

@neistow
Copy link
Author

neistow commented Apr 15, 2025

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.

@DocSvartz
Copy link

DocSvartz commented Apr 15, 2025

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.

@DocSvartz
Copy link

DocSvartz commented Apr 15, 2025

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[]>();

@DocSvartz
Copy link

@neistow In your case, do all the derived of the Base class exist in the same assembly as the Base class?

@neistow
Copy link
Author

neistow commented Apr 17, 2025

@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

@icalderazzo
Copy link

icalderazzo commented Apr 20, 2025

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>(myDomainCollection);
  • myDomainCollection.Adapt<List>();

@DocSvartz
Copy link

DocSvartz commented Apr 21, 2025

@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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants