Skip to content

10.0.7: IgnoreNullValues(true) breaks Include mappings to read-only interfaces #927

@luczito

Description

@luczito

I recently upgraded my codebase from Mapster 9.0.0-pre01 to 10.0.7.

After the upgrade, mappings from an interface DTO to a read-only domain interface fail during config.Compile() when the global configuration has:

config.Default.IgnoreNullValues(true);

The same setup worked in both version 7.4.0 and 9.0.0-pre01.

Repro:

public interface IDomain
{
    string Id { get; }

    string Value { get; }

    IList<string> ValueList { get; }
}

public class DomainDerived : IDomain
{
    public string Id { get; set; }

    public string Value { get; set; }

    public IList<string> ValueList { get; set; }

    public DomainDerived()
    {
    }

    public DomainDerived(
        string id,
        string value,
        IList<string> valueList)
    {
        Id = id;
        Value = value;
        ValueList = valueList;
    }
}

public interface IDto
{
    string Id { get; set; }

    string Value { get; set; }

    IList<string> ValueList { get; set; }
}

public class DtoDerived : IDto
{
    public string Id { get; set; }

    public string Value { get; set; }

    public IList<string> ValueList { get; set; }
}

Configuration:

public static TypeAdapterConfig ConfigureMapster(MappingAssemblyProvider mappingAssemblyProvider)
    {
        var config = TypeAdapterConfig.GlobalSettings;
        config.Default.PreserveReference(true);
        config.Default.EnumMappingStrategy(EnumMappingStrategy.ByName);
        config.Default.IgnoreNullValues(true);
        config.Default.EnableNonPublicMembers(true);
        config.Default.MapToConstructor(true);
        config.Default.ShallowCopyForSameType(false);
        config.AllowImplicitSourceInheritance = true;
        config.RequireDestinationMemberSource = true;
        foreach (var assembly in mappingAssemblyProvider.GetMapperAssemblies())
        {
            config.Scan(assembly);
        }

        config.Compile();

        return config;
    }

Test:

[TestFixture]
public class RecordMappingTests
{
    [SetUp]
    public void SetUp()
    {
        MappingConfiguration.ConfigureMapster(new MappingAssemblyProvider());
    }

    [Test]
    public void DtoBase_Maps_To_DomainBase()
    {
        var dto = new DtoDerived
        {
            Id = "id",
            Value = "test",
            ValueList = new List<string> { "value1", "value2" }
        };

        var domain = dto.Adapt<IDto, IDomain>();

        Assert.That(domain.Id, Is.EqualTo(dto.Id));
        Assert.That(domain.Value, Is.EqualTo(dto.Value));
        Assert.That(domain.ValueList, Is.EquivalentTo(dto.ValueList));
    }
}

Throws the following error on compile:

Mapster.CompileException : Error while compiling
source=Dto.IDto
destination=Domain.IDomain
type=Map
  ----> System.ArgumentException : Expression must be writeable (Parameter 'left')

I can make a workaround by disabling 'IgnoreNullValues(true)' from the global config. But this introduced several other issues in my codebase (see issue #928). I have also tried to add '.Ignore()' on the base mapping config, but this seems to be auto-inherited to the derived configs, and therefore also breaks the mapping.

Notes

This appears to be related to the interaction between:
IgnoreNullValues(true)
read-only destination interface members
Include<TDerivedSource, TDerivedDestination>()
polymorphic mapping from base interface to base interface

The read-only interface itself cannot be assigned to directly, but the included destination implementation has public setters. I would expect the included mapping to be used for the runtime derived type rather than generating assignments against the read-only base interface members.

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions