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
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions