Mapster icon indicating copy to clipboard operation
Mapster copied to clipboard

Constructor mapping does not work properly for abstract parameters

Open ghost opened this issue 3 months ago • 7 comments

Hello,

I have an issue with Mapster when using the MapToConstructor functionality.

If I have a ctor parameter that has an abstract type, and that parameter is null I get the following exception:

System.InvalidOperationException : Cannot instantiate type: AbstractDomainTestClass

I think Mapster should be able to handle null values in ctors for abstract types.

The following code showcases the issue:

using Mapster;
using MapsterMapper;
using NUnit.Framework;

public class MapsterCtorTests
{
    private Mapper _mapper;

    [SetUp]
    public void Setup()
    {
        _mapper = new Mapper();

        _mapper.Config.Default.MapToConstructor(true);

        _mapper.Config.NewConfig<AbstractDtoTestClass, AbstractDomainTestClass>()
            .Include<DerivedDtoTestClass, DerivedDomainTestClass>();
    }

    [Test(Description = "This works as expected")]
    public void Dto_To_Domain_MapsCorrectly()
    {
        var dtoDerived = new DerivedDtoTestClass
        {
            DerivedProperty = "DerivedValue",
            AbstractProperty = "AbstractValue"
        };

        var dto = new DtoTestClass
        {
            AbstractType = dtoDerived
        };

        var domain = dto.Adapt<DomainTestClass>();

        Assert.Multiple(() =>
        {
            Assert.That(domain.AbstractType, Is.Not.Null, "Abstract type not mapped correctly");
            Assert.That(domain.AbstractType, Is.TypeOf<DerivedDomainTestClass>(), "Abstract type not mapped correctly");

            var domainDerived = (DerivedDomainTestClass)domain.AbstractType;
            Assert.That(domainDerived.DerivedProperty, Is.EqualTo(dtoDerived.DerivedProperty), "Derived property not mapped correctly");
            Assert.That(domainDerived.AbstractProperty, Is.EqualTo(dtoDerived.AbstractProperty), "Abstract property not mapped correctly");
        });
    }

    [Test(Description = "This does not work, Mapster throws a InvalidOperationException with the following message: Cannot instantiate type: AbstractDomainTestClass")]
    public void Dto_To_Domain_AbstractClassNull_MapsCorrectly()
    {
        var dto = new DtoTestClass
        {
            AbstractType = null
        };

        var domain = dto.Adapt<DomainTestClass>();

        Assert.That(domain.AbstractType, Is.Null, "Abstract type not mapped correctly");
    }

    #region Immutable classes with private setters, map via ctors
    private abstract class AbstractDomainTestClass
    {
        public string AbstractProperty { get; private set; }

        protected AbstractDomainTestClass(string abstractProperty)
        {
            AbstractProperty = abstractProperty;
        }
    }

    private class DerivedDomainTestClass : AbstractDomainTestClass
    {
        public string DerivedProperty { get; private set; }

        /// <inheritdoc />
        public DerivedDomainTestClass(string abstractProperty, string derivedProperty)
            : base(abstractProperty)
        {
            DerivedProperty = derivedProperty;
        }
    }

    private class DomainTestClass
    {
        public AbstractDomainTestClass AbstractType { get; private set; }

        public DomainTestClass(
            AbstractDomainTestClass abstractType)
        {
            AbstractType = abstractType;
        }
    }
    #endregion

    #region DTO classes
    private abstract class AbstractDtoTestClass
    {
        public string AbstractProperty { get; set; }
    }

    private class DerivedDtoTestClass : AbstractDtoTestClass
    {
        public string DerivedProperty { get; set; }
    }

    private class DtoTestClass
    {
        public AbstractDtoTestClass AbstractType { get; set; }
    }
    #endregion
}

ghost avatar Sep 29 '25 10:09 ghost

Hello @runekeylane Actually, it's most likely two problems:

  1. Creating an instance of a class that doesn't have a public constructor. (Exception: Cannot instantiate type: AbstractDomainTestClass)
private class AbstractDomainTestClass // not abstract
{
    public string AbstractProperty { get; private set; }

    protected AbstractDomainTestClass(string abstractProperty)
    {
        AbstractProperty = abstractProperty;
    }
}
  1. Creating an instance of an abstract class (Exception: Can't compile a NewExpression with a constructor declared on an abstract class)
private abstract class AbstractDomainTestClass
{
    public string AbstractProperty { get; private set; }

    public AbstractDomainTestClass(string abstractProperty) // abstract with public ctor
    {
        AbstractProperty = abstractProperty;
    }
}

At the moment you can try the following:

TypeAdapterConfig<DtoTestClass, DomainTestClass>.NewConfig()
   .IgnoreIf((src, dest) => src.AbstractType == null, dest => dest.AbstractType);

DocSvartz avatar Sep 29 '25 13:09 DocSvartz

Hi @DocSvartz, thank you for the quick reply.

Your solution works, but for a large complex domain model where a lot of types have abstract ctor parameters that can be null, this is not a feasible workaround.

Do you see any problems in updating the Mapster code to ignore instantiation of destination ctor parameters if source property is null?

Either if IgnoreNullValues is set to true or just for abstract destination parameters in general?

ghost avatar Sep 30 '25 08:09 ghost

@runekeylane are you using version 7.4.0?

Yes, there will actually be a problem there: double initialization (a property and a ctor parameter with the same name). But I think I've solved it.

If the parameter is not optional, it cannot be ignored. In any case, will need to set a default value for the destination parameter Type.

DocSvartz avatar Sep 30 '25 09:09 DocSvartz

Yes it is Mapster 7.4.0, is there any version were abstract destination ctor parameters can be null?

ghost avatar Sep 30 '25 09:09 ghost

I checked, it doesn't work in the already released versions.

DocSvartz avatar Sep 30 '25 10:09 DocSvartz

Alright, thanks for the update and the PR with the fix, its much appreciated.

Is the anywhere I can see when 7.5.0 is scheduled for release and if this fix will be in it?

ghost avatar Sep 30 '25 10:09 ghost

You can try latest prerelease 9.0.0-pre01 this fix already included there.

DocSvartz avatar Oct 01 '25 02:10 DocSvartz