From dce29b6461f6c0e2a046f6d013270f5a8a396282 Mon Sep 17 00:00:00 2001 From: wuyangfan Date: Mon, 25 May 2026 14:03:13 +0800 Subject: [PATCH] fix: skip abstract base mapping when nested source is null (#928) When a nullable abstract/base property is mapped with Include<> configured, Mapster attempted to adapt through the abstract destination type even when the source value was null, throwing "Cannot instantiate type" at runtime. Short-circuit null sources before adapting to abstract or non-public-ctor destination types in CreateAdaptExpression. Co-authored-by: Cursor --- .../WhenMappingNullableAbstractWithInclude.cs | 78 +++++++++++++++++++ src/Mapster/Adapters/BaseAdapter.cs | 7 ++ 2 files changed, 85 insertions(+) create mode 100644 src/Mapster.Tests/WhenMappingNullableAbstractWithInclude.cs diff --git a/src/Mapster.Tests/WhenMappingNullableAbstractWithInclude.cs b/src/Mapster.Tests/WhenMappingNullableAbstractWithInclude.cs new file mode 100644 index 00000000..8cbef55f --- /dev/null +++ b/src/Mapster.Tests/WhenMappingNullableAbstractWithInclude.cs @@ -0,0 +1,78 @@ +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Shouldly; + +namespace Mapster.Tests +{ + [TestClass] + public class WhenMappingNullableAbstractWithInclude + { + [TestMethod] + public void Null_Abstract_Property_With_Include_Maps_To_Null() + { + var config = new TypeAdapterConfig(); + config.Default.MapToConstructor(true); + config + .NewConfig() + .Include(); + + var dto = new DtoFoo { Field = null }; + + var domain = dto.Adapt(config); + + domain.Field.ShouldBeNull(); + } + + [TestMethod] + public void Derived_Abstract_Property_With_Include_Maps_To_Derived_Domain() + { + var config = new TypeAdapterConfig(); + config.Default.MapToConstructor(true); + config + .NewConfig() + .Include(); + + var dto = new DtoFoo + { + Field = new DtoDerived { Id = "id" }, + }; + + var domain = dto.Adapt(config); + + domain.Field.ShouldNotBeNull(); + domain.Field.ShouldBeOfType(); + ((DomainDerived)domain.Field).Id.ShouldBe("id"); + } + + #region test classes + + public abstract class DtoBase + { + } + + public class DtoDerived : DtoBase + { + public string Id { get; set; } = null!; + } + + public class DtoFoo + { + public DtoBase? Field { get; set; } + } + + public abstract class DomainBase + { + } + + public class DomainDerived : DomainBase + { + public string Id { get; set; } = null!; + } + + public class DomainFoo + { + public DomainBase? Field { get; set; } + } + + #endregion + } +} diff --git a/src/Mapster/Adapters/BaseAdapter.cs b/src/Mapster/Adapters/BaseAdapter.cs index b31a0dbe..fbe69b7f 100644 --- a/src/Mapster/Adapters/BaseAdapter.cs +++ b/src/Mapster/Adapters/BaseAdapter.cs @@ -520,6 +520,13 @@ internal Expression CreateAdaptExpression(Expression source, Type destinationTyp else exp = CreateAdaptExpressionCore(_source, destinationType, arg, mapping, destination); + if (notUsingDestinationValue + && _source.CanBeNull() + && destinationType.IsAbstractOrNotPublicCtor()) + { + exp = _source.NotNullReturn(exp); + } + //transform(adapt(_source)); if (notUsingDestinationValue) {