From 8d61e4324cc5c39f5768ca8265e4fe5d13c53c2e Mon Sep 17 00:00:00 2001 From: wuyangfan Date: Mon, 25 May 2026 14:01:50 +0800 Subject: [PATCH 01/15] test: ensure Compile skips open generic mapping rules (#925) Add regression coverage for configuring ClassA<> to ClassB<> and calling Compile() without throwing, matching the documented validation behavior. Co-authored-by: Cursor --- .../WhenMappingWithOpenGenerics.cs | 27 +++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/src/Mapster.Tests/WhenMappingWithOpenGenerics.cs b/src/Mapster.Tests/WhenMappingWithOpenGenerics.cs index 7abb5e1f..118f1c75 100644 --- a/src/Mapster.Tests/WhenMappingWithOpenGenerics.cs +++ b/src/Mapster.Tests/WhenMappingWithOpenGenerics.cs @@ -33,6 +33,23 @@ public void Setting_From_OpenGeneric_Has_No_SideEffect() var cCopy = c.Adapt(config); } + /// + /// https://github.com/MapsterMapper/Mapster/issues/925 + /// + [TestMethod] + public void Compile_With_Open_Generic_Mapping_Does_Not_Throw() + { + var config = new TypeAdapterConfig(); + config.ForType(typeof(ClassA<>), typeof(ClassB<>)); + + Should.NotThrow(() => config.Compile()); + + var classA = new ClassA { Variable = 15 }; + var classB = classA.Adapt>(config); + + classB.Variable.ShouldBe(15); + } + [TestMethod] public void MapOpenGenericsUseInherits() { @@ -102,5 +119,15 @@ class A { public string AProperty { get; set; } } class B { public string BProperty { get; set; } } class C { public string BProperty { get; set; } } + + class ClassA + { + public T? Variable { get; set; } + } + + class ClassB + { + public T? Variable { get; set; } + } } } From a0aee31392aa010116334d5988334fca21582170 Mon Sep 17 00:00:00 2001 From: wuyangfan Date: Mon, 25 May 2026 14:37:05 +0800 Subject: [PATCH 02/15] fix: guard nullable navigation in ProjectToType record ctor mapping (#898) When a nullable navigation maps to a record DTO constructor parameter, skip nested mapping when the source is null instead of evaluating record ctor args against a null navigation. Also track NullChecks by expression identity so nested projection parameters are not skipped solely because they share a nullable type with a parent getter. Co-authored-by: Cursor --- .../WhenAddCtorNullablePropagation.cs | 25 +++++++++++++------ src/Mapster/Adapters/BaseClassAdapter.cs | 9 ++++--- src/Mapster/Utils/ExpressionEx.cs | 10 ++++---- 3 files changed, 28 insertions(+), 16 deletions(-) diff --git a/src/Mapster.Tests/WhenAddCtorNullablePropagation.cs b/src/Mapster.Tests/WhenAddCtorNullablePropagation.cs index b13eefa6..d321f971 100644 --- a/src/Mapster.Tests/WhenAddCtorNullablePropagation.cs +++ b/src/Mapster.Tests/WhenAddCtorNullablePropagation.cs @@ -1,5 +1,5 @@ -using Mapster.Tests.Classes; -using Microsoft.VisualStudio.TestTools.UnitTesting; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Shouldly; using System.Collections.Generic; using System.Linq; @@ -15,14 +15,25 @@ public class WhenAddCtorNullablePropagation [TestMethod] public void NullablePropagationFromCtorWorking() { - var source = new List(); + var source = new List + { + new() { Id = 1, Cod = new OrderCodEntity898 { Value = 42L } }, + new() { Id = 2, Cod = null }, + }; - source.Add(new OrderEntity898() { Id = 1, Cod = new OrderCodEntity898 { Value = 42L } }); - source.Add(new OrderEntity898() { Id = 2, Cod = null }); - - var str = new OrderEntity898() { Id = 1, Cod = new OrderCodEntity898 { Value = 42L } }.BuildAdapter().CreateProjectionExpression(); + Should.NotThrow(() => + { + source.AsQueryable().BuildAdapter().CreateProjectionExpression(); + }); var result = source.AsQueryable().ProjectToType().ToList(); + + result.Count.ShouldBe(2); + result[0].Id.ShouldBe(1); + result[0].Cod.ShouldNotBeNull(); + result[0].Cod!.Value.ShouldBe(42L); + result[1].Id.ShouldBe(2); + result[1].Cod.ShouldBeNull(); } } diff --git a/src/Mapster/Adapters/BaseClassAdapter.cs b/src/Mapster/Adapters/BaseClassAdapter.cs index 6d12a934..140f8b18 100644 --- a/src/Mapster/Adapters/BaseClassAdapter.cs +++ b/src/Mapster/Adapters/BaseClassAdapter.cs @@ -218,9 +218,9 @@ protected Expression CreateInstantiationExpression(Expression source, ClassMappi var members = classConverter.Members; var arguments = new List(); + arg.Context.NullChecks.UnionWith(members.Where(x => x.Getter != null).Select(x => (x.Getter, arg))); foreach (var member in members) { - arg.Context.NullChecks.UnionWith(members.Where(x=>x.Getter != null).Select(x=>(x.Getter,arg))); var parameterInfo = (ParameterInfo)member.DestinationMember.Info!; Expression defaultConst; Expression getter; @@ -253,12 +253,13 @@ protected Expression CreateInstantiationExpression(Expression source, ClassMappi else { - if (member.Getter.CanBeNull() && member.DestinationMember.Type.IsAbstractOrNotPublicCtor() - && member.Ignore.Condition == null) + if (member.Getter.CanBeNull() && member.Ignore.Condition == null + && (member.DestinationMember.Type.IsAbstractOrNotPublicCtor() + || member.DestinationMember.Type.UnwrapNullable().IsRecordType())) { var compareNull = Expression.Equal(member.Getter, Expression.Constant(null, member.Getter.Type)); getter = Expression.Condition(ExpressionEx.Not(compareNull), - CreateAdaptExpression(member.Getter, member.DestinationMember.Type, arg, member), + CreateAdaptExpressionCore(member.Getter, member.DestinationMember.Type, arg, member), defaultConst); } else diff --git a/src/Mapster/Utils/ExpressionEx.cs b/src/Mapster/Utils/ExpressionEx.cs index b7ffc365..bffdefde 100644 --- a/src/Mapster/Utils/ExpressionEx.cs +++ b/src/Mapster/Utils/ExpressionEx.cs @@ -453,8 +453,8 @@ public static Expression ApplyNullPropagationFromCtor(this Expression getter, Ex Expression? condition = null; var current = getter; var checks = arg.Context.NullChecks - .Where(x=> !object.ReferenceEquals(x.arg,arg)) - .Select(x=>x.param?.Type); + .Where(x => !object.ReferenceEquals(x.arg, arg)) + .Select(x => x.param); while (current != null) { @@ -462,9 +462,9 @@ public static Expression ApplyNullPropagationFromCtor(this Expression getter, Ex if (current.CanBeNull() && current is not ParameterExpression) compareNull = Expression.NotEqual(current, Expression.Constant(null, current.Type)); - else if (current.CanBeNull() && current is ParameterExpression - && !checks.Contains(current.Type)) - compareNull = Expression.NotEqual(current, Expression.Constant(null, current.Type)); + else if (current.CanBeNull() && current is ParameterExpression param + && !checks.Contains(param)) + compareNull = Expression.NotEqual(param, Expression.Constant(null, param.Type)); if (compareNull != null) { From 8b42d4a150f09d3c5beb406092d030ed080348ba Mon Sep 17 00:00:00 2001 From: wuyangfan Date: Mon, 25 May 2026 16:07:27 +0800 Subject: [PATCH 03/15] fix: cast inherited MapWith results to derived destinations (#947) When AllowImplicitDestinationInheritance merges a base MapWith converter into a derived destination mapping, downcast the converter result so Adapt() no longer throws InvalidCastException. Co-authored-by: Cursor --- ...citInheritanceMapWithDerivedDestination.cs | 75 +++++++++++++++++++ src/Mapster/TypeAdapterConfig.cs | 18 ++++- 2 files changed, 92 insertions(+), 1 deletion(-) create mode 100644 src/Mapster.Tests/WhenImplicitInheritanceMapWithDerivedDestination.cs diff --git a/src/Mapster.Tests/WhenImplicitInheritanceMapWithDerivedDestination.cs b/src/Mapster.Tests/WhenImplicitInheritanceMapWithDerivedDestination.cs new file mode 100644 index 00000000..0bb500c9 --- /dev/null +++ b/src/Mapster.Tests/WhenImplicitInheritanceMapWithDerivedDestination.cs @@ -0,0 +1,75 @@ +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Shouldly; + +namespace Mapster.Tests +{ + /// + /// https://github.com/MapsterMapper/Mapster/issues/947 + /// + [TestClass] + public class WhenImplicitInheritanceMapWithDerivedDestination + { + [TestCleanup] + public void Cleanup() + { + TypeAdapterConfig.GlobalSettings.Clear(); + TypeAdapterConfig.GlobalSettings.AllowImplicitDestinationInheritance = false; + } + + [TestMethod] + public void Inherited_MapWith_On_Base_Destination_Casts_To_Derived_Destination() + { + var config = new TypeAdapterConfig(); + config.AllowImplicitDestinationInheritance = true; + config.NewConfig() + .MapWith(src => src.Type == "Bird" + ? (Animal947)new Bird947 { AnimalValue = src.AnimalValueDto } + : new Dog947 { AnimalValue = src.AnimalValueDto }); + + var source = new AnimalDto947 { AnimalValueDto = "Hello", Type = "Dog" }; + + var dog = source.Adapt(config); + + dog.ShouldBeOfType(); + dog.AnimalValue.ShouldBe("Hello"); + } + + [TestMethod] + public void Inherited_MapWith_Works_For_Explicit_Source_Destination_Pair() + { + var config = new TypeAdapterConfig(); + config.AllowImplicitDestinationInheritance = true; + config.NewConfig() + .MapWith(src => src.Type == "Bird" + ? (Animal947)new Bird947 { AnimalValue = src.AnimalValueDto } + : new Dog947 { AnimalValue = src.AnimalValueDto }); + + var source = new AnimalDto947 { AnimalValueDto = "Hello", Type = "Dog" }; + + var dog = source.Adapt(config); + + dog.ShouldBeOfType(); + dog.AnimalValue.ShouldBe("Hello"); + } + + public abstract class Animal947 + { + public string AnimalValue { get; set; } = null!; + } + + public class Dog947 : Animal947 + { + } + + public class Bird947 : Animal947 + { + } + + public class AnimalDto947 + { + public string AnimalValueDto { get; set; } = null!; + + public string Type { get; set; } = null!; + } + } +} diff --git a/src/Mapster/TypeAdapterConfig.cs b/src/Mapster/TypeAdapterConfig.cs index 1dce2053..af88d5bd 100644 --- a/src/Mapster/TypeAdapterConfig.cs +++ b/src/Mapster/TypeAdapterConfig.cs @@ -431,7 +431,7 @@ private static LambdaExpression CreateMapExpression(CompileArgument arg) throw new CompileException(arg, new InvalidOperationException("ConverterFactory is not found")); try { - return fn(arg); + return AdjustInheritedConverterReturnType(fn(arg), arg); } catch (Exception ex) { @@ -439,6 +439,22 @@ private static LambdaExpression CreateMapExpression(CompileArgument arg) } } + private static LambdaExpression AdjustInheritedConverterReturnType(LambdaExpression lambda, CompileArgument arg) + { + var destinationType = arg.DestinationType; + var returnType = lambda.ReturnType; + if (returnType == destinationType) + return lambda; + + // MapWith configured on a base destination type returns the base type, but implicit + // destination inheritance can compile the converter for a derived destination. + if (!returnType.IsAssignableFrom(destinationType)) + return lambda; + + var body = lambda.Body.To(destinationType, force: true); + return Expression.Lambda(body, lambda.Parameters); + } + private LambdaExpression CreateDynamicMapExpression(TypeTuple tuple) { var lambda = CreateMapExpression(tuple, MapType.Map); From 98f1afa750d0393d8f11a9609200f8c5c3a72552 Mon Sep 17 00:00:00 2001 From: wuyangfan Date: Mon, 25 May 2026 16:10:58 +0800 Subject: [PATCH 04/15] fix: support CompileProjection with Include on abstract types (#801) Generate TypeIs-based conditional projection expressions when mapping abstract destination hierarchies with Include, and harden inline class mapping when instantiation cannot produce a NewExpression. Also add regression coverage for not-self-creation types inside containing classes (#911). Co-authored-by: Cursor --- .../WhenIncludeDerivedClasses.cs | 31 +++++++++++++ .../WhenMappingRecordRegression.cs | 43 +++++++++++++++++++ src/Mapster/Adapters/ClassAdapter.cs | 40 ++++++++++++++++- 3 files changed, 113 insertions(+), 1 deletion(-) diff --git a/src/Mapster.Tests/WhenIncludeDerivedClasses.cs b/src/Mapster.Tests/WhenIncludeDerivedClasses.cs index 3002766a..3af19256 100644 --- a/src/Mapster.Tests/WhenIncludeDerivedClasses.cs +++ b/src/Mapster.Tests/WhenIncludeDerivedClasses.cs @@ -40,6 +40,37 @@ public void Map_Including_Derived_Class_With_List() ((BikeDto)dto[1]).Brand.ShouldBe("BMX"); } + /// + /// https://github.com/MapsterMapper/Mapster/issues/801 + /// + [TestMethod] + public void CompileProjection_Including_Derived_Class() + { + TypeAdapterConfig.NewConfig() + .Include() + .CompileProjection(); + } + + public abstract class PocoA801 + { + public int Id { get; set; } + } + + public class PocoDerived801 : PocoA801 + { + public int DerivedVal { get; set; } + } + + public abstract class DtoA801 + { + public int Id { get; set; } + } + + public class DtoDerived801 : DtoA801 + { + public int DerivedVal { get; set; } + } + #region test classes public abstract class Vehicle { diff --git a/src/Mapster.Tests/WhenMappingRecordRegression.cs b/src/Mapster.Tests/WhenMappingRecordRegression.cs index 13ff3ea3..bcf0c96e 100644 --- a/src/Mapster.Tests/WhenMappingRecordRegression.cs +++ b/src/Mapster.Tests/WhenMappingRecordRegression.cs @@ -539,6 +539,49 @@ public void NotSelfCreationTypeMappingToSelfWithOutError() resultJ.RootElement.GetProperty("key").ToString().ShouldBe("value"); } + /// + /// https://github.com/MapsterMapper/Mapster/issues/911 + /// + [TestMethod] + public void NotSelfCreationTypeMappingInContainingClassWithoutError() + { + var jsonSource = new SourceClassWithJsonDocument911 + { + Json = JsonDocument.Parse("{\"key\": \"value\"}") + }; + + var uriSource = new SourceClassWithUri911 + { + Uri = new Uri("https://www.google.com/") + }; + + var jsonDest = jsonSource.Adapt(); + var uriDest = uriSource.Adapt(); + + jsonDest.Json.RootElement.GetProperty("key").GetString().ShouldBe("value"); + uriDest.Uri.ToString().ShouldBe("https://www.google.com/"); + } + + class SourceClassWithJsonDocument911 + { + public required JsonDocument Json { get; init; } + } + + class DestinationClassWithJsonDocument911 + { + public required JsonDocument Json { get; init; } + } + + class SourceClassWithUri911 + { + public required Uri Uri { get; init; } + } + + class DestinationClassWithUri911 + { + public required Uri Uri { get; init; } + } + /// /// https://github.com/MapsterMapper/Mapster/issues/927 /// diff --git a/src/Mapster/Adapters/ClassAdapter.cs b/src/Mapster/Adapters/ClassAdapter.cs index 27d09d9b..d71067b0 100644 --- a/src/Mapster/Adapters/ClassAdapter.cs +++ b/src/Mapster/Adapters/ClassAdapter.cs @@ -219,9 +219,17 @@ private static Expression SetValueByReflection(MemberMapping member, MemberExpre // Prop2 = convert(src.Prop2), //} + if (arg.MapType == MapType.Projection && arg.DestinationType.IsAbstract && arg.Settings.Includes.Count > 0) + return CreateIncludeProjectionExpression(source, arg); + var exp = CreateInstantiationExpression(source, arg); + if (exp.NodeType == ExpressionType.Throw) + return null; + var memberInit = exp as MemberInitExpression; - var newInstance = memberInit?.NewExpression ?? (NewExpression)exp; + var newInstance = memberInit?.NewExpression ?? exp as NewExpression; + if (newInstance == null) + return null; var contructorMembers = newInstance.GetAllMemberExpressionsMemberInfo().ToArray(); ClassModel? classModel; ClassMapping? classConverter; @@ -272,6 +280,36 @@ private static Expression SetValueByReflection(MemberMapping member, MemberExpre return Expression.MemberInit(newInstance, lines); } + static Expression CreateIncludeProjectionExpression(Expression source, CompileArgument arg) + { + Expression body = Expression.Default(arg.DestinationType); + foreach (var tuple in arg.Settings.Includes) + { + var itemTuple = tuple; + if (tuple.Source.IsOpenGenericType() && tuple.Destination.IsOpenGenericType()) + { + var genericArg = source.Type.GetGenericArguments(); + itemTuple = new TypeTuple(tuple.Source.MakeGenericType(genericArg), tuple.Destination.MakeGenericType(genericArg)); + } + + if (itemTuple.Source == arg.SourceType) + continue; + + if (!arg.SourceType.GetTypeInfo().IsAssignableFrom(itemTuple.Source.GetTypeInfo())) + continue; + + if (!arg.DestinationType.GetTypeInfo().IsAssignableFrom(itemTuple.Destination.GetTypeInfo())) + continue; + + var test = Expression.TypeIs(source, itemTuple.Source); + var cast = Expression.TypeAs(source, itemTuple.Source); + var mapped = CreateAdaptExpressionCore(cast!, itemTuple.Destination, arg); + body = Expression.Condition(test, mapped.To(arg.DestinationType, true), body); + } + + return body; + } + protected override Expression CreateExpressionBody(Expression source, Expression? destination, CompileArgument arg) { TypeAdapterRule? rule; From 49d670e9683cb9b61f5f07321e71480c14eb90fd Mon Sep 17 00:00:00 2001 From: wuyangfan Date: Mon, 25 May 2026 16:28:35 +0800 Subject: [PATCH 05/15] fix: fall back to src-relative snk when SolutionDir is unset (#848) Allow `dotnet test path/to/csproj` to resolve the shared assembly key from Directory.Build.props via MSBuildThisFileDirectory when SolutionDir is not provided by the build. --- src/Directory.Build.props | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/Directory.Build.props b/src/Directory.Build.props index 4eff7b00..a3b70db2 100644 --- a/src/Directory.Build.props +++ b/src/Directory.Build.props @@ -20,7 +20,9 @@ https://cloud.githubusercontent.com/assets/5763993/26522718/d16f3e42-4330-11e7-9b78-f8c7402624e7.png MIT true - $(SolutionDir)/Mapster/Mapster.snk + + $(SolutionDir)Mapster/Mapster.snk + $(MSBuildThisFileDirectory)Mapster/Mapster.snk true Mapper;AutoMapper;Fast;Mapping icon.png From 2c37aa1cfd9e8c3a4ea20e491d91f85f86d72ece Mon Sep 17 00:00:00 2001 From: DocSvartz Date: Mon, 15 Jun 2026 13:24:35 +0500 Subject: [PATCH 06/15] fix(test): Drop OpenGeneric compile check because a separate test was added --- src/Mapster.Tests/WhenMappingWithOpenGenerics.cs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/Mapster.Tests/WhenMappingWithOpenGenerics.cs b/src/Mapster.Tests/WhenMappingWithOpenGenerics.cs index 118f1c75..19891357 100644 --- a/src/Mapster.Tests/WhenMappingWithOpenGenerics.cs +++ b/src/Mapster.Tests/WhenMappingWithOpenGenerics.cs @@ -24,9 +24,7 @@ public void Setting_From_OpenGeneric_Has_No_SideEffect() config .NewConfig(typeof(A<>), typeof(B<>)) .Map("BProperty", "AProperty"); - - config.Compile(); // is not throw exception - + var a = new A { AProperty = "A" }; var c = new C { BProperty = "C" }; var b = a.Adapt>(config); // successful mapping From a2a25ead4f8e5b2fe01cce3d4f78630c4d6300f3 Mon Sep 17 00:00:00 2001 From: DocSvartz Date: Wed, 17 Jun 2026 12:28:27 +0500 Subject: [PATCH 07/15] feat: inherit MapWith and fix Mapping ValueType to NullableValue type using only ValueType to ValueType custom settings --- ...citInheritanceMapWithDerivedDestination.cs | 115 +++++++++++++++++- src/Mapster/TypeAdapterConfig.cs | 61 +++++++++- 2 files changed, 169 insertions(+), 7 deletions(-) diff --git a/src/Mapster.Tests/WhenImplicitInheritanceMapWithDerivedDestination.cs b/src/Mapster.Tests/WhenImplicitInheritanceMapWithDerivedDestination.cs index 0bb500c9..d459e1eb 100644 --- a/src/Mapster.Tests/WhenImplicitInheritanceMapWithDerivedDestination.cs +++ b/src/Mapster.Tests/WhenImplicitInheritanceMapWithDerivedDestination.cs @@ -1,5 +1,8 @@ +using Microsoft.CodeAnalysis.CSharp.Syntax; using Microsoft.VisualStudio.TestTools.UnitTesting; using Shouldly; +using System; +using static Mapster.Tests.DynamicTypeGeneratorTests; namespace Mapster.Tests { @@ -27,11 +30,16 @@ public void Inherited_MapWith_On_Base_Destination_Casts_To_Derived_Destination() : new Dog947 { AnimalValue = src.AnimalValueDto }); var source = new AnimalDto947 { AnimalValueDto = "Hello", Type = "Dog" }; + var sourceInsaider = new AnimalDtoInsaider947() { Animal = source }; var dog = source.Adapt(config); - + var dogInsaider = sourceInsaider.Adapt(config); + dog.ShouldBeOfType(); dog.AnimalValue.ShouldBe("Hello"); + + dogInsaider.Animal.ShouldBeOfType(); + dogInsaider.Animal.AnimalValue.ShouldBe("Hello"); } [TestMethod] @@ -52,6 +60,77 @@ public void Inherited_MapWith_Works_For_Explicit_Source_Destination_Pair() dog.AnimalValue.ShouldBe("Hello"); } + + [TestMethod] + public void Inherited_MapWith_On_Base_Destination_ReturnDefault_When_In_Runtime_ResultType_IsNot_Achievable() + { + var config = new TypeAdapterConfig(); + config.AllowImplicitDestinationInheritance = true; + config.NewConfig() + .MapWith(src => src.Type == "Bird" + ? (Animal947)new Bird947 { AnimalValue = src.AnimalValueDto } + : new Dog947 { AnimalValue = src.AnimalValueDto }); + + var source = new AnimalDto947 { AnimalValueDto = "Hello", Type = "Bird" }; + + var dog = source.Adapt(config); + + dog.ShouldBeNull(); + dog.ShouldBe(default); + } + + + [TestMethod] + public void Inherited_MapWith_On_Base_Destination_Casts_To_Derived_Destination_UsingInterface() + { + var config = new TypeAdapterConfig(); + config.AllowImplicitDestinationInheritance = true; + config.NewConfig() + .MapWith(src => src.Type == "Bird" + ? new ValueTypeBird947 { AnimalValue = src.AnimalValueDto } + : new ValueTypeDog947 { AnimalValue = src.AnimalValueDto }); + + var dog = new AnimalDto947 { AnimalValueDto = "Hello", Type = "Dog" }.Adapt(config); + var defaultdata = new AnimalDto947 { AnimalValueDto = "Hello", Type = "Bird" }.Adapt(config); + + dog.ShouldBeOfType(); + dog.AnimalValue.ShouldBe("Hello"); + + defaultdata.ShouldBeOfType(); + defaultdata.AnimalValue.ShouldBe(default); + } + + [TestMethod] + public void Inherited_MapWith_On_Base_Destination_Casts_To_Derived_Destination_NullableValueType() + { + var config = new TypeAdapterConfig(); + config.AllowImplicitDestinationInheritance = true; + config.NewConfig() + .MapWith(src => src.Type == "Bird" + ? new ValueTypeBird947 { AnimalValue = src.AnimalValueDto } + : new ValueTypeDog947 { AnimalValue = src.AnimalValueDto }); + + var validSrc = new AnimalDto947 { AnimalValueDto = "Hello", Type = "Dog" }; + var invalidSrc = new AnimalDto947 { AnimalValueDto = "Tweet", Type = "Bird" }; ; + + var dog = validSrc.Adapt(config); + var Nulldata = invalidSrc.Adapt(config); + var InsaiderNullableDog = new AnimalDtoInsaider947() { Animal = validSrc}.Adapt(config); + var NullInsaiderNullableDog = new AnimalDtoInsaider947() { Animal = invalidSrc }.Adapt(config); + + dog.ShouldNotBeNull(); + dog?.AnimalValue.ShouldBe("Hello"); + InsaiderNullableDog.Animal.ShouldNotBeNull(); + InsaiderNullableDog.Animal?.AnimalValue.ShouldBe("Hello"); + + + Nulldata.ShouldBeNull(); + NullInsaiderNullableDog.Animal.ShouldBeNull(); + } + + + #region TestClases + public abstract class Animal947 { public string AnimalValue { get; set; } = null!; @@ -65,11 +144,45 @@ public class Bird947 : Animal947 { } + public class AnimalDto947 { public string AnimalValueDto { get; set; } = null!; public string Type { get; set; } = null!; } + + public class AnimalDtoInsaider947 + { + public AnimalDto947 Animal { get; set; } + } + + public class DogInsaider947 + { + public Dog947 Animal { get; set; } + } + + public interface IAnimal + { + public string AnimalValue { get; set; } + } + + public struct ValueTypeBird947 : IAnimal + { + public string AnimalValue { get; set; } + } + + public struct ValueTypeDog947 : IAnimal + { + public string AnimalValue { get; set; } + + } + + public class DogValueTypeNullableInsaider947 + { + public ValueTypeDog947? Animal { get; set; } + } + + #endregion TestClases } } diff --git a/src/Mapster/TypeAdapterConfig.cs b/src/Mapster/TypeAdapterConfig.cs index af88d5bd..938aeab9 100644 --- a/src/Mapster/TypeAdapterConfig.cs +++ b/src/Mapster/TypeAdapterConfig.cs @@ -1,13 +1,14 @@ -using System; +using Mapster.Adapters; +using Mapster.Models; +using Mapster.Utils; +using System; using System.Collections.Concurrent; using System.Collections.Generic; +using System.Data.SqlTypes; using System.Linq; using System.Linq.Expressions; using System.Reflection; using System.Runtime.CompilerServices; -using Mapster.Adapters; -using Mapster.Models; -using Mapster.Utils; namespace Mapster { @@ -266,6 +267,10 @@ private static TypeAdapterRule CreateDestinationTypeRule(TypeTuple key) private static int? GetSubclassDistance(Type type1, Type type2, bool allowInheritance) { + //Support for using ValueType mapping configurations of types, for mapping cases on Nulllable ValueType values + if (type1.IsNullable() && !type1.ContainsGenericParameters) + type1 = type1.GetGenericArguments().FirstOrDefault(); + if (type1 == type2) return 50; @@ -443,15 +448,59 @@ private static LambdaExpression AdjustInheritedConverterReturnType(LambdaExpress { var destinationType = arg.DestinationType; var returnType = lambda.ReturnType; + var lamdaBody = lambda.Body; + + // Support for using ValueType mapping configurations of types, for mapping cases on Nulllable ValueType values + if (arg.DestinationType.IsNullable() && !returnType.IsNullable() && returnType.IsValueType) + { + lamdaBody = Expression.Convert(lambda.Body, typeof(Nullable<>).MakeGenericType(lambda.ReturnType)); + lambda = Expression.Lambda(lamdaBody, lambda.Parameters); + + returnType = lambda.ReturnType; + } + if (returnType == destinationType) return lambda; + //Support for using ValueType mapping configurations of types, for mapping cases on Nulllable ValueType values + if (destinationType.IsNullable() && lambda.ReturnType.IsInterface) + { + var realDestType = destinationType.GetGenericArguments().FirstOrDefault(); + + if (realDestType is null || !returnType.IsAssignableFrom(realDestType)) + return lambda; + } // MapWith configured on a base destination type returns the base type, but implicit // destination inheritance can compile the converter for a derived destination. - if (!returnType.IsAssignableFrom(destinationType)) + else if (!returnType.IsAssignableFrom(destinationType)) return lambda; - var body = lambda.Body.To(destinationType, force: true); + Expression body; + + if(destinationType.CanBeNull()) + body = Expression.TypeAs(lamdaBody, destinationType); + else + { + var tempDest = Expression.Variable(returnType, "tempDest"); + + var variables = new[] {tempDest}; + + var blockbody = new List() { Expression.Assign(tempDest, lamdaBody) }; + + var condition = Expression.TypeIs(tempDest, destinationType); + UnaryExpression ifTrue = Expression.Convert(tempDest, destinationType); + DefaultExpression ifFalse = Expression.Default(destinationType); + + ConditionalExpression conditionalExpr = Expression.Condition(condition, ifTrue, ifFalse); + blockbody.Add(conditionalExpr); + + BlockExpression body2 = Expression.Block(variables, blockbody); + + return Expression.Lambda(body2,lambda.Parameters); + + } + + return Expression.Lambda(body, lambda.Parameters); } From fb49423afe08c7f6946c26c140edaa0ce622a56b Mon Sep 17 00:00:00 2001 From: DocSvartz Date: Thu, 18 Jun 2026 19:02:13 +0500 Subject: [PATCH 08/15] fix: add Nullable Adapter --- src/Mapster/Adapters/BaseAdapter.cs | 23 +++++----------- src/Mapster/Adapters/NullableAdapter.cs | 36 +++++++++++++++++++++++++ src/Mapster/TypeAdapterConfig.cs | 1 + src/Mapster/Utils/ExpressionEx.cs | 19 +++---------- src/Mapster/Utils/ReflectionUtils.cs | 8 ++++++ 5 files changed, 55 insertions(+), 32 deletions(-) create mode 100644 src/Mapster/Adapters/NullableAdapter.cs diff --git a/src/Mapster/Adapters/BaseAdapter.cs b/src/Mapster/Adapters/BaseAdapter.cs index b31a0dbe..0e9c7923 100644 --- a/src/Mapster/Adapters/BaseAdapter.cs +++ b/src/Mapster/Adapters/BaseAdapter.cs @@ -485,12 +485,7 @@ protected Expression CreateAdaptExpression(Expression source, Type destinationTy } internal Expression CreateAdaptExpression(Expression source, Type destinationType, CompileArgument arg, MemberMapping? mapping, Expression? destination = null) { - Expression _source; - - if (arg.MapType != MapType.Projection) - _source = source.NullableEnumExtractor(); // Extraction Nullable Enum - else - _source = source; + Expression _source = source; if (_source.Type == destinationType && arg.MapType == MapType.Projection) return _source; @@ -506,20 +501,14 @@ internal Expression CreateAdaptExpression(Expression source, Type destinationTyp if (_source.Type == destinationType && arg.Settings.ShallowCopyForSameType == true && notUsingDestinationValue && rule == null) exp = _source; - else if (source is ConditionalExpression cond && mapping != null) - { - // convert ApplyNullable Propagation for NotPrimitive Nullable types - if (mapping.Getter.Type.IsNotPrimitiveNullableType() && !mapping.DestinationMember.Type.IsNullable()) - { - var adapt = CreateAdaptExpressionCore(cond.IfTrue.GetNotPrimitiveNullableValue(), mapping.DestinationMember.Type, arg, mapping); - exp = Expression.Condition(cond.Test, adapt, mapping.DestinationMember.Type.CreateDefault()); - } - else - exp = CreateAdaptExpressionCore(_source, destinationType, arg, mapping, destination); - } else exp = CreateAdaptExpressionCore(_source, destinationType, arg, mapping, destination); + // NullablePropagation when for member using Custom converter MapWith + if (notUsingDestinationValue && arg.MapType != MapType.Projection + && mapping != null && mapping.Getter.CanBeNull()) + exp = mapping.Getter.NotNullReturn(exp); + //transform(adapt(_source)); if (notUsingDestinationValue) { diff --git a/src/Mapster/Adapters/NullableAdapter.cs b/src/Mapster/Adapters/NullableAdapter.cs new file mode 100644 index 00000000..bfc7d836 --- /dev/null +++ b/src/Mapster/Adapters/NullableAdapter.cs @@ -0,0 +1,36 @@ +using Mapster.Utils; +using System.Linq.Expressions; + +namespace Mapster.Adapters +{ + internal class NullableAdapter : BaseAdapter + { + + protected override int Score => 0; //must do first + + protected override bool CanMap(PreCompileArgument arg) + { + return arg.SourceType.IsNullable() || arg.DestinationType.IsNullable(); + } + protected override bool CanInline(Expression source, Expression? destination, CompileArgument arg) + { + return true; + } + + protected override Expression? CreateInlineExpression(Expression source, CompileArgument arg, bool IsRequiredOnly = false) + { + var _source = source.Type.IsNullable() + ? Expression.Convert(source, source.Type.GetGenericArguments()[0]) + : source; + + Expression adapt = CreateAdaptExpression(_source, arg.DestinationType.GetNotNullableTypeDefenition(),arg); + + return adapt.ToNullableExp(arg); + } + + protected override Expression CreateBlockExpression(Expression source, Expression destination, CompileArgument arg) + { + return Expression.Empty(); + } + } +} diff --git a/src/Mapster/TypeAdapterConfig.cs b/src/Mapster/TypeAdapterConfig.cs index 938aeab9..9463072b 100644 --- a/src/Mapster/TypeAdapterConfig.cs +++ b/src/Mapster/TypeAdapterConfig.cs @@ -33,6 +33,7 @@ private static List CreateRuleTemplate() new ObjectAdapter().CreateRule(), //-111 new StringAdapter().CreateRule(), //-110 new EnumAdapter().CreateRule(), //-109 + new NullableAdapter().CreateRule(), // 0 //fallback rules new TypeAdapterRule diff --git a/src/Mapster/Utils/ExpressionEx.cs b/src/Mapster/Utils/ExpressionEx.cs index b7ffc365..df0b3058 100644 --- a/src/Mapster/Utils/ExpressionEx.cs +++ b/src/Mapster/Utils/ExpressionEx.cs @@ -537,22 +537,11 @@ internal static Expression GetNameConverterExpression(Func conve return Expression.Constant(converter); } - public static bool IsNotPrimitiveNullableType(this Type type) + public static Expression ToNullableExp(this Expression adapt, CompileArgument arg) { - return Nullable.GetUnderlyingType(type) != null && !type.IsMapsterPrimitive(); - } - - public static Expression GetNotPrimitiveNullableValue(this Expression exp) - { - if (exp.Type.IsNotPrimitiveNullableType()) - { - var getValueOrDefaultMethod = exp.Type.GetMethod("GetValueOrDefault", Type.EmptyTypes); - var getValue = Expression.Call(exp, getValueOrDefaultMethod); - - return getValue; - } - - return exp; + if (arg.DestinationType.IsNullable()) + return Expression.Convert(adapt, arg.DestinationType); + return adapt; } } diff --git a/src/Mapster/Utils/ReflectionUtils.cs b/src/Mapster/Utils/ReflectionUtils.cs index 81c3e21e..36a792fc 100644 --- a/src/Mapster/Utils/ReflectionUtils.cs +++ b/src/Mapster/Utils/ReflectionUtils.cs @@ -479,5 +479,13 @@ public static bool IsNotCustomConverterFactory(this CompileArgument arg, TypeAda return true; } + + public static Type GetNotNullableTypeDefenition(this Type inputType) + { + if (inputType.IsNullable()) + return inputType.GetGenericArguments()[0]; + + return inputType; + } } } From 221a7175d1f46f3c97ee44fbd6abcf8adcddfd2e Mon Sep 17 00:00:00 2001 From: DocSvartz Date: Mon, 22 Jun 2026 08:52:01 +0500 Subject: [PATCH 09/15] feat(test): add Issue #987 test case --- ...enPropertyNullablePropagationRegression.cs | 46 +++++++++++++++++++ 1 file changed, 46 insertions(+) diff --git a/src/Mapster.Tests/WhenPropertyNullablePropagationRegression.cs b/src/Mapster.Tests/WhenPropertyNullablePropagationRegression.cs index 1e1e27d9..61b79dbb 100644 --- a/src/Mapster.Tests/WhenPropertyNullablePropagationRegression.cs +++ b/src/Mapster.Tests/WhenPropertyNullablePropagationRegression.cs @@ -134,9 +134,55 @@ public void MapToTargetWorkCorrect() } + /// + /// https://github.com/MapsterMapper/Mapster/issues/987 + /// + [TestMethod] + public void CustomMapUsingNullableValueTypesWorkCorrect() + { + var config = new TypeAdapterConfig(); + config.ForType() + .Map(dest => dest.Value, src => src.UseSecondaryValue + ? src.SecondaryValue1 + src.SecondaryValue2 + : src.PrimaryValue); + + + var source1 = new SourceClass987 + { + UseSecondaryValue = false, + PrimaryValue = 100 + }; + var source2 = new SourceClass987 + { + UseSecondaryValue = true, + SecondaryValue1 = 10, + SecondaryValue2 = 20 + }; + + var result1 = source1.Adapt(config); + var result2 = source2.Adapt(config); + + result1.Value.ShouldBe(100); + result2.Value.ShouldBe(30); + } + } #region TestClasses + +public class SourceClass987 +{ + public bool UseSecondaryValue { get; set; } + public int? PrimaryValue { get; set; } + public int? SecondaryValue1 { get; set; } + public int? SecondaryValue2 { get; set; } +} + +public class DestinationClass987 +{ + public int Value { get; set; } +} + public enum Currency858 { Eur, From be6ee62005f45ee3831d5376214a724ff59bee8b Mon Sep 17 00:00:00 2001 From: DocSvartz Date: Mon, 22 Jun 2026 15:21:42 +0500 Subject: [PATCH 10/15] fix: drop null check --- src/Mapster/Adapters/BaseAdapter.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Mapster/Adapters/BaseAdapter.cs b/src/Mapster/Adapters/BaseAdapter.cs index 0e9c7923..58e95e86 100644 --- a/src/Mapster/Adapters/BaseAdapter.cs +++ b/src/Mapster/Adapters/BaseAdapter.cs @@ -504,10 +504,10 @@ internal Expression CreateAdaptExpression(Expression source, Type destinationTyp else exp = CreateAdaptExpressionCore(_source, destinationType, arg, mapping, destination); - // NullablePropagation when for member using Custom converter MapWith - if (notUsingDestinationValue && arg.MapType != MapType.Projection - && mapping != null && mapping.Getter.CanBeNull()) - exp = mapping.Getter.NotNullReturn(exp); + //// NullablePropagation when for member using Custom converter MapWith + //if (notUsingDestinationValue && arg.MapType != MapType.Projection + // && mapping != null && mapping.Getter.CanBeNull()) + // exp = mapping.Getter.NotNullReturn(exp); //transform(adapt(_source)); if (notUsingDestinationValue) From 2c7d600f175c5877d1bff3ce7805c305bcceefe0 Mon Sep 17 00:00:00 2001 From: DocSvartz Date: Tue, 23 Jun 2026 13:37:53 +0500 Subject: [PATCH 11/15] fix: add dependency for Mapster.EFCore TFM Net 8.0 (#988) --- src/Mapster.EFCore/Mapster.EFCore.csproj | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/Mapster.EFCore/Mapster.EFCore.csproj b/src/Mapster.EFCore/Mapster.EFCore.csproj index 7e7922d3..b73fd4bb 100644 --- a/src/Mapster.EFCore/Mapster.EFCore.csproj +++ b/src/Mapster.EFCore/Mapster.EFCore.csproj @@ -11,6 +11,8 @@ + From ea15411d230594292997fd1bcf1292234f3f587b Mon Sep 17 00:00:00 2001 From: DocSvartz Date: Tue, 23 Jun 2026 13:50:57 +0500 Subject: [PATCH 12/15] chore: Bump version to v10.0.9 --- src/Directory.Build.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Directory.Build.props b/src/Directory.Build.props index 4eff7b00..af102088 100644 --- a/src/Directory.Build.props +++ b/src/Directory.Build.props @@ -2,7 +2,7 @@ false - 10.0.8 + 10.0.9 netstandard2.0;net10.0;net9.0;net8.0 netstandard2.0;net10.0;net9.0;net8.0 net10.0;net9.0;net8.0 From c670690c40a5f1371e9adcf0c21e767e2b196ac7 Mon Sep 17 00:00:00 2001 From: DocSvartz Date: Thu, 25 Jun 2026 12:03:03 +0500 Subject: [PATCH 13/15] fix: quick fix Custom converter supporting from nullable ValueType --- .../WhenMappingNullablePrimitives.cs | 40 +++++++++++++++++++ src/Mapster/Adapters/NullableAdapter.cs | 19 ++++++++- 2 files changed, 58 insertions(+), 1 deletion(-) diff --git a/src/Mapster.Tests/WhenMappingNullablePrimitives.cs b/src/Mapster.Tests/WhenMappingNullablePrimitives.cs index 8d0fec3f..47aa080d 100644 --- a/src/Mapster.Tests/WhenMappingNullablePrimitives.cs +++ b/src/Mapster.Tests/WhenMappingNullablePrimitives.cs @@ -158,8 +158,48 @@ public void MappingNullTuple() result.Application.ShouldBeNull(); } + [TestMethod] + public void CustomConverterWorkWithNullablePrimitiveTypes() + { + TypeAdapterConfig config = new TypeAdapterConfig(); + config.NewConfig().MapWith(src => Helper992.ParseToNullableBool(src)); + config.Compile(); + + var src = new Source992 { Prop1 = "yes" }; + var result = src.Adapt(config); + + result.Prop1.ShouldBe(true); + + } + + #region TestClasses + static class Helper992 + { + public static bool? ParseToNullableBool(string? value) + { + if (string.IsNullOrWhiteSpace(value)) + return null; + + return value.Trim().ToLower() switch + { + "true" or "t" or "yes" or "1" => true, + "false" or "f" or "no" or "0" => false, + _ => null + }; + } + } + + public class Source992 + { + public string? Prop1 { get; set; } + } + + public class Destination992 + { + public bool? Prop1 { get; set; } + } public class Output414 { diff --git a/src/Mapster/Adapters/NullableAdapter.cs b/src/Mapster/Adapters/NullableAdapter.cs index bfc7d836..598a2192 100644 --- a/src/Mapster/Adapters/NullableAdapter.cs +++ b/src/Mapster/Adapters/NullableAdapter.cs @@ -1,4 +1,5 @@ -using Mapster.Utils; +using Mapster.Models; +using Mapster.Utils; using System.Linq.Expressions; namespace Mapster.Adapters @@ -19,6 +20,22 @@ protected override bool CanInline(Expression source, Expression? destination, Co protected override Expression? CreateInlineExpression(Expression source, CompileArgument arg, bool IsRequiredOnly = false) { + if (arg.ExplicitMapping) + { + LambdaExpression? Convert = null; + + TypeAdapterRule? getsettings; + arg.Context.Config.RuleMap.TryGetValue(new TypeTuple(arg.SourceType, arg.DestinationType), out getsettings); + + if (getsettings != null) + if (arg.MapType == MapType.MapToTarget) + Convert = getsettings.Settings.ConverterToTargetFactory(arg); + else + Convert = getsettings.Settings.ConverterFactory(arg); + if (Convert != null) + return Convert.Apply(arg.MapType, source); + } + var _source = source.Type.IsNullable() ? Expression.Convert(source, source.Type.GetGenericArguments()[0]) : source; From c0021f98be38f8f2768a5bdfd3a8df9a18ef0524 Mon Sep 17 00:00:00 2001 From: DocSvartz Date: Mon, 29 Jun 2026 06:46:58 +0500 Subject: [PATCH 14/15] fix: add supporting nullable property to Ignore() setter setting from Nullable context analizer (#991) --- src/Mapster/TypeAdapterSetter.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Mapster/TypeAdapterSetter.cs b/src/Mapster/TypeAdapterSetter.cs index b50933de..03c41eeb 100644 --- a/src/Mapster/TypeAdapterSetter.cs +++ b/src/Mapster/TypeAdapterSetter.cs @@ -402,7 +402,7 @@ internal TypeAdapterSetter(TypeAdapterSettings settings, TypeAdapterConfig paren : base(settings, parentConfig) { } - public TypeAdapterSetter Ignore(params Expression>[] members) + public TypeAdapterSetter Ignore(params Expression>[] members) { this.CheckCompiled(); @@ -568,12 +568,12 @@ internal TypeAdapterSetter(TypeAdapterSettings settings, TypeAdapterConfig paren #region replace for chaining - public new TypeAdapterSetter Ignore(params Expression>[] members) + public new TypeAdapterSetter Ignore(params Expression>[] members) { return (TypeAdapterSetter)base.Ignore(members); } - public TypeAdapterSetter IgnoredRemove(params Expression>[] members) + public TypeAdapterSetter IgnoredRemove(params Expression>[] members) { this.CheckCompiled(); @@ -1114,7 +1114,7 @@ public TwoWaysTypeAdapterSetter MapToConstructor(bool val return this; } - public TwoWaysTypeAdapterSetter Ignore(params Expression>[] members) + public TwoWaysTypeAdapterSetter Ignore(params Expression>[] members) { foreach (var member in members) { From 5d22318185c4a7e10e9a75fb50cd25a726635bd8 Mon Sep 17 00:00:00 2001 From: DocSvartz Date: Mon, 29 Jun 2026 06:52:54 +0500 Subject: [PATCH 15/15] chore: Bump version to v10.0.10 --- src/Directory.Build.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Directory.Build.props b/src/Directory.Build.props index af102088..93a1e67c 100644 --- a/src/Directory.Build.props +++ b/src/Directory.Build.props @@ -2,7 +2,7 @@ false - 10.0.9 + 10.0.10 netstandard2.0;net10.0;net9.0;net8.0 netstandard2.0;net10.0;net9.0;net8.0 net10.0;net9.0;net8.0