From 5a4cbf39b9d80656974222a8f2b9e9564534a9bc Mon Sep 17 00:00:00 2001 From: DocSvartz Date: Wed, 18 Mar 2026 23:02:35 +0500 Subject: [PATCH 01/80] fix: restore PackageLicense --- src/Mapster/Directory.Build.props | 14 -------------- src/Mapster/Mapster.csproj | 4 +++- 2 files changed, 3 insertions(+), 15 deletions(-) delete mode 100644 src/Mapster/Directory.Build.props diff --git a/src/Mapster/Directory.Build.props b/src/Mapster/Directory.Build.props deleted file mode 100644 index 184391ef..00000000 --- a/src/Mapster/Directory.Build.props +++ /dev/null @@ -1,14 +0,0 @@ - - - true - Mapster.snk - - - true - false - - - true - false - - \ No newline at end of file diff --git a/src/Mapster/Mapster.csproj b/src/Mapster/Mapster.csproj index 86ee1e21..92fc7244 100644 --- a/src/Mapster/Mapster.csproj +++ b/src/Mapster/Mapster.csproj @@ -14,9 +14,11 @@ https://github.com/MapsterMapper/Mapster https://github.com/MapsterMapper/Mapster + true + Mapster.snk true Mapster - 10.0.0 + 10.0.1 enable 1701;1702;8618 From 6b5e46107baba5c11c91bf8250ff38570c797218 Mon Sep 17 00:00:00 2001 From: DocSvartz Date: Fri, 20 Mar 2026 15:36:29 +0500 Subject: [PATCH 02/80] fix(ProjectToType): add Test case --- src/Mapster.EFCore.Tests/EFCoreTest.cs | 31 ++++++++++++++++++++++ src/Mapster.EFCore.Tests/Models/Student.cs | 1 + 2 files changed, 32 insertions(+) diff --git a/src/Mapster.EFCore.Tests/EFCoreTest.cs b/src/Mapster.EFCore.Tests/EFCoreTest.cs index 6fba5c15..1a61eabc 100644 --- a/src/Mapster.EFCore.Tests/EFCoreTest.cs +++ b/src/Mapster.EFCore.Tests/EFCoreTest.cs @@ -117,6 +117,37 @@ public void MergeIncludeWhenUsingEFCoreProjectToType() first.Enrollments.Count.ShouldBe(1); first.LastName.ShouldBe("Alexander"); } + + /// + /// https://github.com/MapsterMapper/Mapster/issues/875 + /// + [TestMethod] + public void NotMemberNameEFCoreProjectToType_not_Error() + { + var options = new DbContextOptionsBuilder() + .UseInMemoryDatabase(Guid.NewGuid().ToString("N")) + .Options; + var context = new SchoolContext(options); + DbInitializer.Initialize(context); + + var config = new TypeAdapterConfig(); + + config + .NewConfig() + .Map(dest => dest.LastName, src => src.GetLastName()); + + Should.NotThrow(() => + { + var query = context.Students + .Include(x => x.Enrollments.OrderByDescending(x => x.StudentID).Take(1)) + .EFCoreProjectToType(config); + }); + + } + + + + } public class StudentDto diff --git a/src/Mapster.EFCore.Tests/Models/Student.cs b/src/Mapster.EFCore.Tests/Models/Student.cs index 36127fde..69a863e8 100644 --- a/src/Mapster.EFCore.Tests/Models/Student.cs +++ b/src/Mapster.EFCore.Tests/Models/Student.cs @@ -11,5 +11,6 @@ public class Student public DateTime EnrollmentDate { get; set; } public ICollection Enrollments { get; set; } + public string GetLastName () { return LastName; } } } From 5c63ddcb41e045d60076093ad9b3d06e2bbae76c Mon Sep 17 00:00:00 2001 From: DocSvartz Date: Fri, 20 Mar 2026 15:37:25 +0500 Subject: [PATCH 03/80] fix(ProjectToType): fix 10.0 regression - member name is null --- src/Mapster.Core/Utils/ProjectToTypeVisitors.cs | 6 +++--- src/Mapster.EFCore/EFCoreExtensions.cs | 2 +- src/Mapster/Adapters/BaseClassAdapter.cs | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/Mapster.Core/Utils/ProjectToTypeVisitors.cs b/src/Mapster.Core/Utils/ProjectToTypeVisitors.cs index 5ee11e7a..324effe0 100644 --- a/src/Mapster.Core/Utils/ProjectToTypeVisitors.cs +++ b/src/Mapster.Core/Utils/ProjectToTypeVisitors.cs @@ -5,7 +5,7 @@ namespace Mapster.Utils { public sealed class TopLevelMemberNameVisitor : ExpressionVisitor { - public string? MemeberName { get; private set; } + public string? MemberName { get; private set; } public override Expression Visit(Expression node) { @@ -15,8 +15,8 @@ public override Expression Visit(Expression node) { case ExpressionType.MemberAccess: { - if (string.IsNullOrEmpty(MemeberName)) - MemeberName = ((MemberExpression)node).Member.Name; + if (string.IsNullOrEmpty(MemberName)) + MemberName = ((MemberExpression)node).Member.Name; return base.Visit(node); } diff --git a/src/Mapster.EFCore/EFCoreExtensions.cs b/src/Mapster.EFCore/EFCoreExtensions.cs index 4ed597f0..d93e2f6c 100644 --- a/src/Mapster.EFCore/EFCoreExtensions.cs +++ b/src/Mapster.EFCore/EFCoreExtensions.cs @@ -82,7 +82,7 @@ public override Expression Visit(Expression node) var memberv = new TopLevelMemberNameVisitor(); memberv.Visit(item); - IncludeExpression.TryAdd(memberv.MemeberName, item); + IncludeExpression.TryAdd(memberv.MemberName, item); } } return base.Visit(node); diff --git a/src/Mapster/Adapters/BaseClassAdapter.cs b/src/Mapster/Adapters/BaseClassAdapter.cs index 4e246dc6..41208035 100644 --- a/src/Mapster/Adapters/BaseClassAdapter.cs +++ b/src/Mapster/Adapters/BaseClassAdapter.cs @@ -50,7 +50,7 @@ select fn(src, destinationMember, arg)) s.Visit(getter); - if (arg.Settings.ProjectToTypeResolvers.TryGetValue(s.MemeberName, out var match)) + if (s.MemberName != null && arg.Settings.ProjectToTypeResolvers.TryGetValue(s.MemberName, out var match)) { arg.Settings.Resolvers.Add(new InvokerModel { From b18cc86e52c7b21d35536f137f59edaec8caf6b5 Mon Sep 17 00:00:00 2001 From: DocSvartz Date: Fri, 20 Mar 2026 15:51:24 +0500 Subject: [PATCH 04/80] chore: Up mapster version to 10.0.2 --- src/Mapster/Mapster.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Mapster/Mapster.csproj b/src/Mapster/Mapster.csproj index 92fc7244..59a332c3 100644 --- a/src/Mapster/Mapster.csproj +++ b/src/Mapster/Mapster.csproj @@ -18,7 +18,7 @@ Mapster.snk true Mapster - 10.0.1 + 10.0.2 enable 1701;1702;8618 From 2b6f16f30be3785bf989253248e2c1c5fc432cbb Mon Sep 17 00:00:00 2001 From: DocSvartz Date: Fri, 20 Mar 2026 16:43:38 +0500 Subject: [PATCH 05/80] feat: Up EF6 and EF Core to v10.0.1 --- src/Mapster.EF6/Mapster.EF6.csproj | 2 +- src/Mapster.EFCore/Mapster.EFCore.csproj | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Mapster.EF6/Mapster.EF6.csproj b/src/Mapster.EF6/Mapster.EF6.csproj index 199e8e13..b4e7aa8a 100644 --- a/src/Mapster.EF6/Mapster.EF6.csproj +++ b/src/Mapster.EF6/Mapster.EF6.csproj @@ -8,7 +8,7 @@ True true Mapster.EF6.snk - 10.0.0 + 10.0.1 diff --git a/src/Mapster.EFCore/Mapster.EFCore.csproj b/src/Mapster.EFCore/Mapster.EFCore.csproj index 377fd86d..bfdf09c4 100644 --- a/src/Mapster.EFCore/Mapster.EFCore.csproj +++ b/src/Mapster.EFCore/Mapster.EFCore.csproj @@ -8,7 +8,7 @@ True true Mapster.EFCore.snk - 10.0.0 + 10.0.1 From f8395afeebdfa91e03873f04e88f7145999ab8c4 Mon Sep 17 00:00:00 2001 From: DocSvartz Date: Fri, 20 Mar 2026 17:08:44 +0500 Subject: [PATCH 06/80] fix: mapster Version and mapster core version --- src/Mapster.Core/Mapster.Core.csproj | 2 +- src/Mapster/Mapster.csproj | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Mapster.Core/Mapster.Core.csproj b/src/Mapster.Core/Mapster.Core.csproj index f57efaf1..a2baa466 100644 --- a/src/Mapster.Core/Mapster.Core.csproj +++ b/src/Mapster.Core/Mapster.Core.csproj @@ -4,7 +4,7 @@ netstandard2.0 Mapster.Core Mapster - 10.0.0 + 10.0.1 enable true true diff --git a/src/Mapster/Mapster.csproj b/src/Mapster/Mapster.csproj index 59a332c3..03cec68d 100644 --- a/src/Mapster/Mapster.csproj +++ b/src/Mapster/Mapster.csproj @@ -18,7 +18,7 @@ Mapster.snk true Mapster - 10.0.2 + 10.0.3 enable 1701;1702;8618 From bbf9b7631de2d98ca4a1308633f1e23c22147018 Mon Sep 17 00:00:00 2001 From: DocSvartz Date: Fri, 20 Mar 2026 17:57:55 +0500 Subject: [PATCH 07/80] fix: fix version number --- src/Mapster.EF6/Mapster.EF6.csproj | 2 +- src/Mapster.EFCore/Mapster.EFCore.csproj | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Mapster.EF6/Mapster.EF6.csproj b/src/Mapster.EF6/Mapster.EF6.csproj index b4e7aa8a..199e8e13 100644 --- a/src/Mapster.EF6/Mapster.EF6.csproj +++ b/src/Mapster.EF6/Mapster.EF6.csproj @@ -8,7 +8,7 @@ True true Mapster.EF6.snk - 10.0.1 + 10.0.0 diff --git a/src/Mapster.EFCore/Mapster.EFCore.csproj b/src/Mapster.EFCore/Mapster.EFCore.csproj index bfdf09c4..515bc86b 100644 --- a/src/Mapster.EFCore/Mapster.EFCore.csproj +++ b/src/Mapster.EFCore/Mapster.EFCore.csproj @@ -8,7 +8,7 @@ True true Mapster.EFCore.snk - 10.0.1 + 10.0.2 From d9ad04865bd4459467130c5f15070c3b4c8c4d3d Mon Sep 17 00:00:00 2001 From: DocSvartz Date: Sat, 21 Mar 2026 07:26:25 +0500 Subject: [PATCH 08/80] feat: Add Record Projecttion test --- src/Mapster.EFCore.Tests/EFCoreTest.cs | 34 ++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/src/Mapster.EFCore.Tests/EFCoreTest.cs b/src/Mapster.EFCore.Tests/EFCoreTest.cs index 1a61eabc..154f0120 100644 --- a/src/Mapster.EFCore.Tests/EFCoreTest.cs +++ b/src/Mapster.EFCore.Tests/EFCoreTest.cs @@ -146,8 +146,42 @@ public void NotMemberNameEFCoreProjectToType_not_Error() } + [TestMethod] + public void RecordsEFCoreProjectToType_not_Error() + { + var options = new DbContextOptionsBuilder() + .UseInMemoryDatabase(Guid.NewGuid().ToString("N")) + .Options; + var context = new SchoolContext(options); + DbInitializer.Initialize(context); + + Should.NotThrow(() => + { + var query = context.Students + .Include(x => x.Enrollments.OrderByDescending(x => x.StudentID).Take(1)) + .EFCoreProjectToType(); + + var first = query.First(); + + first.Enrollments.Count.ShouldBe(1); + first.LastName.ShouldBe("Alexander"); + }); + + } + + + + + } + + + public record StudentRecordDto + { + public int ID { get; set; } + public string LastName { get; set; } + public ICollection Enrollments { get; set; } } public class StudentDto From 6f046f5d15f339a529ae26dd42601993d41ae78c Mon Sep 17 00:00:00 2001 From: DocSvartz Date: Sat, 21 Mar 2026 07:30:22 +0500 Subject: [PATCH 09/80] fix: Quick fix for #881 ProjectToType from Record types --- src/Mapster.EFCore.Tests/EFCoreTest.cs | 8 +++----- src/Mapster/Adapters/RecordTypeAdapter.cs | 2 +- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/src/Mapster.EFCore.Tests/EFCoreTest.cs b/src/Mapster.EFCore.Tests/EFCoreTest.cs index 154f0120..54d9c82a 100644 --- a/src/Mapster.EFCore.Tests/EFCoreTest.cs +++ b/src/Mapster.EFCore.Tests/EFCoreTest.cs @@ -145,7 +145,9 @@ public void NotMemberNameEFCoreProjectToType_not_Error() } - + /// + /// https://github.com/MapsterMapper/Mapster/issues/881 + /// [TestMethod] public void RecordsEFCoreProjectToType_not_Error() { @@ -170,10 +172,6 @@ public void RecordsEFCoreProjectToType_not_Error() }); } - - - - } diff --git a/src/Mapster/Adapters/RecordTypeAdapter.cs b/src/Mapster/Adapters/RecordTypeAdapter.cs index 06a3c421..a4057111 100644 --- a/src/Mapster/Adapters/RecordTypeAdapter.cs +++ b/src/Mapster/Adapters/RecordTypeAdapter.cs @@ -19,7 +19,7 @@ internal class RecordTypeAdapter : ClassAdapter protected override bool CanMap(PreCompileArgument arg) { - return arg.DestinationType.IsRecordType(); + return arg.DestinationType.IsRecordType() && arg.MapType != MapType.Projection; } protected override bool CanInline(Expression source, Expression? destination, CompileArgument arg) From 73620c0effeeb6c86de74db80f0a4cd8cf99c797 Mon Sep 17 00:00:00 2001 From: DocSvartz Date: Sat, 21 Mar 2026 07:31:21 +0500 Subject: [PATCH 10/80] feat: Up Mapster version --- src/Mapster/Mapster.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Mapster/Mapster.csproj b/src/Mapster/Mapster.csproj index 03cec68d..80d1adb5 100644 --- a/src/Mapster/Mapster.csproj +++ b/src/Mapster/Mapster.csproj @@ -18,7 +18,7 @@ Mapster.snk true Mapster - 10.0.3 + 10.0.4-a enable 1701;1702;8618 From e9b33cf3a5b22f2cd57be94ba64bc7cfdd11ea98 Mon Sep 17 00:00:00 2001 From: DocSvartz Date: Mon, 23 Mar 2026 15:07:47 +0500 Subject: [PATCH 11/80] feat: fix #883 activate ctor param using default value --- src/Mapster/Adapters/ClassAdapter.cs | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/Mapster/Adapters/ClassAdapter.cs b/src/Mapster/Adapters/ClassAdapter.cs index 9e44105d..6d343eed 100644 --- a/src/Mapster/Adapters/ClassAdapter.cs +++ b/src/Mapster/Adapters/ClassAdapter.cs @@ -66,11 +66,16 @@ protected override Expression CreateInstantiationExpression(Expression source, E : arg.DestinationType; if (destType == null) return base.CreateInstantiationExpression(source, destination, arg); - classConverter = destType.GetConstructors() + + var constructors = destType.GetConstructors(); + classConverter = constructors .OrderByDescending(it => it.GetParameters().Length) .Select(it => GetConstructorModel(it, true)) - .Select(it => CreateClassConverter(source, it, arg, ctorMapping:true)) + .Select(it => CreateClassConverter(source, it, arg, ctorMapping: true)) .FirstOrDefault(it => it != null); + + if (classConverter == null && constructors.Length > 0) + classConverter = CreateClassConverter(source, GetConstructorModel(constructors[0], false), arg, ctorMapping: true); } else { From a60020bf1125c39bf74fad9ba97cd39702b3f721 Mon Sep 17 00:00:00 2001 From: DocSvartz Date: Mon, 23 Mar 2026 15:27:31 +0500 Subject: [PATCH 12/80] fix(test): ignore and remove not actual tests after fix #883 --- .../WhenMappingRecordRegression.cs | 53 +++++++++++++------ .../WhenUsingNonDefaultConstructor.cs | 4 ++ 2 files changed, 41 insertions(+), 16 deletions(-) diff --git a/src/Mapster.Tests/WhenMappingRecordRegression.cs b/src/Mapster.Tests/WhenMappingRecordRegression.cs index bdb9b833..c1bb9dad 100644 --- a/src/Mapster.Tests/WhenMappingRecordRegression.cs +++ b/src/Mapster.Tests/WhenMappingRecordRegression.cs @@ -2,6 +2,7 @@ using Shouldly; using System; using System.Collections.Generic; +using static Mapster.Tests.WhenExplicitMappingRequired; using static Mapster.Tests.WhenMappingDerived; namespace Mapster.Tests @@ -474,22 +475,6 @@ public void ClassCtorAutomapingWorking() result.X.ShouldBe(100); } - /// - /// https://github.com/MapsterMapper/Mapster/issues/842 - /// - [TestMethod] - public void ClassCustomCtorWitoutMapNotWorking() - { - TypeAdapterConfig.GlobalSettings.Clear(); - - var source = new TestRecord() { X = 100 }; - - Should.Throw(() => - { - source.Adapt(); - }); - } - /// /// https://github.com/MapsterMapper/Mapster/issues/842 /// @@ -537,6 +522,24 @@ public void ClassUpdateAutoPropertyWitoutSetterWorking() result.X.ShouldBe(200); } + /// + /// https://github.com/MapsterMapper/Mapster/issues/883 + /// + [TestMethod] + public void ClassCtorActivateDefaultValue() + { + var source = new Source833 + { + Value1 = "123", + }; + + Should.NotThrow(() => + { + var target = source.Adapt(); + target.Value1.ShouldBe("123"); + target.Value2.ShouldBe(default); + }); + } #region NowNotWorking @@ -565,6 +568,24 @@ public void CollectionUpdate() #region TestClasses + public class Source833 + { + public required string Value1 { get; init; } + } + + public class Target833 + { + public Target833(string value1, string value2) + { + Value1 = value1; + Value2 = value2; + } + + public string Value1 { get; } + + public string Value2 { get; } + } + public sealed record Database746( string Server = "", string Name = "", diff --git a/src/Mapster.Tests/WhenUsingNonDefaultConstructor.cs b/src/Mapster.Tests/WhenUsingNonDefaultConstructor.cs index ac41e5fc..b9ee74b8 100644 --- a/src/Mapster.Tests/WhenUsingNonDefaultConstructor.cs +++ b/src/Mapster.Tests/WhenUsingNonDefaultConstructor.cs @@ -72,6 +72,10 @@ public void Map_To_Existing_Destination_Instance_Should_Pass() dto.Unmapped.ShouldBe("unmapped"); } + /// + /// ignore after implement fix https://github.com/MapsterMapper/Mapster/issues/883 + /// + [Ignore] [TestMethod] public void Map_To_Destination_Type_Without_Default_Constructor_Shoud_Throw_Exception() { From f9bbb969f3322b694a31fc744cac911d3fadc5ee Mon Sep 17 00:00:00 2001 From: DocSvartz Date: Mon, 23 Mar 2026 15:49:39 +0500 Subject: [PATCH 13/80] feat: Adding a simple way to set member using the UseDistinationValue feature --- src/Mapster/Adapters/BaseClassAdapter.cs | 12 +++++++++++- src/Mapster/TypeAdapterSetter.cs | 20 ++++++++++++++++++++ src/Mapster/TypeAdapterSettings.cs | 5 +++++ 3 files changed, 36 insertions(+), 1 deletion(-) diff --git a/src/Mapster/Adapters/BaseClassAdapter.cs b/src/Mapster/Adapters/BaseClassAdapter.cs index 41208035..e46c287a 100644 --- a/src/Mapster/Adapters/BaseClassAdapter.cs +++ b/src/Mapster/Adapters/BaseClassAdapter.cs @@ -111,7 +111,7 @@ select fn(src, destinationMember, arg)) NextIgnore = nextIgnore, Source = (ParameterExpression)source, Destination = (ParameterExpression?)destination, - UseDestinationValue = arg.MapType != MapType.Projection && destinationMember.UseDestinationValue(arg), + UseDestinationValue = IsCanUsingDestinationValue(arg, destinationMember), }; if(getter == null && !arg.DestinationType.IsRecordType() && destinationMember.Info is PropertyInfo propinfo) @@ -189,6 +189,16 @@ select fn(src, destinationMember, arg)) }; } + protected static bool IsCanUsingDestinationValue(CompileArgument arg, IMemberModelEx destinationMember) + { + if (arg.MapType == MapType.Projection) + return false; + if (destinationMember.UseDestinationValue(arg) || arg.Settings.UseDestinationMembers.Contains(destinationMember.Name)) + return true; + + return false; + } + protected static bool ProcessIgnores( CompileArgument arg, IMemberModel destinationMember, diff --git a/src/Mapster/TypeAdapterSetter.cs b/src/Mapster/TypeAdapterSetter.cs index e08ef312..834f4dbe 100644 --- a/src/Mapster/TypeAdapterSetter.cs +++ b/src/Mapster/TypeAdapterSetter.cs @@ -510,6 +510,26 @@ public TypeAdapterSetter AfterMappingInline(Expression lambda); return this; } + + public TypeAdapterSetter UseDestinationValue(Expression> destinationMember) + { + this.CheckCompiled(); + var memberName = destinationMember.GetMemberPath()!; + + if (memberName != null) + Settings.UseDestinationMembers.Add(memberName); + + return this; + } + + public TypeAdapterSetter UseDestinationValue(string destinationMemberName) + { + this.CheckCompiled(); + Settings.UseDestinationMembers.Add(destinationMemberName); + + return this; + } + } [System.Diagnostics.CodeAnalysis.SuppressMessage("Minor Code Smell", "S4136:Method overloads should be grouped together", Justification = "")] diff --git a/src/Mapster/TypeAdapterSettings.cs b/src/Mapster/TypeAdapterSettings.cs index 15c666a2..f94172a3 100644 --- a/src/Mapster/TypeAdapterSettings.cs +++ b/src/Mapster/TypeAdapterSettings.cs @@ -185,6 +185,11 @@ public Action? Fork set => Set(nameof(Fork), value); } + public List UseDestinationMembers + { + get => Get(nameof(UseDestinationMembers), () => new List()); + } + internal bool Compiled { get; set; } public TypeAdapterSettings Clone() From 94b86dcaca5ffa08efd0d0852eddca17f075083d Mon Sep 17 00:00:00 2001 From: DocSvartz Date: Mon, 23 Mar 2026 16:17:03 +0500 Subject: [PATCH 14/80] feat(test): add test to new UseDestinatonValue config method --- ...WhenUseDestinatonValueMappingRegression.cs | 31 +++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/src/Mapster.Tests/WhenUseDestinatonValueMappingRegression.cs b/src/Mapster.Tests/WhenUseDestinatonValueMappingRegression.cs index 4200ada6..92986b30 100644 --- a/src/Mapster.Tests/WhenUseDestinatonValueMappingRegression.cs +++ b/src/Mapster.Tests/WhenUseDestinatonValueMappingRegression.cs @@ -46,8 +46,39 @@ public void UseDestinatonValueUsingMapWithasParam() channelDest.TempThumbnails.Count.ShouldBe(3); } + [TestMethod] + public void UseDestinatonValueFromSimpleConfigMethod() + { + var config = new TypeAdapterConfig(); + config.NewConfig() + .UseDestinationValue(dest => dest.Company); + + var source = new SimplySourceContractingParty() { Company = new() { CompanyName = "Mapster" } }; + + var notUseDestinatonValue = source.Adapt(); + var UseDestinatonValue = source.Adapt(config); + + notUseDestinatonValue.Company.CompanyName.ShouldBeNullOrEmpty(); + UseDestinatonValue.Company.CompanyName.ShouldBe(source.Company.CompanyName); + } #region TestClasses + + public class SimplySourceContractingParty + { + public SimplyContractingParty Company { get; set; } + } + + public class SimplyDestinationContractingParty + { + public SimplyContractingParty Company { get; } = new(); + } + + public class SimplyContractingParty + { + public string CompanyName { get; set; } + } + private static IEnumerable MapThumbnailDetailsData(ThumbnailDetailsSource thumbnailDetails) { yield return MapThumbnail(thumbnailDetails.Default, "Default"); From 307d28282973921ff51eafd964cad11b7b605556 Mon Sep 17 00:00:00 2001 From: DocSvartz Date: Mon, 23 Mar 2026 17:51:27 +0500 Subject: [PATCH 15/80] feat: add IsMapsterImmutable type helper --- src/Mapster/Utils/ReflectionUtils.cs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/Mapster/Utils/ReflectionUtils.cs b/src/Mapster/Utils/ReflectionUtils.cs index 3b9b1a1b..505166eb 100644 --- a/src/Mapster/Utils/ReflectionUtils.cs +++ b/src/Mapster/Utils/ReflectionUtils.cs @@ -448,5 +448,10 @@ public static bool isDefaultCtor(this Type type) { return type.GetConstructor(new Type[] { }) is not null ? true : false; } + + public static bool IsMapsterImmutable(this Type type) + { + return type.IsMapsterPrimitive() || type.IsRecordType(); + } } } From dfae8b60e0437b1cb29a8bc32aa7fd3147fb461b Mon Sep 17 00:00:00 2001 From: DocSvartz Date: Mon, 23 Mar 2026 17:53:54 +0500 Subject: [PATCH 16/80] fix: Restored v7.4.0 behavior from class property without setter --- src/Mapster/Adapters/ClassAdapter.cs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/Mapster/Adapters/ClassAdapter.cs b/src/Mapster/Adapters/ClassAdapter.cs index 9e44105d..8cfc9ed0 100644 --- a/src/Mapster/Adapters/ClassAdapter.cs +++ b/src/Mapster/Adapters/ClassAdapter.cs @@ -110,9 +110,11 @@ protected override Expression CreateBlockExpression(Expression source, Expressio var adapt = CreateAdaptExpression(member.Getter, member.DestinationMember.Type, arg, member, destMember); - if (!member.UseDestinationValue && member.DestinationMember.SetterModifier == AccessModifier.None) + if (member.UseDestinationValue + && member.DestinationMember.Type.IsMapsterImmutable() + && member.DestinationMember.SetterModifier == AccessModifier.None) { - if (member.DestinationMember is PropertyModel && arg.MapType == MapType.MapToTarget) + if (member.DestinationMember is PropertyModel && arg.MapType != MapType.Projection) adapt = SetValueTypeAutoPropertyByReflection(member, adapt, classModel); else continue; From 58efada7b3ec92e7247d9767646ba0ca93b3c5fa Mon Sep 17 00:00:00 2001 From: DocSvartz Date: Mon, 23 Mar 2026 18:09:50 +0500 Subject: [PATCH 17/80] feat(test): add Readonly AutoProperty Primitive type use UsingDestinationValue --- .../WhenUsingDestinationValue.cs | 21 ++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/src/Mapster.Tests/WhenUsingDestinationValue.cs b/src/Mapster.Tests/WhenUsingDestinationValue.cs index f3f7d3a8..1b66901a 100644 --- a/src/Mapster.Tests/WhenUsingDestinationValue.cs +++ b/src/Mapster.Tests/WhenUsingDestinationValue.cs @@ -47,11 +47,28 @@ public void MappingToReadonlyPropertyWhenPocoDetectRegression() student.Name.ShouldBe("John"); } + [TestMethod] + public void MappingToReadonlyAutoPropertyPrimitiveOrImmutableType() + { + var studentDto = new StudentDtoOrigin { Name = "Marta" }; + var UseDestinationValue = studentDto.Adapt(); + var NotUseDestinationValue = studentDto.Adapt(); + + UseDestinationValue.Name.ShouldBe(studentDto.Name); + NotUseDestinationValue.Name.ShouldBe("John"); // not modified Name == "John" - origin value + } + + #region TestClasses + + public class StudentOriginNoUseDestinationValue + { + public string Name { get; } = "John"; // readonly primitive type autoproperty + } public class StudentOrigin { [UseDestinationValue] - public string Name { get; } = "John"; // only readonly + public string Name { get; } = "John"; // readonly primitive type autoproperty } public class StudentDtoOrigin @@ -93,5 +110,7 @@ public class InvoiceDto public IEnumerable Numbers { get; set; } public ICollection Strings { get; set; } } + + #endregion TestClasses } } From 58bed37caa2232231acda4fd3bcb3e5d5efbb74c Mon Sep 17 00:00:00 2001 From: DocSvartz Date: Mon, 23 Mar 2026 18:13:11 +0500 Subject: [PATCH 18/80] fix(test): Correction of the test based on the behavior used in Mapster v10 --- src/Mapster.Tests/WhenUsingDestinationValue.cs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/Mapster.Tests/WhenUsingDestinationValue.cs b/src/Mapster.Tests/WhenUsingDestinationValue.cs index 1b66901a..7145cf8a 100644 --- a/src/Mapster.Tests/WhenUsingDestinationValue.cs +++ b/src/Mapster.Tests/WhenUsingDestinationValue.cs @@ -42,9 +42,11 @@ public void MapUsingDestinationValue() public void MappingToReadonlyPropertyWhenPocoDetectRegression() { var studentDto = new StudentDtoOrigin { Name = "Marta" }; - var student = studentDto.Adapt(); // No exception. - student.Name.ShouldBe("John"); + Should.NotThrow(() => + { + var student = studentDto.Adapt(); // No exception. + }); } [TestMethod] From 6e69ad83e6453f347860a7e0604c6c0224c42164 Mon Sep 17 00:00:00 2001 From: DocSvartz Date: Mon, 23 Mar 2026 20:04:54 +0500 Subject: [PATCH 19/80] fix: #881 - ProjectToType record regression --- src/Mapster/Adapters/RecordTypeAdapter.cs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/Mapster/Adapters/RecordTypeAdapter.cs b/src/Mapster/Adapters/RecordTypeAdapter.cs index a4057111..1836fe45 100644 --- a/src/Mapster/Adapters/RecordTypeAdapter.cs +++ b/src/Mapster/Adapters/RecordTypeAdapter.cs @@ -19,17 +19,19 @@ internal class RecordTypeAdapter : ClassAdapter protected override bool CanMap(PreCompileArgument arg) { - return arg.DestinationType.IsRecordType() && arg.MapType != MapType.Projection; + return arg.DestinationType.IsRecordType(); } protected override bool CanInline(Expression source, Expression? destination, CompileArgument arg) { + if(arg.MapType == MapType.Projection) + return true; return false; } protected override Expression CreateInlineExpression(Expression source, CompileArgument arg, bool IsRequiredOnly = false) { - return base.CreateInstantiationExpression(source, arg); + return CreateInstantiationExpression(source, arg); } protected override Expression CreateInstantiationExpression(Expression source, Expression? destination, CompileArgument arg) { From 0f44b7e4ab91b6184a08da0ea424080017b87eca Mon Sep 17 00:00:00 2001 From: DocSvartz Date: Mon, 23 Mar 2026 20:18:32 +0500 Subject: [PATCH 20/80] feat: Simplifying library version changes --- src/Directory.Build.props | 1 + src/ExpressionDebugger/ExpressionDebugger.csproj | 1 - src/ExpressionTranslator/ExpressionTranslator.csproj | 1 - src/Mapster.Async/Mapster.Async.csproj | 1 - src/Mapster.Core/Mapster.Core.csproj | 1 - .../Mapster.DependencyInjection.csproj | 1 - src/Mapster.EF6/Mapster.EF6.csproj | 1 - src/Mapster.EFCore/Mapster.EFCore.csproj | 1 - src/Mapster.Immutable/Mapster.Immutable.csproj | 1 - src/Mapster.JsonNet/Mapster.JsonNet.csproj | 1 - src/Mapster.Tool/Mapster.Tool.csproj | 1 - src/Mapster/Mapster.csproj | 1 - 12 files changed, 1 insertion(+), 11 deletions(-) diff --git a/src/Directory.Build.props b/src/Directory.Build.props index 151e793e..efb4ec43 100644 --- a/src/Directory.Build.props +++ b/src/Directory.Build.props @@ -2,6 +2,7 @@ false + 10.0.4 diff --git a/src/ExpressionDebugger/ExpressionDebugger.csproj b/src/ExpressionDebugger/ExpressionDebugger.csproj index 90abc2f4..f2a8baed 100644 --- a/src/ExpressionDebugger/ExpressionDebugger.csproj +++ b/src/ExpressionDebugger/ExpressionDebugger.csproj @@ -12,7 +12,6 @@ True true ExpressionDebugger.snk - 10.0.0 https://github.com/chaowlert/ExpressionDebugger/blob/master/LICENSE 8.0 enable diff --git a/src/ExpressionTranslator/ExpressionTranslator.csproj b/src/ExpressionTranslator/ExpressionTranslator.csproj index 635bb977..8c40f91f 100644 --- a/src/ExpressionTranslator/ExpressionTranslator.csproj +++ b/src/ExpressionTranslator/ExpressionTranslator.csproj @@ -12,7 +12,6 @@ True true ExpressionTranslator.snk - 10.0.0 ExpressionDebugger MIT icon.png diff --git a/src/Mapster.Async/Mapster.Async.csproj b/src/Mapster.Async/Mapster.Async.csproj index 092b90a2..ccf06f21 100644 --- a/src/Mapster.Async/Mapster.Async.csproj +++ b/src/Mapster.Async/Mapster.Async.csproj @@ -7,7 +7,6 @@ Mapster;Async true Mapster.Async.snk - 10.0.0 diff --git a/src/Mapster.Core/Mapster.Core.csproj b/src/Mapster.Core/Mapster.Core.csproj index a2baa466..10b75707 100644 --- a/src/Mapster.Core/Mapster.Core.csproj +++ b/src/Mapster.Core/Mapster.Core.csproj @@ -4,7 +4,6 @@ netstandard2.0 Mapster.Core Mapster - 10.0.1 enable true true diff --git a/src/Mapster.DependencyInjection/Mapster.DependencyInjection.csproj b/src/Mapster.DependencyInjection/Mapster.DependencyInjection.csproj index 82d46a2f..f27349f7 100644 --- a/src/Mapster.DependencyInjection/Mapster.DependencyInjection.csproj +++ b/src/Mapster.DependencyInjection/Mapster.DependencyInjection.csproj @@ -7,7 +7,6 @@ Mapster;DependencyInjection true Mapster.DependencyInjection.snk - 10.0.0 diff --git a/src/Mapster.EF6/Mapster.EF6.csproj b/src/Mapster.EF6/Mapster.EF6.csproj index 199e8e13..d6d47f29 100644 --- a/src/Mapster.EF6/Mapster.EF6.csproj +++ b/src/Mapster.EF6/Mapster.EF6.csproj @@ -8,7 +8,6 @@ True true Mapster.EF6.snk - 10.0.0 diff --git a/src/Mapster.EFCore/Mapster.EFCore.csproj b/src/Mapster.EFCore/Mapster.EFCore.csproj index 515bc86b..4bb8809f 100644 --- a/src/Mapster.EFCore/Mapster.EFCore.csproj +++ b/src/Mapster.EFCore/Mapster.EFCore.csproj @@ -8,7 +8,6 @@ True true Mapster.EFCore.snk - 10.0.2 diff --git a/src/Mapster.Immutable/Mapster.Immutable.csproj b/src/Mapster.Immutable/Mapster.Immutable.csproj index 7b791ce1..ed91137f 100644 --- a/src/Mapster.Immutable/Mapster.Immutable.csproj +++ b/src/Mapster.Immutable/Mapster.Immutable.csproj @@ -7,7 +7,6 @@ Mapster;Immutable true Mapster.Immutable.snk - 10.0.0 enable diff --git a/src/Mapster.JsonNet/Mapster.JsonNet.csproj b/src/Mapster.JsonNet/Mapster.JsonNet.csproj index be9dd3ed..3524467c 100644 --- a/src/Mapster.JsonNet/Mapster.JsonNet.csproj +++ b/src/Mapster.JsonNet/Mapster.JsonNet.csproj @@ -7,7 +7,6 @@ Mapster;Json.net true Mapster.JsonNet.snk - 10.0.0 diff --git a/src/Mapster.Tool/Mapster.Tool.csproj b/src/Mapster.Tool/Mapster.Tool.csproj index 3628258c..20e678c1 100644 --- a/src/Mapster.Tool/Mapster.Tool.csproj +++ b/src/Mapster.Tool/Mapster.Tool.csproj @@ -10,7 +10,6 @@ Mapster;Tool true Mapster.Tool.snk - 10.0.0 enable diff --git a/src/Mapster/Mapster.csproj b/src/Mapster/Mapster.csproj index 80d1adb5..b6391ed8 100644 --- a/src/Mapster/Mapster.csproj +++ b/src/Mapster/Mapster.csproj @@ -18,7 +18,6 @@ Mapster.snk true Mapster - 10.0.4-a enable 1701;1702;8618 From fd5c6b66eab3fcdbf651ce5a6e5347052e134e0b Mon Sep 17 00:00:00 2001 From: DocSvartz Date: Mon, 23 Mar 2026 20:28:14 +0500 Subject: [PATCH 21/80] fix(build): drop dublicate props: Copyright, Authors, Description in Mapster project --- src/Mapster/Mapster.csproj | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/Mapster/Mapster.csproj b/src/Mapster/Mapster.csproj index b6391ed8..e6b79780 100644 --- a/src/Mapster/Mapster.csproj +++ b/src/Mapster/Mapster.csproj @@ -1,9 +1,6 @@  - A fast, fun and stimulating object to object mapper. Kind of like AutoMapper, just simpler and way, way faster. - Copyright (c) 2016 Chaowlert Chaisrichalermpol, Eric Swann - chaowlert;eric_swann netstandard2.0;net10.0;net9.0;net8.0 Mapster A fast, fun and stimulating object to object mapper. Kind of like AutoMapper, just simpler and way, way faster. From 22b232a40013fe7a6b9dcf7b5ac95877ac793acc Mon Sep 17 00:00:00 2001 From: NLHaarP <149793688+NLHaarP@users.noreply.github.com> Date: Tue, 24 Mar 2026 15:16:52 +0100 Subject: [PATCH 22/80] fix: Typo in Data-types.md (#891) Changed "MultyConsturctor" to "MultiConstructor" --- docs/articles/mapping/Data-types.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/articles/mapping/Data-types.md b/docs/articles/mapping/Data-types.md index dcdb1182..c139ee0c 100644 --- a/docs/articles/mapping/Data-types.md +++ b/docs/articles/mapping/Data-types.md @@ -135,7 +135,7 @@ var result = source.Adapt() ``` -#### MultyConsturctor Record types +#### MultiConstructor Record types If there is more than one constructor, by default, mapping will be performed on the constructor with the largest number of parameters. @@ -189,4 +189,4 @@ var target = src.Adapt(); |:-----------------|:------:|:-----:| |[Custom constructor mapping](xref:Mapster.Settings.ConstructorMapping)| - | ✅ | |[Ignore](xref:Mapster.Settings.Custom.IgnoringMembers#ignore-extension-method)| - | ✅ | -|[IgnoreNullValues](xref:Mapster.Settings.Custom.IgnoringMembers#ignorenullvalues-extension-method)| - | ✅ | \ No newline at end of file +|[IgnoreNullValues](xref:Mapster.Settings.Custom.IgnoringMembers#ignorenullvalues-extension-method)| - | ✅ | From d3a3fe8972ff307d0ec8b49caccc363d7dcf6c06 Mon Sep 17 00:00:00 2001 From: DocSvartz Date: Wed, 25 Mar 2026 14:49:25 +0500 Subject: [PATCH 23/80] feat(build): Consolidate TFMs --- src/Directory.Build.props | 4 ++++ src/Mapster.Async/Mapster.Async.csproj | 2 +- .../Mapster.DependencyInjection.csproj | 2 +- src/Mapster.EFCore/Mapster.EFCore.csproj | 2 +- src/Mapster.Immutable/Mapster.Immutable.csproj | 2 +- src/Mapster.JsonNet/Mapster.JsonNet.csproj | 2 +- src/Mapster.Tool/Mapster.Tool.csproj | 2 +- src/Mapster/Mapster.csproj | 2 +- 8 files changed, 11 insertions(+), 7 deletions(-) diff --git a/src/Directory.Build.props b/src/Directory.Build.props index efb4ec43..ec8b8ee8 100644 --- a/src/Directory.Build.props +++ b/src/Directory.Build.props @@ -3,6 +3,10 @@ false 10.0.4 + netstandard2.0;netstandard2.1;net10.0;net9.0;net8.0 + netstandard2.0;netstandard2.1;net10.0;net9.0;net8.0 + net10.0;net9.0;net8.0 + net10.0;net9.0;net8.0 diff --git a/src/Mapster.Async/Mapster.Async.csproj b/src/Mapster.Async/Mapster.Async.csproj index ccf06f21..5ece3100 100644 --- a/src/Mapster.Async/Mapster.Async.csproj +++ b/src/Mapster.Async/Mapster.Async.csproj @@ -1,7 +1,7 @@  - netstandard2.0;net10.0;net9.0;net8.0 + $(MapsterPluginsTFMs) Async supports for Mapster true Mapster;Async diff --git a/src/Mapster.DependencyInjection/Mapster.DependencyInjection.csproj b/src/Mapster.DependencyInjection/Mapster.DependencyInjection.csproj index f27349f7..3c4f5d5a 100644 --- a/src/Mapster.DependencyInjection/Mapster.DependencyInjection.csproj +++ b/src/Mapster.DependencyInjection/Mapster.DependencyInjection.csproj @@ -1,7 +1,7 @@  - netstandard2.0;net10.0;net9.0;net8.0 + $(MapsterPluginsTFMs) Dependency Injection supports for Mapster true Mapster;DependencyInjection diff --git a/src/Mapster.EFCore/Mapster.EFCore.csproj b/src/Mapster.EFCore/Mapster.EFCore.csproj index 4bb8809f..7e7922d3 100644 --- a/src/Mapster.EFCore/Mapster.EFCore.csproj +++ b/src/Mapster.EFCore/Mapster.EFCore.csproj @@ -1,7 +1,7 @@  - net10.0;net9.0;net8.0; + $(MapsterEFCoreTFMs) EFCore plugin for Mapster true Mapster;EFCore diff --git a/src/Mapster.Immutable/Mapster.Immutable.csproj b/src/Mapster.Immutable/Mapster.Immutable.csproj index ed91137f..8887adec 100644 --- a/src/Mapster.Immutable/Mapster.Immutable.csproj +++ b/src/Mapster.Immutable/Mapster.Immutable.csproj @@ -1,7 +1,7 @@  - netstandard2.0;net10.0;net9.0;net8.0 + $(MapsterPluginsTFMs) Immutable collection supports for Mapster true Mapster;Immutable diff --git a/src/Mapster.JsonNet/Mapster.JsonNet.csproj b/src/Mapster.JsonNet/Mapster.JsonNet.csproj index 3524467c..703726ac 100644 --- a/src/Mapster.JsonNet/Mapster.JsonNet.csproj +++ b/src/Mapster.JsonNet/Mapster.JsonNet.csproj @@ -1,7 +1,7 @@  - netstandard2.0;net10.0;net9.0;net8.0 + $(MapsterPluginsTFMs) Json.net conversion supports for Mapster true Mapster;Json.net diff --git a/src/Mapster.Tool/Mapster.Tool.csproj b/src/Mapster.Tool/Mapster.Tool.csproj index 20e678c1..9d4e64a6 100644 --- a/src/Mapster.Tool/Mapster.Tool.csproj +++ b/src/Mapster.Tool/Mapster.Tool.csproj @@ -2,7 +2,7 @@ Exe - net10.0;net9.0;net8.0; + $(MapsterToolTFMs) true true dotnet-mapster diff --git a/src/Mapster/Mapster.csproj b/src/Mapster/Mapster.csproj index e6b79780..930cf61b 100644 --- a/src/Mapster/Mapster.csproj +++ b/src/Mapster/Mapster.csproj @@ -1,7 +1,7 @@  - netstandard2.0;net10.0;net9.0;net8.0 + $(MapsterTFMs) Mapster A fast, fun and stimulating object to object mapper. Kind of like AutoMapper, just simpler and way, way faster. Mapster From 3f05df6e8d3d27b2ea9614328742bd427cd8d583 Mon Sep 17 00:00:00 2001 From: DocSvartz Date: Wed, 25 Mar 2026 14:54:25 +0500 Subject: [PATCH 24/80] fix: #893 --- src/Directory.Build.props | 4 ++-- src/Mapster/Utils/CodeAnalysisAttributes.cs | 4 ++-- src/Mapster/Utils/CoreExtensions.cs | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/Directory.Build.props b/src/Directory.Build.props index ec8b8ee8..17e76a5d 100644 --- a/src/Directory.Build.props +++ b/src/Directory.Build.props @@ -3,8 +3,8 @@ false 10.0.4 - netstandard2.0;netstandard2.1;net10.0;net9.0;net8.0 - netstandard2.0;netstandard2.1;net10.0;net9.0;net8.0 + netstandard2.0;net10.0;net9.0;net8.0 + netstandard2.0;net10.0;net9.0;net8.0 net10.0;net9.0;net8.0 net10.0;net9.0;net8.0 diff --git a/src/Mapster/Utils/CodeAnalysisAttributes.cs b/src/Mapster/Utils/CodeAnalysisAttributes.cs index 212fb714..a2a4b487 100644 --- a/src/Mapster/Utils/CodeAnalysisAttributes.cs +++ b/src/Mapster/Utils/CodeAnalysisAttributes.cs @@ -4,14 +4,14 @@ namespace System.Diagnostics.CodeAnalysis { [AttributeUsage(AttributeTargets.Parameter, Inherited = false)] - public sealed class NotNullWhenAttribute : Attribute + internal sealed class NotNullWhenAttribute : Attribute { public NotNullWhenAttribute(bool returnValue) => ReturnValue = returnValue; public bool ReturnValue { get; } } [AttributeUsage(AttributeTargets.Parameter | AttributeTargets.ReturnValue, Inherited = false)] - public sealed class NotNullIfNotNullAttribute : Attribute + internal sealed class NotNullIfNotNullAttribute : Attribute { public NotNullIfNotNullAttribute(string parameterName) => ParameterName = parameterName; public string ParameterName { get; } diff --git a/src/Mapster/Utils/CoreExtensions.cs b/src/Mapster/Utils/CoreExtensions.cs index e5866402..c2799f92 100644 --- a/src/Mapster/Utils/CoreExtensions.cs +++ b/src/Mapster/Utils/CoreExtensions.cs @@ -25,7 +25,7 @@ public static void LockRemove(this List list, T item) } } -#if !NET6_0_OR_GREATER +#if NETSTANDARD2_0 public static HashSet ToHashSet(this IEnumerable source) { return new HashSet(source); From 0721ae024cfb230a00b077ef6a5362af7209a922 Mon Sep 17 00:00:00 2001 From: DocSvartz Date: Wed, 25 Mar 2026 15:37:50 +0500 Subject: [PATCH 25/80] chore: Bump Version to 10.0.5 --- 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 17e76a5d..3efb4b24 100644 --- a/src/Directory.Build.props +++ b/src/Directory.Build.props @@ -2,7 +2,7 @@ false - 10.0.4 + 10.0.5 netstandard2.0;net10.0;net9.0;net8.0 netstandard2.0;net10.0;net9.0;net8.0 net10.0;net9.0;net8.0 From 3ea2df4c9b1e0ea9566ebfa388382a9150a38fa3 Mon Sep 17 00:00:00 2001 From: DocSvartz Date: Thu, 26 Mar 2026 05:53:16 +0500 Subject: [PATCH 26/80] fix: Regression in v10 - ConstructUsing and MapToConstructor was disabled if have public parameterless constructor (#896) * fix Regression in v10 - ConstructUsing and MapToConstructor was disabled if have public parameterless constructor new() * fix using MapToConstructor for RecordType --- src/Mapster/Adapters/ClassAdapter.cs | 7 ++++--- src/Mapster/Adapters/RecordTypeAdapter.cs | 2 +- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/src/Mapster/Adapters/ClassAdapter.cs b/src/Mapster/Adapters/ClassAdapter.cs index e77fb562..78cfacc1 100644 --- a/src/Mapster/Adapters/ClassAdapter.cs +++ b/src/Mapster/Adapters/ClassAdapter.cs @@ -53,9 +53,10 @@ protected override bool CanInline(Expression source, Expression? destination, Co protected override Expression CreateInstantiationExpression(Expression source, Expression? destination, CompileArgument arg) { //new TDestination(src.Prop1, src.Prop2) - - if (arg.DestinationType.isDefaultCtor() || arg.GetConstructUsing() != null && arg.Settings.MapToConstructor == null) - return base.CreateInstantiationExpression(source, destination, arg); + + if (arg.DestinationType.isDefaultCtor() || arg.GetConstructUsing() != null) + if (arg.Settings.MapToConstructor == null) + return base.CreateInstantiationExpression(source, destination, arg); ClassMapping? classConverter; var ctor = arg.Settings.MapToConstructor as ConstructorInfo; diff --git a/src/Mapster/Adapters/RecordTypeAdapter.cs b/src/Mapster/Adapters/RecordTypeAdapter.cs index 1836fe45..659da39c 100644 --- a/src/Mapster/Adapters/RecordTypeAdapter.cs +++ b/src/Mapster/Adapters/RecordTypeAdapter.cs @@ -40,7 +40,7 @@ protected override Expression CreateInstantiationExpression(Expression source, E SkipIgnoreNullValuesMemberMap.Clear(); Expression installExpr; - if (arg.GetConstructUsing() != null || arg.DestinationType == null) + if (arg.GetConstructUsing() != null || arg.Settings.MapToConstructor != null || arg.DestinationType == null) installExpr = base.CreateInstantiationExpression(source, destination, arg); else { From e192f413392e6ee5437ee115e551582bb6f52c5b Mon Sep 17 00:00:00 2001 From: DocSvartz Date: Thu, 26 Mar 2026 06:02:55 +0500 Subject: [PATCH 27/80] chore: Bump version to 10.0.6 (#897) --- 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 3efb4b24..199c084e 100644 --- a/src/Directory.Build.props +++ b/src/Directory.Build.props @@ -2,7 +2,7 @@ false - 10.0.5 + 10.0.6 netstandard2.0;net10.0;net9.0;net8.0 netstandard2.0;net10.0;net9.0;net8.0 net10.0;net9.0;net8.0 From 39a4ca4f1f7b4d70b7e3b090e00725f7c3733fcd Mon Sep 17 00:00:00 2001 From: DocSvartz Date: Sun, 29 Mar 2026 10:48:02 +0500 Subject: [PATCH 28/80] fix: regression when update required property (#902) * fix(test): fix RequiredProperty test * fix: Regression in v10 - Forced Update of Required Property #899 --- src/Mapster.Core/Enums/MapType.cs | 1 + .../WhenMappingRecordRegression.cs | 6 +---- src/Mapster/Adapters/BaseAdapter.cs | 2 +- src/Mapster/Adapters/BaseClassAdapter.cs | 3 ++- src/Mapster/Compile/CompileArgument.cs | 24 +++++++++++++++++++ 5 files changed, 29 insertions(+), 7 deletions(-) diff --git a/src/Mapster.Core/Enums/MapType.cs b/src/Mapster.Core/Enums/MapType.cs index 420cc812..fa8762f1 100644 --- a/src/Mapster.Core/Enums/MapType.cs +++ b/src/Mapster.Core/Enums/MapType.cs @@ -8,5 +8,6 @@ public enum MapType Map = 1, MapToTarget = 2, Projection = 4, + ApplyNullPropagation = 8, } } \ No newline at end of file diff --git a/src/Mapster.Tests/WhenMappingRecordRegression.cs b/src/Mapster.Tests/WhenMappingRecordRegression.cs index c1bb9dad..85d291e8 100644 --- a/src/Mapster.Tests/WhenMappingRecordRegression.cs +++ b/src/Mapster.Tests/WhenMappingRecordRegression.cs @@ -449,11 +449,7 @@ public void RequiredProperty() { var source = new Person553 { FirstMidName = "John", LastName = "Dow" }; var destination = new Person554 { ID = 245, FirstMidName = "Mary", LastName = "Dow" }; - - TypeAdapterConfig.NewConfig() - //.Map(dest => dest.ID, source => 0) - .Ignore(x => x.ID); - + var s = source.BuildAdapter().CreateMapToTargetExpression(); var result = source.Adapt(destination); diff --git a/src/Mapster/Adapters/BaseAdapter.cs b/src/Mapster/Adapters/BaseAdapter.cs index b519a334..65c9d0f9 100644 --- a/src/Mapster/Adapters/BaseAdapter.cs +++ b/src/Mapster/Adapters/BaseAdapter.cs @@ -220,7 +220,7 @@ protected Expression CreateBlockExpressionBody(Expression source, Expression? de .Any(y => y.GetType().FullName == "System.Runtime.CompilerServices.RequiredMemberAttribute")); if (requiremembers.Count() != 0) - set = CreateInlineExpression(source, arg, true); + set = CreateInlineExpression(source, arg.CloneWith(MapType.ApplyNullPropagation), true); else set = CreateInstantiationExpression(transformedSource, destination, arg); diff --git a/src/Mapster/Adapters/BaseClassAdapter.cs b/src/Mapster/Adapters/BaseClassAdapter.cs index e46c287a..f092c3ab 100644 --- a/src/Mapster/Adapters/BaseClassAdapter.cs +++ b/src/Mapster/Adapters/BaseClassAdapter.cs @@ -113,7 +113,8 @@ select fn(src, destinationMember, arg)) Destination = (ParameterExpression?)destination, UseDestinationValue = IsCanUsingDestinationValue(arg, destinationMember), }; - if(getter == null && !arg.DestinationType.IsRecordType() + if(arg.MapType == MapType.ApplyNullPropagation && + getter == null && !arg.DestinationType.IsRecordType() && destinationMember.Info is PropertyInfo propinfo) { if (propinfo.GetCustomAttributes() diff --git a/src/Mapster/Compile/CompileArgument.cs b/src/Mapster/Compile/CompileArgument.cs index d324661d..827c1d13 100644 --- a/src/Mapster/Compile/CompileArgument.cs +++ b/src/Mapster/Compile/CompileArgument.cs @@ -48,5 +48,29 @@ select split _fetchConstructUsing = true; return _constructUsing; } + + public CompileArgument CloneWith(MapType? mapType = null) + { + var result = new CompileArgument() + { + SourceType = this.SourceType, + DestinationType = this.DestinationType, + MapType = this.MapType, + ExplicitMapping = this.ExplicitMapping, + Settings = this.Settings, + Context = this.Context, + UseDestinationValue = this.UseDestinationValue, + ConstructorMapping = this.ConstructorMapping, + _srcNames = this._srcNames, + _destNames = this._destNames, + _fetchConstructUsing = this._fetchConstructUsing, + _constructUsing = this._constructUsing + }; + + if (mapType != null) + result.MapType = mapType.Value; + + return result; + } } } From 1117602c5b0db272d48dd645acd44c46157c6f56 Mon Sep 17 00:00:00 2001 From: DocSvartz Date: Sun, 29 Mar 2026 10:56:15 +0500 Subject: [PATCH 29/80] fix: added Null Propagation improvement for constructor (#903) * fix(test): fix RequiredProperty test * feat: add Nullable Ctor Checker test * fix: #898 - add Ctor NullPropagateion improvement --- .../WhenAddCtorNullablePropagation.cs | 51 +++++++++++++++++++ src/Mapster/Adapters/BaseClassAdapter.cs | 5 +- src/Mapster/Compile/CompileContext.cs | 1 + src/Mapster/Utils/ExpressionEx.cs | 41 ++++++++++++++- 4 files changed, 96 insertions(+), 2 deletions(-) create mode 100644 src/Mapster.Tests/WhenAddCtorNullablePropagation.cs diff --git a/src/Mapster.Tests/WhenAddCtorNullablePropagation.cs b/src/Mapster.Tests/WhenAddCtorNullablePropagation.cs new file mode 100644 index 00000000..b13eefa6 --- /dev/null +++ b/src/Mapster.Tests/WhenAddCtorNullablePropagation.cs @@ -0,0 +1,51 @@ +using Mapster.Tests.Classes; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using System.Collections.Generic; +using System.Linq; + +namespace Mapster.Tests +{ + [TestClass] + public class WhenAddCtorNullablePropagation + { + + /// + /// https://github.com/MapsterMapper/Mapster/issues/898 + /// + [TestMethod] + public void NullablePropagationFromCtorWorking() + { + var source = new List(); + + 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(); + + var result = source.AsQueryable().ProjectToType().ToList(); + } + + } + + #region TestClasses + + public record OrderDto898(int Id, OrderCodDto898? Cod); + public record OrderCodDto898(long Value); + + + public class OrderEntity898 + { + public int Id { get; set; } + public int? CodId { get; set; } + public OrderCodEntity898? Cod { get; set; } + } + + public class OrderCodEntity898 + { + public int Id { get; set; } + public long Value { get; set; } + } + + + #endregion TestClasses +} diff --git a/src/Mapster/Adapters/BaseClassAdapter.cs b/src/Mapster/Adapters/BaseClassAdapter.cs index f092c3ab..e36ad7a8 100644 --- a/src/Mapster/Adapters/BaseClassAdapter.cs +++ b/src/Mapster/Adapters/BaseClassAdapter.cs @@ -220,6 +220,7 @@ protected Expression CreateInstantiationExpression(Expression source, ClassMappi var arguments = new List(); foreach (var member in members) { + arg.Context.NullChecks.UnionWith(members.Select(x=>(x.Getter,arg))); var parameterInfo = (ParameterInfo)member.DestinationMember.Info!; var defaultConst = parameterInfo.IsOptional ? Expression.Constant(parameterInfo.DefaultValue, member.DestinationMember.Type) @@ -245,7 +246,9 @@ protected Expression CreateInstantiationExpression(Expression source, ClassMappi defaultConst); } else - getter = CreateAdaptExpression(member.Getter, member.DestinationMember.Type, arg, member); + getter = member.Getter + .ApplyNullPropagationFromCtor(CreateAdaptExpression(member.Getter, member.DestinationMember.Type, arg, member), arg); + if (member.Ignore.Condition != null) { diff --git a/src/Mapster/Compile/CompileContext.cs b/src/Mapster/Compile/CompileContext.cs index f0188fb4..72d640c8 100644 --- a/src/Mapster/Compile/CompileContext.cs +++ b/src/Mapster/Compile/CompileContext.cs @@ -12,6 +12,7 @@ public class CompileContext public int? MaxDepth { get; set; } public int Depth { get; set; } public HashSet ExtraParameters { get; } = new(); + public HashSet<(Expression param, CompileArgument arg)> NullChecks { get; } = new(); internal bool IsSubFunction() { diff --git a/src/Mapster/Utils/ExpressionEx.cs b/src/Mapster/Utils/ExpressionEx.cs index 090d81e1..bca07c31 100644 --- a/src/Mapster/Utils/ExpressionEx.cs +++ b/src/Mapster/Utils/ExpressionEx.cs @@ -1,8 +1,9 @@ -using System.Linq.Expressions; +using Mapster.Models; using System; using System.Collections; using System.Collections.Generic; using System.Linq; +using System.Linq.Expressions; using System.Reflection; namespace Mapster.Utils @@ -433,6 +434,44 @@ public static Expression ApplyNullPropagation(this Expression getter) return getter; } + public static Expression ApplyNullPropagationFromCtor(this Expression getter, Expression adapt, CompileArgument arg) + { + Expression? condition = null; + var current = getter; + var checks = arg.Context.NullChecks + .Where(x=> !object.ReferenceEquals(x.arg,arg)) + .Select(x=>x.param.Type); + + while (current != null) + { + Expression? compareNull = null; + + 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)); + + if (compareNull != null) + { + if (condition == null) + condition = compareNull; + else + condition = Expression.AndAlso(compareNull, condition); + } + + if (current is MemberExpression member) + current = member.Expression; + else + current = null; + } + + if (condition == null) + return adapt; + + return Expression.Condition(condition, adapt, adapt.Type.CreateDefault()); + } + public static string? GetMemberPath(this LambdaExpression lambda, bool firstLevelOnly = false, bool noError = false) { var props = new List(); From 4ceb0649bb5a2200381f3b63cc610def3445ddf2 Mon Sep 17 00:00:00 2001 From: DocSvartz Date: Sun, 29 Mar 2026 11:12:08 +0500 Subject: [PATCH 30/80] chore: Bump Version to 10.0.7-pre01 --- 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 199c084e..f73ad693 100644 --- a/src/Directory.Build.props +++ b/src/Directory.Build.props @@ -2,7 +2,7 @@ false - 10.0.6 + 10.0.7-pre01 netstandard2.0;net10.0;net9.0;net8.0 netstandard2.0;net10.0;net9.0;net8.0 net10.0;net9.0;net8.0 From 5848f8536972289e8f668df2e9faca0c7995fd90 Mon Sep 17 00:00:00 2001 From: DocSvartz Date: Mon, 30 Mar 2026 13:26:35 +0500 Subject: [PATCH 31/80] fix: fix bug in #903. Drop collect ctor param with Getter == null --- src/Mapster/Adapters/BaseClassAdapter.cs | 2 +- src/Mapster/Utils/ExpressionEx.cs | 5 ++++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/src/Mapster/Adapters/BaseClassAdapter.cs b/src/Mapster/Adapters/BaseClassAdapter.cs index e36ad7a8..9d9a2f6a 100644 --- a/src/Mapster/Adapters/BaseClassAdapter.cs +++ b/src/Mapster/Adapters/BaseClassAdapter.cs @@ -220,7 +220,7 @@ protected Expression CreateInstantiationExpression(Expression source, ClassMappi var arguments = new List(); foreach (var member in members) { - arg.Context.NullChecks.UnionWith(members.Select(x=>(x.Getter,arg))); + arg.Context.NullChecks.UnionWith(members.Where(x=>x.Getter != null).Select(x=>(x.Getter,arg))); var parameterInfo = (ParameterInfo)member.DestinationMember.Info!; var defaultConst = parameterInfo.IsOptional ? Expression.Constant(parameterInfo.DefaultValue, member.DestinationMember.Type) diff --git a/src/Mapster/Utils/ExpressionEx.cs b/src/Mapster/Utils/ExpressionEx.cs index bca07c31..e442e7a4 100644 --- a/src/Mapster/Utils/ExpressionEx.cs +++ b/src/Mapster/Utils/ExpressionEx.cs @@ -436,11 +436,14 @@ public static Expression ApplyNullPropagation(this Expression getter) public static Expression ApplyNullPropagationFromCtor(this Expression getter, Expression adapt, CompileArgument arg) { + if (getter == null) + return adapt; + Expression? condition = null; var current = getter; var checks = arg.Context.NullChecks .Where(x=> !object.ReferenceEquals(x.arg,arg)) - .Select(x=>x.param.Type); + .Select(x=>x.param?.Type); while (current != null) { From f9a44f6f35fdd86b8e3fcb321a0001db1df860c7 Mon Sep 17 00:00:00 2001 From: DocSvartz Date: Mon, 30 Mar 2026 14:57:32 +0500 Subject: [PATCH 32/80] feat(test): add test for new behavior Property NullablePropagation --- ...enPropertyNullablePropagationRegression.cs | 113 ++++++++++++++++++ 1 file changed, 113 insertions(+) create mode 100644 src/Mapster.Tests/WhenPropertyNullablePropagationRegression.cs diff --git a/src/Mapster.Tests/WhenPropertyNullablePropagationRegression.cs b/src/Mapster.Tests/WhenPropertyNullablePropagationRegression.cs new file mode 100644 index 00000000..d2239a18 --- /dev/null +++ b/src/Mapster.Tests/WhenPropertyNullablePropagationRegression.cs @@ -0,0 +1,113 @@ +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Shouldly; +using System.Threading.Tasks; + +namespace Mapster.Tests; + +[TestClass] +public class WhenPropertyNullablePropagationRegression +{ + /// + /// https://github.com/MapsterMapper/Mapster/issues/858 + /// + /// + [TestMethod] + public async Task NotNullableStructMapToNotNullableCorrect() + { + TypeAdapterConfig + .NewConfig() + .Map(dest => dest.Amount, src => src.Amount) + .Map(dest => dest.InnerAmount, src => src.Inner.Amount); + + + Foo858 foo = new() + { + Amount = new(1, Currency858.Usd), + Inner = new() + { + Amount = new(10, Currency858.Eur), + Int = 100, + } + }; + + // Act + var bar = foo.Adapt(); + // Assert + bar.InnerAmount.Amount.ShouldBe(10m); + } + + [TestMethod] + public async Task NotNullableStructMapToNullableCorrect() + { + TypeAdapterConfig + .NewConfig() + .Map(dest => dest.Amount, src => src.Amount) + .Map(dest => dest.InnerAmount, src => src.Inner.Amount); + + + Foo858 foo = new() + { + Amount = new(1, Currency858.Usd), + Inner = new() + { + Amount = new(10, Currency858.Eur), + Int = 100, + } + }; + + // Act + var bar = foo.Adapt(); + // Assert + bar.InnerAmount?.Amount.ShouldBe(10m); + } + +} + +#region TestClasses +public enum Currency858 +{ + Eur, + Usd, + Ron +} + +file class Foo858 +{ + public required Money858 Amount { get; set; } + public required FooInner858 Inner { get; set; } +} + +file class FooInner858 +{ + public required Money858 Amount { get; set; } + public int Int { get; set; } +} + +file class Bar858 +{ + public Money858 Amount { get; set; } + public Money858 InnerAmount { get; set; } + +} + +file class Bar858Nullable +{ + public Money858? Amount { get; set; } + public Money858? InnerAmount { get; set; } + +} + +public struct Money858 +{ + public decimal? Amount { get; set; } + + public Currency858 Currency { get; set; } = Currency858.Ron; + + public Money858(decimal? amount, Currency858 currency = Currency858.Eur) + { + Amount = amount; + Currency = currency; + } +} + +#endregion TestClasses \ No newline at end of file From d88d346652a7535995f3e48dc91378f3c094ca7e Mon Sep 17 00:00:00 2001 From: DocSvartz Date: Mon, 30 Mar 2026 15:13:06 +0500 Subject: [PATCH 33/80] fix: add new Property NullablePropagation --- src/Mapster/Adapters/BaseClassAdapter.cs | 2 +- src/Mapster/Utils/ExpressionEx.cs | 25 +++++++++++++++++------- 2 files changed, 19 insertions(+), 8 deletions(-) diff --git a/src/Mapster/Adapters/BaseClassAdapter.cs b/src/Mapster/Adapters/BaseClassAdapter.cs index 9d9a2f6a..22314a5b 100644 --- a/src/Mapster/Adapters/BaseClassAdapter.cs +++ b/src/Mapster/Adapters/BaseClassAdapter.cs @@ -132,7 +132,7 @@ select fn(src, destinationMember, arg)) { propertyModel.Getter = arg.MapType == MapType.Projection ? getter - : getter.ApplyNullPropagation(); + : getter.ApplyPropertyNullPropagation(propertyModel); properties.Add(propertyModel); } else diff --git a/src/Mapster/Utils/ExpressionEx.cs b/src/Mapster/Utils/ExpressionEx.cs index e442e7a4..c00c5391 100644 --- a/src/Mapster/Utils/ExpressionEx.cs +++ b/src/Mapster/Utils/ExpressionEx.cs @@ -407,25 +407,36 @@ public static Expression NullableEnumExtractor(this Expression param) return param; } - public static Expression ApplyNullPropagation(this Expression getter) + public static Expression ApplyPropertyNullPropagation(this Expression getter, MemberMapping property) { var current = getter; var result = getter; + Expression? condition = null; + while (current.NodeType == ExpressionType.MemberAccess) { var memEx = (MemberExpression) current; var expr = memEx.Expression; if (expr == null) break; - if (expr.NodeType == ExpressionType.Parameter) - return result; + if (expr.NodeType == ExpressionType.Parameter && condition != null) + { + if (property.DestinationMember.Type.CanBeNull() && !getter.CanBeNull()) + { + var transform = Expression.Convert(getter, typeof(Nullable<>).MakeGenericType(getter.Type)); + return Expression.Condition(condition, transform, transform.Type.CreateDefault()); + } + else + return Expression.Condition(condition, getter, getter.Type.CreateDefault()); + } if (expr.CanBeNull()) { - var compareNull = Expression.Equal(expr, Expression.Constant(null, expr.Type)); - if (!result.Type.CanBeNull()) - result = Expression.Convert(result, typeof(Nullable<>).MakeGenericType(result.Type)); - result = Expression.Condition(compareNull, result.Type.CreateDefault(), result); + var compareNull = Expression.NotEqual(expr, Expression.Constant(null, expr.Type)); + if (condition == null) + condition = compareNull; + else + condition = Expression.AndAlso(compareNull, condition); } current = expr; From d27d46662bb3eee0c9e1466dbc54fcaaf1a23b93 Mon Sep 17 00:00:00 2001 From: DocSvartz Date: Mon, 30 Mar 2026 15:15:10 +0500 Subject: [PATCH 34/80] chore: Bump version to 10.0.7-pre02 --- 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 f73ad693..8ce13048 100644 --- a/src/Directory.Build.props +++ b/src/Directory.Build.props @@ -2,7 +2,7 @@ false - 10.0.7-pre01 + 10.0.7-pre02 netstandard2.0;net10.0;net9.0;net8.0 netstandard2.0;net10.0;net9.0;net8.0 net10.0;net9.0;net8.0 From ec6cf5238ba70126e15cc8765fb979f6734e19a7 Mon Sep 17 00:00:00 2001 From: DocSvartz Date: Tue, 31 Mar 2026 15:23:06 +0500 Subject: [PATCH 35/80] feat(test): replace RequiredProperty test and new Polymorphic test --- .../WhenMappingRecordRegression.cs | 17 +---- .../WhenMappingRequiredPropertyRegression.cs | 68 +++++++++++++++++++ 2 files changed, 69 insertions(+), 16 deletions(-) create mode 100644 src/Mapster.Tests/WhenMappingRequiredPropertyRegression.cs diff --git a/src/Mapster.Tests/WhenMappingRecordRegression.cs b/src/Mapster.Tests/WhenMappingRecordRegression.cs index 85d291e8..4a065274 100644 --- a/src/Mapster.Tests/WhenMappingRecordRegression.cs +++ b/src/Mapster.Tests/WhenMappingRecordRegression.cs @@ -443,22 +443,7 @@ public void FixCtorParamMapping() result.Order.Payment.CVV.ShouldBe("234"); resultID.UserID.ShouldBe("256"); } - - [TestMethod] - public void RequiredProperty() - { - var source = new Person553 { FirstMidName = "John", LastName = "Dow" }; - var destination = new Person554 { ID = 245, FirstMidName = "Mary", LastName = "Dow" }; - - var s = source.BuildAdapter().CreateMapToTargetExpression(); - - var result = source.Adapt(destination); - - result.ID.ShouldBe(245); - result.FirstMidName.ShouldBe(source.FirstMidName); - result.LastName.ShouldBe(source.LastName); - } - + /// /// https://github.com/MapsterMapper/Mapster/issues/842 /// diff --git a/src/Mapster.Tests/WhenMappingRequiredPropertyRegression.cs b/src/Mapster.Tests/WhenMappingRequiredPropertyRegression.cs new file mode 100644 index 00000000..a578321b --- /dev/null +++ b/src/Mapster.Tests/WhenMappingRequiredPropertyRegression.cs @@ -0,0 +1,68 @@ +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Shouldly; +using System; +using System.Collections.Generic; +using System.Text; + +namespace Mapster.Tests +{ + [TestClass] + public class WhenMappingRequiredPropertyRegression + { + [TestMethod] + public void RequiredProperty() + { + var source = new Person553 { FirstMidName = "John", LastName = "Dow" }; + var destination = new Person554 { ID = 245, FirstMidName = "Mary", LastName = "Dow" }; + + var s = source.BuildAdapter().CreateMapToTargetExpression(); + + var result = source.Adapt(destination); + + result.ID.ShouldBe(245); + result.FirstMidName.ShouldBe(source.FirstMidName); + result.LastName.ShouldBe(source.LastName); + } + + [TestMethod] + public void PolymorphicMappingToAbstractClassCompileWithoutError() + { + var config = TypeAdapterConfig.GlobalSettings; + + config.NewConfig(); + + config.NewConfig() + .Include(); + + Should.NotThrow(() => + { + config.Compile(); + }); + + } + } + + #region TestClasses + + public abstract class AbstractSource + { + public abstract string Name { get; } + } + + public class ConcreteSource : AbstractSource + { + public override string Name => "Test"; + } + + public abstract class AbstractDestination + { + public required string Name { get; set; } + } + + public class ConcreteDestination : AbstractDestination + { + + } + + #endregion TestClasses +} From a06ce0287a305f09ccecd9c21107433b06d00e56 Mon Sep 17 00:00:00 2001 From: DocSvartz Date: Tue, 31 Mar 2026 15:29:19 +0500 Subject: [PATCH 36/80] fix: #906 - InvalidCastException when using polymorphic mapping with required properties --- src/Mapster/Adapters/BaseAdapter.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Mapster/Adapters/BaseAdapter.cs b/src/Mapster/Adapters/BaseAdapter.cs index 65c9d0f9..dba50681 100644 --- a/src/Mapster/Adapters/BaseAdapter.cs +++ b/src/Mapster/Adapters/BaseAdapter.cs @@ -219,7 +219,7 @@ protected Expression CreateBlockExpressionBody(Expression source, Expression? de .Where(x => x.GetCustomAttributes() .Any(y => y.GetType().FullName == "System.Runtime.CompilerServices.RequiredMemberAttribute")); - if (requiremembers.Count() != 0) + if (requiremembers.Count() != 0 && !arg.DestinationType.IsAbstract) set = CreateInlineExpression(source, arg.CloneWith(MapType.ApplyNullPropagation), true); else set = CreateInstantiationExpression(transformedSource, destination, arg); From c049cf6f9a56aaaa57dff87e8670c51f5273d907 Mon Sep 17 00:00:00 2001 From: DocSvartz Date: Tue, 31 Mar 2026 15:35:58 +0500 Subject: [PATCH 37/80] chore: Bump Version to 10.0.7-pre03 --- 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 8ce13048..064b39cf 100644 --- a/src/Directory.Build.props +++ b/src/Directory.Build.props @@ -2,7 +2,7 @@ false - 10.0.7-pre02 + 10.0.7-pre03 netstandard2.0;net10.0;net9.0;net8.0 netstandard2.0;net10.0;net9.0;net8.0 net10.0;net9.0;net8.0 From 8b02e2848ff619481fb2eb60fc1b08e086a54d58 Mon Sep 17 00:00:00 2001 From: DocSvartz Date: Wed, 1 Apr 2026 14:47:25 +0500 Subject: [PATCH 38/80] fix: re-fix #858 and fix IgnoreNullValues regression from MapToTarget cases in #905 (#908) --- ...enPropertyNullablePropagationRegression.cs | 83 +++++++++++++++++-- src/Mapster/Adapters/BaseClassAdapter.cs | 2 +- src/Mapster/Adapters/ClassAdapter.cs | 24 +++++- src/Mapster/Utils/ExpressionEx.cs | 22 ++++- 4 files changed, 121 insertions(+), 10 deletions(-) diff --git a/src/Mapster.Tests/WhenPropertyNullablePropagationRegression.cs b/src/Mapster.Tests/WhenPropertyNullablePropagationRegression.cs index d2239a18..1e1e27d9 100644 --- a/src/Mapster.Tests/WhenPropertyNullablePropagationRegression.cs +++ b/src/Mapster.Tests/WhenPropertyNullablePropagationRegression.cs @@ -12,12 +12,16 @@ public class WhenPropertyNullablePropagationRegression /// /// [TestMethod] - public async Task NotNullableStructMapToNotNullableCorrect() + public void NotNullableStructMapToNotNullableCorrect() { - TypeAdapterConfig + TypeAdapterConfig + .NewConfig() + .Map(dest => dest.Amount, src => src.Amount) + .Map(dest => dest.InnerAmount, src => src.Inner.Amount); + + TypeAdapterConfig .NewConfig() - .Map(dest => dest.Amount, src => src.Amount) - .Map(dest => dest.InnerAmount, src => src.Inner.Amount); + .IgnoreNullValues(true); Foo858 foo = new() @@ -32,12 +36,13 @@ public async Task NotNullableStructMapToNotNullableCorrect() // Act var bar = foo.Adapt(); + // Assert bar.InnerAmount.Amount.ShouldBe(10m); } [TestMethod] - public async Task NotNullableStructMapToNullableCorrect() + public void NotNullableStructMapToNullableCorrect() { TypeAdapterConfig .NewConfig() @@ -61,6 +66,74 @@ public async Task NotNullableStructMapToNullableCorrect() bar.InnerAmount?.Amount.ShouldBe(10m); } + [TestMethod] + public void IgnoreNullValueWorkCorrect() + { + TypeAdapterConfig + .NewConfig() + .IgnoreNullValues(true) + .Map(dest => dest.Amount, src => src.Amount) + .Map(dest => dest.InnerAmount, src => src.Inner.Amount); + + Foo858 foo = new() + { + Amount = new(1, Currency858.Usd), + Inner = new() + { + Amount = new(10, Currency858.Eur), + Int = 100, + } + }; + + var nullFoo = new Foo858() { Amount = new(2, Currency858.Ron), Inner = null }; + + // Act + var bar = foo.Adapt(); + nullFoo.Adapt(bar); + + // Assert + bar.InnerAmount.Amount.ShouldBe(10m); + bar.Amount.Amount.ShouldBe(2m); + bar.Amount.Currency.ShouldBe(Currency858.Ron); + } + + [TestMethod] + public void MapToTargetWorkCorrect() + { + TypeAdapterConfig + .NewConfig() + .IgnoreNullValues(true) + .Map(dest => dest.Amount, src => src.Amount) + .Map(dest => dest.InnerAmount, src => src.Inner.Amount); + + Foo858 foo = new() + { + Amount = new(1, Currency858.Usd), + Inner = new() + { + Amount = new(10, Currency858.Eur), + Int = 100, + } + }; + + var nullFoo = new Foo858() { Amount = new(2, Currency858.Ron), Inner = new() + { + Amount = new(20, Currency858.Eur), + Int = 100, + } + }; + + // Act + var bar = foo.Adapt(); + nullFoo.Adapt(bar); + + // Assert + bar.InnerAmount.Amount.ShouldBe(20m); + bar.Amount.Amount.ShouldBe(2m); + bar.Amount.Currency.ShouldBe(Currency858.Ron); + } + + } #region TestClasses diff --git a/src/Mapster/Adapters/BaseClassAdapter.cs b/src/Mapster/Adapters/BaseClassAdapter.cs index 22314a5b..a8b7a4c0 100644 --- a/src/Mapster/Adapters/BaseClassAdapter.cs +++ b/src/Mapster/Adapters/BaseClassAdapter.cs @@ -132,7 +132,7 @@ select fn(src, destinationMember, arg)) { propertyModel.Getter = arg.MapType == MapType.Projection ? getter - : getter.ApplyPropertyNullPropagation(propertyModel); + : getter.ApplyPropertyNullPropagation(); properties.Add(propertyModel); } else diff --git a/src/Mapster/Adapters/ClassAdapter.cs b/src/Mapster/Adapters/ClassAdapter.cs index 78cfacc1..a17b079a 100644 --- a/src/Mapster/Adapters/ClassAdapter.cs +++ b/src/Mapster/Adapters/ClassAdapter.cs @@ -114,7 +114,17 @@ protected override Expression CreateBlockExpression(Expression source, Expressio ? member.DestinationMember.GetExpression(destination) : null; - var adapt = CreateAdaptExpression(member.Getter, member.DestinationMember.Type, arg, member, destMember); + Expression adapt; + + // convert ApplyNullable Propagation for NotPrimitive Nullable types + if (member.Getter is ConditionalExpression cond && member.Getter.Type.IsNotPrimitiveNullableType() + && !member.DestinationMember.Type.IsNullable()) + { + var value = CreateAdaptExpression(cond.IfTrue.GetNotPrimitiveNullableValue(), member.DestinationMember.Type, arg, member, destMember); + adapt = Expression.Condition(cond.Test, value, member.DestinationMember.Type.CreateDefault()); + } + else + adapt = CreateAdaptExpression(member.Getter, member.DestinationMember.Type, arg, member, destMember); if (member.UseDestinationValue && member.DestinationMember.Type.IsMapsterImmutable() @@ -251,7 +261,17 @@ private static Expression SetValueByReflection(MemberMapping member, MemberExpre if (member.DestinationMember.SetterModifier == AccessModifier.None) continue; - var value = CreateAdaptExpression(member.Getter, member.DestinationMember.Type, arg, member); + Expression value; + + // convert ApplyNullable Propagation for NotPrimitive Nullable types + if (member.Getter is ConditionalExpression cond && member.Getter.Type.IsNotPrimitiveNullableType() + && !member.DestinationMember.Type.IsNullable()) + { + var adapt = CreateAdaptExpression(cond.IfTrue.GetNotPrimitiveNullableValue(), member.DestinationMember.Type, arg, member); + value = Expression.Condition(cond.Test, adapt, member.DestinationMember.Type.CreateDefault()); + } + else + value = CreateAdaptExpression(member.Getter, member.DestinationMember.Type, arg, member); //special null property check for projection //if we don't set null to property, EF will create empty object diff --git a/src/Mapster/Utils/ExpressionEx.cs b/src/Mapster/Utils/ExpressionEx.cs index c00c5391..4e6af8a7 100644 --- a/src/Mapster/Utils/ExpressionEx.cs +++ b/src/Mapster/Utils/ExpressionEx.cs @@ -407,7 +407,7 @@ public static Expression NullableEnumExtractor(this Expression param) return param; } - public static Expression ApplyPropertyNullPropagation(this Expression getter, MemberMapping property) + public static Expression ApplyPropertyNullPropagation(this Expression getter) { var current = getter; var result = getter; @@ -421,7 +421,7 @@ public static Expression ApplyPropertyNullPropagation(this Expression getter, Me break; if (expr.NodeType == ExpressionType.Parameter && condition != null) { - if (property.DestinationMember.Type.CanBeNull() && !getter.CanBeNull()) + if (!getter.CanBeNull()) { var transform = Expression.Convert(getter, typeof(Nullable<>).MakeGenericType(getter.Type)); return Expression.Condition(condition, transform, transform.Type.CreateDefault()); @@ -532,5 +532,23 @@ internal static Expression GetNameConverterExpression(Func conve return Expression.Constant(converter); } + public static bool IsNotPrimitiveNullableType(this Type type) + { + 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; + } + } } From 731af095a93b4964d147e238c03dc695a54ba0cf Mon Sep 17 00:00:00 2001 From: DocSvartz Date: Wed, 1 Apr 2026 14:50:11 +0500 Subject: [PATCH 39/80] chore: Bump version to 10.0.7-pre04 --- 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 064b39cf..79ca77e6 100644 --- a/src/Directory.Build.props +++ b/src/Directory.Build.props @@ -2,7 +2,7 @@ false - 10.0.7-pre03 + 10.0.7-pre04 netstandard2.0;net10.0;net9.0;net8.0 netstandard2.0;net10.0;net9.0;net8.0 net10.0;net9.0;net8.0 From 1fcf33bca0de2c7db23deea3b72527f4adf70636 Mon Sep 17 00:00:00 2001 From: DocSvartz Date: Tue, 7 Apr 2026 09:18:13 +0500 Subject: [PATCH 40/80] fix: #911- return in 7.4.0 behavior for types cannot be instantiated from itselves --- .../WhenMappingRecordRegression.cs | 17 +++++++++++++++++ src/Mapster/Adapters/ClassAdapter.cs | 2 +- src/Mapster/Adapters/NotSelfCreationAdapter.cs | 16 ++++++++++++++++ src/Mapster/Adapters/PrimitiveAdapter.cs | 2 +- src/Mapster/TypeAdapterConfig.cs | 5 +++-- src/Mapster/Utils/ReflectionUtils.cs | 13 +++++++++++++ 6 files changed, 51 insertions(+), 4 deletions(-) create mode 100644 src/Mapster/Adapters/NotSelfCreationAdapter.cs diff --git a/src/Mapster.Tests/WhenMappingRecordRegression.cs b/src/Mapster.Tests/WhenMappingRecordRegression.cs index 4a065274..94518641 100644 --- a/src/Mapster.Tests/WhenMappingRecordRegression.cs +++ b/src/Mapster.Tests/WhenMappingRecordRegression.cs @@ -2,6 +2,7 @@ using Shouldly; using System; using System.Collections.Generic; +using System.Text.Json; using static Mapster.Tests.WhenExplicitMappingRequired; using static Mapster.Tests.WhenMappingDerived; @@ -522,6 +523,22 @@ public void ClassCtorActivateDefaultValue() }); } + /// + /// https://github.com/MapsterMapper/Mapster/issues/911 + /// + [TestMethod] + public void NotSelfCreationTypeMappingToSelfWithOutError() + { + var src = new Uri("https://www.google.com/"); + var srcJ = JsonDocument.Parse("{\"key\": \"value\"}"); + + var result = src.Adapt(); + var resultJ = srcJ.Adapt(); + + result.ToString().ShouldBe("https://www.google.com/"); + resultJ.RootElement.GetProperty("key").ToString().ShouldBe("value"); + } + #region NowNotWorking /// diff --git a/src/Mapster/Adapters/ClassAdapter.cs b/src/Mapster/Adapters/ClassAdapter.cs index a17b079a..03a2eb6b 100644 --- a/src/Mapster/Adapters/ClassAdapter.cs +++ b/src/Mapster/Adapters/ClassAdapter.cs @@ -16,7 +16,7 @@ namespace Mapster.Adapters /// internal class ClassAdapter : BaseClassAdapter { - protected override int Score => -150; + protected override int Score => -200; protected override bool CanMap(PreCompileArgument arg) { diff --git a/src/Mapster/Adapters/NotSelfCreationAdapter.cs b/src/Mapster/Adapters/NotSelfCreationAdapter.cs new file mode 100644 index 00000000..7079ec86 --- /dev/null +++ b/src/Mapster/Adapters/NotSelfCreationAdapter.cs @@ -0,0 +1,16 @@ +namespace Mapster.Adapters +{ + /// + /// Immitation behavior in 7.4.0 for Types that cannot be instantiated from itselves + /// Example: Uri, JsonDocument + /// + internal class NotSelfCreationAdapter : PrimitiveAdapter + { + protected override int Score => -150; + + protected override bool CanMap(PreCompileArgument arg) + { + return !arg.ExplicitMapping && arg.SourceType == arg.DestinationType && arg.DestinationType.IsNotSelfCreation(); + } + } +} diff --git a/src/Mapster/Adapters/PrimitiveAdapter.cs b/src/Mapster/Adapters/PrimitiveAdapter.cs index c2d411db..622f32b1 100644 --- a/src/Mapster/Adapters/PrimitiveAdapter.cs +++ b/src/Mapster/Adapters/PrimitiveAdapter.cs @@ -9,7 +9,7 @@ namespace Mapster.Adapters { internal class PrimitiveAdapter : BaseAdapter { - protected override int Score => -200; //must do last + protected override int Score => -210; //must do last protected override bool CanMap(PreCompileArgument arg) { diff --git a/src/Mapster/TypeAdapterConfig.cs b/src/Mapster/TypeAdapterConfig.cs index 1e6abade..34f22495 100644 --- a/src/Mapster/TypeAdapterConfig.cs +++ b/src/Mapster/TypeAdapterConfig.cs @@ -20,8 +20,9 @@ private static List CreateRuleTemplate() { return new List { - new PrimitiveAdapter().CreateRule(), //-200 - new ClassAdapter().CreateRule(), //-150 + new PrimitiveAdapter().CreateRule(), //-210 + new ClassAdapter().CreateRule(), //-200 + new NotSelfCreationAdapter().CreateRule(), //-150 new RecordTypeAdapter().CreateRule(), //-149 new ReadOnlyInterfaceAdapter().CreateRule(), // -148 new CollectionAdapter().CreateRule(), //-125 diff --git a/src/Mapster/Utils/ReflectionUtils.cs b/src/Mapster/Utils/ReflectionUtils.cs index 505166eb..a010984a 100644 --- a/src/Mapster/Utils/ReflectionUtils.cs +++ b/src/Mapster/Utils/ReflectionUtils.cs @@ -453,5 +453,18 @@ public static bool IsMapsterImmutable(this Type type) { return type.IsMapsterPrimitive() || type.IsRecordType(); } + + public static bool IsNotSelfCreation(this Type type) + { + if (type.IsMapsterPrimitive()) + return false; + if(type.IsCollectionCompatible()) + return false; + + if (type == typeof(Type) || type.BaseType == typeof(MulticastDelegate)) + return true; + + return type.GetFieldsAndProperties().Any(it => (it.SetterModifier & (AccessModifier.Public | AccessModifier.NonPublic)) == 0); + } } } From 27f41a45271f717cea2009ebdf10a72d3eba7d9b Mon Sep 17 00:00:00 2001 From: DocSvartz Date: Tue, 7 Apr 2026 13:45:17 +0500 Subject: [PATCH 41/80] chore: Bump version to 10.0.7 - Release --- 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 79ca77e6..91cb6918 100644 --- a/src/Directory.Build.props +++ b/src/Directory.Build.props @@ -2,7 +2,7 @@ false - 10.0.7-pre04 + 10.0.7 netstandard2.0;net10.0;net9.0;net8.0 netstandard2.0;net10.0;net9.0;net8.0 net10.0;net9.0;net8.0 From 56935ea789b105ece8d357a3a4e4aa74908ba267 Mon Sep 17 00:00:00 2001 From: DocSvartz Date: Wed, 8 Apr 2026 10:39:49 +0500 Subject: [PATCH 42/80] --Duplicate .\Data-types.md history into .\data-types\Record-types.md --- .../mapping/{Data-types.md => data-types/Record-types.md} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename docs/articles/mapping/{Data-types.md => data-types/Record-types.md} (100%) diff --git a/docs/articles/mapping/Data-types.md b/docs/articles/mapping/data-types/Record-types.md similarity index 100% rename from docs/articles/mapping/Data-types.md rename to docs/articles/mapping/data-types/Record-types.md From 491eec7b9efe34f74292c0c2e174d29b3907abdc Mon Sep 17 00:00:00 2001 From: DocSvartz Date: Wed, 8 Apr 2026 10:39:49 +0500 Subject: [PATCH 43/80] --Restore .\Data-types.md --- docs/articles/mapping/Data-types.md | 192 ++++++++++++++++++++++++++++ 1 file changed, 192 insertions(+) create mode 100644 docs/articles/mapping/Data-types.md diff --git a/docs/articles/mapping/Data-types.md b/docs/articles/mapping/Data-types.md new file mode 100644 index 00000000..c139ee0c --- /dev/null +++ b/docs/articles/mapping/Data-types.md @@ -0,0 +1,192 @@ +--- +uid: Mapster.Mapping.DataTypes +title: "Mapping - Data Types" +--- + +## Primitives + +Converting between primitive types (ie. int, bool, double, decimal) is supported, including when those types are nullable. For all other types, if you can cast types in c#, you can also cast in Mapster. + +```csharp +decimal i = 123.Adapt(); //equal to (decimal)123; +``` + +## Enums + +Mapster maps enums to numerics automatically, but it also maps strings to and from enums automatically in a fast manner. +The default Enum.ToString() in .NET is quite slow. The implementation in Mapster is double the speed. Likewise, a fast conversion from strings to enums is also included. If the string is null or empty, the enum will initialize to the first enum value. + +In Mapster, flagged enums are also supported. + +```csharp +var e = "Read, Write, Delete".Adapt(); +//FileShare.Read | FileShare.Write | FileShare.Delete +``` + +For enum to enum with different type, by default, Mapster will map enum by value. You can override to map enum by name by: + +```csharp +TypeAdapterConfig.GlobalSettings.Default + .EnumMappingStrategy(EnumMappingStrategy.ByName); +``` + +## Strings + +When Mapster maps other types to string, Mapster will use `ToString` method. And whenever Mapster maps string to the other types, Mapster will use `Parse` method. + +```csharp +var s = 123.Adapt(); //equal to 123.ToString(); +var i = "123".Adapt(); //equal to int.Parse("123"); +``` + +## Collections + +This includes mapping among lists, arrays, collections, dictionary including various interfaces: `IList`, `ICollection`, `IEnumerable`, `ISet`, `IDictionary` etc... + +```csharp +var list = db.Pocos.ToList(); +var target = list.Adapt>(); +``` + +## Mappable Objects + +Mapster can map two different objects using the following rules: + +- Source and destination property names are the same. Ex: `dest.Name = src.Name` +- Source has get method. Ex: `dest.Name = src.GetName()` +- Source property has child object which can flatten to destination. Ex: `dest.ContactName = src.Contact.Name` or `dest.Contact_Name = src.Contact.Name` + +Example: + +```csharp +class Staff { + public string Name { get; set; } + public int GetAge() { + return (DateTime.Now - this.BirthDate).TotalDays / 365.25; + } + public Staff Supervisor { get; set; } + ... +} + +struct StaffDto { + public string Name { get; set; } + public int Age { get; set; } + public string SupervisorName { get; set; } +} + +var dto = staff.Adapt(); +//dto.Name = staff.Name, dto.Age = staff.GetAge(), dto.SupervisorName = staff.Supervisor.Name +``` + +**Mappable Object types are included:** + +- POCO classes +- POCO structs +- POCO interfaces +- Dictionary type implement `IDictionary` +- Record types (either class, struct, and interface) + +Example for object to dictionary: + +```csharp +var point = new { X = 2, Y = 3 }; +var dict = point.Adapt>(); +dict["Y"].ShouldBe(3); +``` + +## Record types + +>[!IMPORTANT] +> Mapster treats Record type as an immutable type. +> Only a Nondestructive mutation - creating a new object with modified properties. +> +> ```csharp +> var result = source.adapt(data) +>//equal var result = data with { X = source.X.Adapt(), ...} +>``` + +### Features and Limitations: + +# [v10.0](#tab/Records-v10) + +>[!NOTE] +> By default, all [C# Records](https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/builtin-types/record) are defined as a record type. +> Limitations by count of constructors and constructor parameters used in Mapster version 7.4.0 do not apply. + + +#### Using default value in constuctor param + +If the source type does not contain members that can be used as constructor parameters, then will be used the default values ​​for the parameter type. + +Example: + +```csharp + +class SourceData +{ + public string MyString {get; set;} +} + +record RecordDestination(int myInt, string myString); + +var result = source.Adapt() + +// equal var result = new RecordDestination (default(int),source.myString) + +``` + +#### MultiConstructor Record types + +If there is more than one constructor, by default, mapping will be performed on the constructor with the largest number of parameters. + +Example: + +```csharp +record MultiCtorRecord +{ + public MultiCtorRecord(int myInt) + { + MyInt = myInt; + } + + public MultiCtorRecord(int myInt, string myString) // This constructor will be used + : this(myInt) + { + MyString = myString; + } + +} +``` + +# [v7.4.0](#tab/Records-v7-4-0) + +>[!NOTE] +>Record type must not have a setter and have only one non-empty constructor, and all parameter names must match with properties. + +Otherwise you need to add [`MapToConstructor` configuration](xref:Mapster.Settings.ConstructorMapping#map-to-constructor). + +Example for record types: + +```csharp +class Person { + public string Name { get; } + public int Age { get; } + + public Person(string name, int age) { + this.Name = name; + this.Age = age; + } +} + +var src = new { Name = "Mapster", Age = 3 }; +var target = src.Adapt(); +``` +--- + +### Support additional mapping features: + +| Mapping features | v7.4.0 | v10.0 | +|:-----------------|:------:|:-----:| +|[Custom constructor mapping](xref:Mapster.Settings.ConstructorMapping)| - | ✅ | +|[Ignore](xref:Mapster.Settings.Custom.IgnoringMembers#ignore-extension-method)| - | ✅ | +|[IgnoreNullValues](xref:Mapster.Settings.Custom.IgnoringMembers#ignorenullvalues-extension-method)| - | ✅ | From 4bccc4c51eafbb04c13dc7d14470c917e5b6115b Mon Sep 17 00:00:00 2001 From: DocSvartz Date: Wed, 8 Apr 2026 10:40:35 +0500 Subject: [PATCH 44/80] --Duplicate .\Data-types.md history into .\data-types\Mapping-types.md --- .../mapping/{Data-types.md => data-types/Mapping-types.md} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename docs/articles/mapping/{Data-types.md => data-types/Mapping-types.md} (100%) diff --git a/docs/articles/mapping/Data-types.md b/docs/articles/mapping/data-types/Mapping-types.md similarity index 100% rename from docs/articles/mapping/Data-types.md rename to docs/articles/mapping/data-types/Mapping-types.md From d84b3e788152a5ae76b4f23848fb936fc653e5d2 Mon Sep 17 00:00:00 2001 From: DocSvartz Date: Wed, 8 Apr 2026 10:40:35 +0500 Subject: [PATCH 45/80] --Restore .\Data-types.md --- docs/articles/mapping/Data-types.md | 192 ++++++++++++++++++++++++++++ 1 file changed, 192 insertions(+) create mode 100644 docs/articles/mapping/Data-types.md diff --git a/docs/articles/mapping/Data-types.md b/docs/articles/mapping/Data-types.md new file mode 100644 index 00000000..c139ee0c --- /dev/null +++ b/docs/articles/mapping/Data-types.md @@ -0,0 +1,192 @@ +--- +uid: Mapster.Mapping.DataTypes +title: "Mapping - Data Types" +--- + +## Primitives + +Converting between primitive types (ie. int, bool, double, decimal) is supported, including when those types are nullable. For all other types, if you can cast types in c#, you can also cast in Mapster. + +```csharp +decimal i = 123.Adapt(); //equal to (decimal)123; +``` + +## Enums + +Mapster maps enums to numerics automatically, but it also maps strings to and from enums automatically in a fast manner. +The default Enum.ToString() in .NET is quite slow. The implementation in Mapster is double the speed. Likewise, a fast conversion from strings to enums is also included. If the string is null or empty, the enum will initialize to the first enum value. + +In Mapster, flagged enums are also supported. + +```csharp +var e = "Read, Write, Delete".Adapt(); +//FileShare.Read | FileShare.Write | FileShare.Delete +``` + +For enum to enum with different type, by default, Mapster will map enum by value. You can override to map enum by name by: + +```csharp +TypeAdapterConfig.GlobalSettings.Default + .EnumMappingStrategy(EnumMappingStrategy.ByName); +``` + +## Strings + +When Mapster maps other types to string, Mapster will use `ToString` method. And whenever Mapster maps string to the other types, Mapster will use `Parse` method. + +```csharp +var s = 123.Adapt(); //equal to 123.ToString(); +var i = "123".Adapt(); //equal to int.Parse("123"); +``` + +## Collections + +This includes mapping among lists, arrays, collections, dictionary including various interfaces: `IList`, `ICollection`, `IEnumerable`, `ISet`, `IDictionary` etc... + +```csharp +var list = db.Pocos.ToList(); +var target = list.Adapt>(); +``` + +## Mappable Objects + +Mapster can map two different objects using the following rules: + +- Source and destination property names are the same. Ex: `dest.Name = src.Name` +- Source has get method. Ex: `dest.Name = src.GetName()` +- Source property has child object which can flatten to destination. Ex: `dest.ContactName = src.Contact.Name` or `dest.Contact_Name = src.Contact.Name` + +Example: + +```csharp +class Staff { + public string Name { get; set; } + public int GetAge() { + return (DateTime.Now - this.BirthDate).TotalDays / 365.25; + } + public Staff Supervisor { get; set; } + ... +} + +struct StaffDto { + public string Name { get; set; } + public int Age { get; set; } + public string SupervisorName { get; set; } +} + +var dto = staff.Adapt(); +//dto.Name = staff.Name, dto.Age = staff.GetAge(), dto.SupervisorName = staff.Supervisor.Name +``` + +**Mappable Object types are included:** + +- POCO classes +- POCO structs +- POCO interfaces +- Dictionary type implement `IDictionary` +- Record types (either class, struct, and interface) + +Example for object to dictionary: + +```csharp +var point = new { X = 2, Y = 3 }; +var dict = point.Adapt>(); +dict["Y"].ShouldBe(3); +``` + +## Record types + +>[!IMPORTANT] +> Mapster treats Record type as an immutable type. +> Only a Nondestructive mutation - creating a new object with modified properties. +> +> ```csharp +> var result = source.adapt(data) +>//equal var result = data with { X = source.X.Adapt(), ...} +>``` + +### Features and Limitations: + +# [v10.0](#tab/Records-v10) + +>[!NOTE] +> By default, all [C# Records](https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/builtin-types/record) are defined as a record type. +> Limitations by count of constructors and constructor parameters used in Mapster version 7.4.0 do not apply. + + +#### Using default value in constuctor param + +If the source type does not contain members that can be used as constructor parameters, then will be used the default values ​​for the parameter type. + +Example: + +```csharp + +class SourceData +{ + public string MyString {get; set;} +} + +record RecordDestination(int myInt, string myString); + +var result = source.Adapt() + +// equal var result = new RecordDestination (default(int),source.myString) + +``` + +#### MultiConstructor Record types + +If there is more than one constructor, by default, mapping will be performed on the constructor with the largest number of parameters. + +Example: + +```csharp +record MultiCtorRecord +{ + public MultiCtorRecord(int myInt) + { + MyInt = myInt; + } + + public MultiCtorRecord(int myInt, string myString) // This constructor will be used + : this(myInt) + { + MyString = myString; + } + +} +``` + +# [v7.4.0](#tab/Records-v7-4-0) + +>[!NOTE] +>Record type must not have a setter and have only one non-empty constructor, and all parameter names must match with properties. + +Otherwise you need to add [`MapToConstructor` configuration](xref:Mapster.Settings.ConstructorMapping#map-to-constructor). + +Example for record types: + +```csharp +class Person { + public string Name { get; } + public int Age { get; } + + public Person(string name, int age) { + this.Name = name; + this.Age = age; + } +} + +var src = new { Name = "Mapster", Age = 3 }; +var target = src.Adapt(); +``` +--- + +### Support additional mapping features: + +| Mapping features | v7.4.0 | v10.0 | +|:-----------------|:------:|:-----:| +|[Custom constructor mapping](xref:Mapster.Settings.ConstructorMapping)| - | ✅ | +|[Ignore](xref:Mapster.Settings.Custom.IgnoringMembers#ignore-extension-method)| - | ✅ | +|[IgnoreNullValues](xref:Mapster.Settings.Custom.IgnoringMembers#ignorenullvalues-extension-method)| - | ✅ | From f86367852f0c8560266dc9daaaca5276515ed2a7 Mon Sep 17 00:00:00 2001 From: DocSvartz Date: Wed, 8 Apr 2026 10:40:56 +0500 Subject: [PATCH 46/80] --Duplicate .\Data-types.md history into .\data-types\Primitive-types.md --- .../mapping/{Data-types.md => data-types/Primitive-types.md} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename docs/articles/mapping/{Data-types.md => data-types/Primitive-types.md} (100%) diff --git a/docs/articles/mapping/Data-types.md b/docs/articles/mapping/data-types/Primitive-types.md similarity index 100% rename from docs/articles/mapping/Data-types.md rename to docs/articles/mapping/data-types/Primitive-types.md From 1878ffe26d4953a7652e6895ee8c875e157393d3 Mon Sep 17 00:00:00 2001 From: DocSvartz Date: Wed, 8 Apr 2026 10:40:56 +0500 Subject: [PATCH 47/80] --Restore .\Data-types.md --- docs/articles/mapping/Data-types.md | 192 ++++++++++++++++++++++++++++ 1 file changed, 192 insertions(+) create mode 100644 docs/articles/mapping/Data-types.md diff --git a/docs/articles/mapping/Data-types.md b/docs/articles/mapping/Data-types.md new file mode 100644 index 00000000..c139ee0c --- /dev/null +++ b/docs/articles/mapping/Data-types.md @@ -0,0 +1,192 @@ +--- +uid: Mapster.Mapping.DataTypes +title: "Mapping - Data Types" +--- + +## Primitives + +Converting between primitive types (ie. int, bool, double, decimal) is supported, including when those types are nullable. For all other types, if you can cast types in c#, you can also cast in Mapster. + +```csharp +decimal i = 123.Adapt(); //equal to (decimal)123; +``` + +## Enums + +Mapster maps enums to numerics automatically, but it also maps strings to and from enums automatically in a fast manner. +The default Enum.ToString() in .NET is quite slow. The implementation in Mapster is double the speed. Likewise, a fast conversion from strings to enums is also included. If the string is null or empty, the enum will initialize to the first enum value. + +In Mapster, flagged enums are also supported. + +```csharp +var e = "Read, Write, Delete".Adapt(); +//FileShare.Read | FileShare.Write | FileShare.Delete +``` + +For enum to enum with different type, by default, Mapster will map enum by value. You can override to map enum by name by: + +```csharp +TypeAdapterConfig.GlobalSettings.Default + .EnumMappingStrategy(EnumMappingStrategy.ByName); +``` + +## Strings + +When Mapster maps other types to string, Mapster will use `ToString` method. And whenever Mapster maps string to the other types, Mapster will use `Parse` method. + +```csharp +var s = 123.Adapt(); //equal to 123.ToString(); +var i = "123".Adapt(); //equal to int.Parse("123"); +``` + +## Collections + +This includes mapping among lists, arrays, collections, dictionary including various interfaces: `IList`, `ICollection`, `IEnumerable`, `ISet`, `IDictionary` etc... + +```csharp +var list = db.Pocos.ToList(); +var target = list.Adapt>(); +``` + +## Mappable Objects + +Mapster can map two different objects using the following rules: + +- Source and destination property names are the same. Ex: `dest.Name = src.Name` +- Source has get method. Ex: `dest.Name = src.GetName()` +- Source property has child object which can flatten to destination. Ex: `dest.ContactName = src.Contact.Name` or `dest.Contact_Name = src.Contact.Name` + +Example: + +```csharp +class Staff { + public string Name { get; set; } + public int GetAge() { + return (DateTime.Now - this.BirthDate).TotalDays / 365.25; + } + public Staff Supervisor { get; set; } + ... +} + +struct StaffDto { + public string Name { get; set; } + public int Age { get; set; } + public string SupervisorName { get; set; } +} + +var dto = staff.Adapt(); +//dto.Name = staff.Name, dto.Age = staff.GetAge(), dto.SupervisorName = staff.Supervisor.Name +``` + +**Mappable Object types are included:** + +- POCO classes +- POCO structs +- POCO interfaces +- Dictionary type implement `IDictionary` +- Record types (either class, struct, and interface) + +Example for object to dictionary: + +```csharp +var point = new { X = 2, Y = 3 }; +var dict = point.Adapt>(); +dict["Y"].ShouldBe(3); +``` + +## Record types + +>[!IMPORTANT] +> Mapster treats Record type as an immutable type. +> Only a Nondestructive mutation - creating a new object with modified properties. +> +> ```csharp +> var result = source.adapt(data) +>//equal var result = data with { X = source.X.Adapt(), ...} +>``` + +### Features and Limitations: + +# [v10.0](#tab/Records-v10) + +>[!NOTE] +> By default, all [C# Records](https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/builtin-types/record) are defined as a record type. +> Limitations by count of constructors and constructor parameters used in Mapster version 7.4.0 do not apply. + + +#### Using default value in constuctor param + +If the source type does not contain members that can be used as constructor parameters, then will be used the default values ​​for the parameter type. + +Example: + +```csharp + +class SourceData +{ + public string MyString {get; set;} +} + +record RecordDestination(int myInt, string myString); + +var result = source.Adapt() + +// equal var result = new RecordDestination (default(int),source.myString) + +``` + +#### MultiConstructor Record types + +If there is more than one constructor, by default, mapping will be performed on the constructor with the largest number of parameters. + +Example: + +```csharp +record MultiCtorRecord +{ + public MultiCtorRecord(int myInt) + { + MyInt = myInt; + } + + public MultiCtorRecord(int myInt, string myString) // This constructor will be used + : this(myInt) + { + MyString = myString; + } + +} +``` + +# [v7.4.0](#tab/Records-v7-4-0) + +>[!NOTE] +>Record type must not have a setter and have only one non-empty constructor, and all parameter names must match with properties. + +Otherwise you need to add [`MapToConstructor` configuration](xref:Mapster.Settings.ConstructorMapping#map-to-constructor). + +Example for record types: + +```csharp +class Person { + public string Name { get; } + public int Age { get; } + + public Person(string name, int age) { + this.Name = name; + this.Age = age; + } +} + +var src = new { Name = "Mapster", Age = 3 }; +var target = src.Adapt(); +``` +--- + +### Support additional mapping features: + +| Mapping features | v7.4.0 | v10.0 | +|:-----------------|:------:|:-----:| +|[Custom constructor mapping](xref:Mapster.Settings.ConstructorMapping)| - | ✅ | +|[Ignore](xref:Mapster.Settings.Custom.IgnoringMembers#ignore-extension-method)| - | ✅ | +|[IgnoreNullValues](xref:Mapster.Settings.Custom.IgnoringMembers#ignorenullvalues-extension-method)| - | ✅ | From e34d6599429461bc41cdc146fc0eeeec676dc04d Mon Sep 17 00:00:00 2001 From: DocSvartz Date: Wed, 8 Apr 2026 10:42:39 +0500 Subject: [PATCH 48/80] --Duplicate .\Data-types.md history into .\data-types\Mappable-Objects.md --- .../mapping/{Data-types.md => data-types/Mappable-Objects.md} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename docs/articles/mapping/{Data-types.md => data-types/Mappable-Objects.md} (100%) diff --git a/docs/articles/mapping/Data-types.md b/docs/articles/mapping/data-types/Mappable-Objects.md similarity index 100% rename from docs/articles/mapping/Data-types.md rename to docs/articles/mapping/data-types/Mappable-Objects.md From c075df53470a62dfe0f83534ba09804c77d0f458 Mon Sep 17 00:00:00 2001 From: DocSvartz Date: Wed, 8 Apr 2026 10:42:39 +0500 Subject: [PATCH 49/80] --Restore .\Data-types.md --- docs/articles/mapping/Data-types.md | 192 ++++++++++++++++++++++++++++ 1 file changed, 192 insertions(+) create mode 100644 docs/articles/mapping/Data-types.md diff --git a/docs/articles/mapping/Data-types.md b/docs/articles/mapping/Data-types.md new file mode 100644 index 00000000..c139ee0c --- /dev/null +++ b/docs/articles/mapping/Data-types.md @@ -0,0 +1,192 @@ +--- +uid: Mapster.Mapping.DataTypes +title: "Mapping - Data Types" +--- + +## Primitives + +Converting between primitive types (ie. int, bool, double, decimal) is supported, including when those types are nullable. For all other types, if you can cast types in c#, you can also cast in Mapster. + +```csharp +decimal i = 123.Adapt(); //equal to (decimal)123; +``` + +## Enums + +Mapster maps enums to numerics automatically, but it also maps strings to and from enums automatically in a fast manner. +The default Enum.ToString() in .NET is quite slow. The implementation in Mapster is double the speed. Likewise, a fast conversion from strings to enums is also included. If the string is null or empty, the enum will initialize to the first enum value. + +In Mapster, flagged enums are also supported. + +```csharp +var e = "Read, Write, Delete".Adapt(); +//FileShare.Read | FileShare.Write | FileShare.Delete +``` + +For enum to enum with different type, by default, Mapster will map enum by value. You can override to map enum by name by: + +```csharp +TypeAdapterConfig.GlobalSettings.Default + .EnumMappingStrategy(EnumMappingStrategy.ByName); +``` + +## Strings + +When Mapster maps other types to string, Mapster will use `ToString` method. And whenever Mapster maps string to the other types, Mapster will use `Parse` method. + +```csharp +var s = 123.Adapt(); //equal to 123.ToString(); +var i = "123".Adapt(); //equal to int.Parse("123"); +``` + +## Collections + +This includes mapping among lists, arrays, collections, dictionary including various interfaces: `IList`, `ICollection`, `IEnumerable`, `ISet`, `IDictionary` etc... + +```csharp +var list = db.Pocos.ToList(); +var target = list.Adapt>(); +``` + +## Mappable Objects + +Mapster can map two different objects using the following rules: + +- Source and destination property names are the same. Ex: `dest.Name = src.Name` +- Source has get method. Ex: `dest.Name = src.GetName()` +- Source property has child object which can flatten to destination. Ex: `dest.ContactName = src.Contact.Name` or `dest.Contact_Name = src.Contact.Name` + +Example: + +```csharp +class Staff { + public string Name { get; set; } + public int GetAge() { + return (DateTime.Now - this.BirthDate).TotalDays / 365.25; + } + public Staff Supervisor { get; set; } + ... +} + +struct StaffDto { + public string Name { get; set; } + public int Age { get; set; } + public string SupervisorName { get; set; } +} + +var dto = staff.Adapt(); +//dto.Name = staff.Name, dto.Age = staff.GetAge(), dto.SupervisorName = staff.Supervisor.Name +``` + +**Mappable Object types are included:** + +- POCO classes +- POCO structs +- POCO interfaces +- Dictionary type implement `IDictionary` +- Record types (either class, struct, and interface) + +Example for object to dictionary: + +```csharp +var point = new { X = 2, Y = 3 }; +var dict = point.Adapt>(); +dict["Y"].ShouldBe(3); +``` + +## Record types + +>[!IMPORTANT] +> Mapster treats Record type as an immutable type. +> Only a Nondestructive mutation - creating a new object with modified properties. +> +> ```csharp +> var result = source.adapt(data) +>//equal var result = data with { X = source.X.Adapt(), ...} +>``` + +### Features and Limitations: + +# [v10.0](#tab/Records-v10) + +>[!NOTE] +> By default, all [C# Records](https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/builtin-types/record) are defined as a record type. +> Limitations by count of constructors and constructor parameters used in Mapster version 7.4.0 do not apply. + + +#### Using default value in constuctor param + +If the source type does not contain members that can be used as constructor parameters, then will be used the default values ​​for the parameter type. + +Example: + +```csharp + +class SourceData +{ + public string MyString {get; set;} +} + +record RecordDestination(int myInt, string myString); + +var result = source.Adapt() + +// equal var result = new RecordDestination (default(int),source.myString) + +``` + +#### MultiConstructor Record types + +If there is more than one constructor, by default, mapping will be performed on the constructor with the largest number of parameters. + +Example: + +```csharp +record MultiCtorRecord +{ + public MultiCtorRecord(int myInt) + { + MyInt = myInt; + } + + public MultiCtorRecord(int myInt, string myString) // This constructor will be used + : this(myInt) + { + MyString = myString; + } + +} +``` + +# [v7.4.0](#tab/Records-v7-4-0) + +>[!NOTE] +>Record type must not have a setter and have only one non-empty constructor, and all parameter names must match with properties. + +Otherwise you need to add [`MapToConstructor` configuration](xref:Mapster.Settings.ConstructorMapping#map-to-constructor). + +Example for record types: + +```csharp +class Person { + public string Name { get; } + public int Age { get; } + + public Person(string name, int age) { + this.Name = name; + this.Age = age; + } +} + +var src = new { Name = "Mapster", Age = 3 }; +var target = src.Adapt(); +``` +--- + +### Support additional mapping features: + +| Mapping features | v7.4.0 | v10.0 | +|:-----------------|:------:|:-----:| +|[Custom constructor mapping](xref:Mapster.Settings.ConstructorMapping)| - | ✅ | +|[Ignore](xref:Mapster.Settings.Custom.IgnoringMembers#ignore-extension-method)| - | ✅ | +|[IgnoreNullValues](xref:Mapster.Settings.Custom.IgnoringMembers#ignorenullvalues-extension-method)| - | ✅ | From 15904682d87b5760a4204cdc1e99dd83af8c7d69 Mon Sep 17 00:00:00 2001 From: DocSvartz Date: Wed, 8 Apr 2026 10:42:56 +0500 Subject: [PATCH 50/80] --Duplicate .\Data-types.md history into .\data-types\Collections.md --- .../articles/mapping/{Data-types.md => data-types/Collections.md} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename docs/articles/mapping/{Data-types.md => data-types/Collections.md} (100%) diff --git a/docs/articles/mapping/Data-types.md b/docs/articles/mapping/data-types/Collections.md similarity index 100% rename from docs/articles/mapping/Data-types.md rename to docs/articles/mapping/data-types/Collections.md From 7edece503850516b415444a3fccfba5145fc6bf1 Mon Sep 17 00:00:00 2001 From: DocSvartz Date: Wed, 8 Apr 2026 10:42:56 +0500 Subject: [PATCH 51/80] --Restore .\Data-types.md --- docs/articles/mapping/Data-types.md | 192 ++++++++++++++++++++++++++++ 1 file changed, 192 insertions(+) create mode 100644 docs/articles/mapping/Data-types.md diff --git a/docs/articles/mapping/Data-types.md b/docs/articles/mapping/Data-types.md new file mode 100644 index 00000000..c139ee0c --- /dev/null +++ b/docs/articles/mapping/Data-types.md @@ -0,0 +1,192 @@ +--- +uid: Mapster.Mapping.DataTypes +title: "Mapping - Data Types" +--- + +## Primitives + +Converting between primitive types (ie. int, bool, double, decimal) is supported, including when those types are nullable. For all other types, if you can cast types in c#, you can also cast in Mapster. + +```csharp +decimal i = 123.Adapt(); //equal to (decimal)123; +``` + +## Enums + +Mapster maps enums to numerics automatically, but it also maps strings to and from enums automatically in a fast manner. +The default Enum.ToString() in .NET is quite slow. The implementation in Mapster is double the speed. Likewise, a fast conversion from strings to enums is also included. If the string is null or empty, the enum will initialize to the first enum value. + +In Mapster, flagged enums are also supported. + +```csharp +var e = "Read, Write, Delete".Adapt(); +//FileShare.Read | FileShare.Write | FileShare.Delete +``` + +For enum to enum with different type, by default, Mapster will map enum by value. You can override to map enum by name by: + +```csharp +TypeAdapterConfig.GlobalSettings.Default + .EnumMappingStrategy(EnumMappingStrategy.ByName); +``` + +## Strings + +When Mapster maps other types to string, Mapster will use `ToString` method. And whenever Mapster maps string to the other types, Mapster will use `Parse` method. + +```csharp +var s = 123.Adapt(); //equal to 123.ToString(); +var i = "123".Adapt(); //equal to int.Parse("123"); +``` + +## Collections + +This includes mapping among lists, arrays, collections, dictionary including various interfaces: `IList`, `ICollection`, `IEnumerable`, `ISet`, `IDictionary` etc... + +```csharp +var list = db.Pocos.ToList(); +var target = list.Adapt>(); +``` + +## Mappable Objects + +Mapster can map two different objects using the following rules: + +- Source and destination property names are the same. Ex: `dest.Name = src.Name` +- Source has get method. Ex: `dest.Name = src.GetName()` +- Source property has child object which can flatten to destination. Ex: `dest.ContactName = src.Contact.Name` or `dest.Contact_Name = src.Contact.Name` + +Example: + +```csharp +class Staff { + public string Name { get; set; } + public int GetAge() { + return (DateTime.Now - this.BirthDate).TotalDays / 365.25; + } + public Staff Supervisor { get; set; } + ... +} + +struct StaffDto { + public string Name { get; set; } + public int Age { get; set; } + public string SupervisorName { get; set; } +} + +var dto = staff.Adapt(); +//dto.Name = staff.Name, dto.Age = staff.GetAge(), dto.SupervisorName = staff.Supervisor.Name +``` + +**Mappable Object types are included:** + +- POCO classes +- POCO structs +- POCO interfaces +- Dictionary type implement `IDictionary` +- Record types (either class, struct, and interface) + +Example for object to dictionary: + +```csharp +var point = new { X = 2, Y = 3 }; +var dict = point.Adapt>(); +dict["Y"].ShouldBe(3); +``` + +## Record types + +>[!IMPORTANT] +> Mapster treats Record type as an immutable type. +> Only a Nondestructive mutation - creating a new object with modified properties. +> +> ```csharp +> var result = source.adapt(data) +>//equal var result = data with { X = source.X.Adapt(), ...} +>``` + +### Features and Limitations: + +# [v10.0](#tab/Records-v10) + +>[!NOTE] +> By default, all [C# Records](https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/builtin-types/record) are defined as a record type. +> Limitations by count of constructors and constructor parameters used in Mapster version 7.4.0 do not apply. + + +#### Using default value in constuctor param + +If the source type does not contain members that can be used as constructor parameters, then will be used the default values ​​for the parameter type. + +Example: + +```csharp + +class SourceData +{ + public string MyString {get; set;} +} + +record RecordDestination(int myInt, string myString); + +var result = source.Adapt() + +// equal var result = new RecordDestination (default(int),source.myString) + +``` + +#### MultiConstructor Record types + +If there is more than one constructor, by default, mapping will be performed on the constructor with the largest number of parameters. + +Example: + +```csharp +record MultiCtorRecord +{ + public MultiCtorRecord(int myInt) + { + MyInt = myInt; + } + + public MultiCtorRecord(int myInt, string myString) // This constructor will be used + : this(myInt) + { + MyString = myString; + } + +} +``` + +# [v7.4.0](#tab/Records-v7-4-0) + +>[!NOTE] +>Record type must not have a setter and have only one non-empty constructor, and all parameter names must match with properties. + +Otherwise you need to add [`MapToConstructor` configuration](xref:Mapster.Settings.ConstructorMapping#map-to-constructor). + +Example for record types: + +```csharp +class Person { + public string Name { get; } + public int Age { get; } + + public Person(string name, int age) { + this.Name = name; + this.Age = age; + } +} + +var src = new { Name = "Mapster", Age = 3 }; +var target = src.Adapt(); +``` +--- + +### Support additional mapping features: + +| Mapping features | v7.4.0 | v10.0 | +|:-----------------|:------:|:-----:| +|[Custom constructor mapping](xref:Mapster.Settings.ConstructorMapping)| - | ✅ | +|[Ignore](xref:Mapster.Settings.Custom.IgnoringMembers#ignore-extension-method)| - | ✅ | +|[IgnoreNullValues](xref:Mapster.Settings.Custom.IgnoringMembers#ignorenullvalues-extension-method)| - | ✅ | From 7cb1f6d42b6b8d569c8a19a2968d60764ce9ee60 Mon Sep 17 00:00:00 2001 From: DocSvartz Date: Wed, 8 Apr 2026 10:47:47 +0500 Subject: [PATCH 52/80] fix(docs): copy data to new articles --- .../mapping/data-types/Collections.md | 185 +---------------- .../mapping/data-types/Mappable-Objects.md | 146 +------------ .../mapping/data-types/Mapping-types.md | 192 ------------------ .../mapping/data-types/Primitive-types.md | 158 +------------- .../mapping/data-types/Record-types.md | 95 +-------- 5 files changed, 10 insertions(+), 766 deletions(-) delete mode 100644 docs/articles/mapping/data-types/Mapping-types.md diff --git a/docs/articles/mapping/data-types/Collections.md b/docs/articles/mapping/data-types/Collections.md index c139ee0c..a3d2d669 100644 --- a/docs/articles/mapping/data-types/Collections.md +++ b/docs/articles/mapping/data-types/Collections.md @@ -1,44 +1,8 @@ --- -uid: Mapster.Mapping.DataTypes -title: "Mapping - Data Types" +uid: Mapster.Mapping.DataTypes.Collections +title: "Mapping - Collections" --- -## Primitives - -Converting between primitive types (ie. int, bool, double, decimal) is supported, including when those types are nullable. For all other types, if you can cast types in c#, you can also cast in Mapster. - -```csharp -decimal i = 123.Adapt(); //equal to (decimal)123; -``` - -## Enums - -Mapster maps enums to numerics automatically, but it also maps strings to and from enums automatically in a fast manner. -The default Enum.ToString() in .NET is quite slow. The implementation in Mapster is double the speed. Likewise, a fast conversion from strings to enums is also included. If the string is null or empty, the enum will initialize to the first enum value. - -In Mapster, flagged enums are also supported. - -```csharp -var e = "Read, Write, Delete".Adapt(); -//FileShare.Read | FileShare.Write | FileShare.Delete -``` - -For enum to enum with different type, by default, Mapster will map enum by value. You can override to map enum by name by: - -```csharp -TypeAdapterConfig.GlobalSettings.Default - .EnumMappingStrategy(EnumMappingStrategy.ByName); -``` - -## Strings - -When Mapster maps other types to string, Mapster will use `ToString` method. And whenever Mapster maps string to the other types, Mapster will use `Parse` method. - -```csharp -var s = 123.Adapt(); //equal to 123.ToString(); -var i = "123".Adapt(); //equal to int.Parse("123"); -``` - ## Collections This includes mapping among lists, arrays, collections, dictionary including various interfaces: `IList`, `ICollection`, `IEnumerable`, `ISet`, `IDictionary` etc... @@ -46,147 +10,4 @@ This includes mapping among lists, arrays, collections, dictionary including var ```csharp var list = db.Pocos.ToList(); var target = list.Adapt>(); -``` - -## Mappable Objects - -Mapster can map two different objects using the following rules: - -- Source and destination property names are the same. Ex: `dest.Name = src.Name` -- Source has get method. Ex: `dest.Name = src.GetName()` -- Source property has child object which can flatten to destination. Ex: `dest.ContactName = src.Contact.Name` or `dest.Contact_Name = src.Contact.Name` - -Example: - -```csharp -class Staff { - public string Name { get; set; } - public int GetAge() { - return (DateTime.Now - this.BirthDate).TotalDays / 365.25; - } - public Staff Supervisor { get; set; } - ... -} - -struct StaffDto { - public string Name { get; set; } - public int Age { get; set; } - public string SupervisorName { get; set; } -} - -var dto = staff.Adapt(); -//dto.Name = staff.Name, dto.Age = staff.GetAge(), dto.SupervisorName = staff.Supervisor.Name -``` - -**Mappable Object types are included:** - -- POCO classes -- POCO structs -- POCO interfaces -- Dictionary type implement `IDictionary` -- Record types (either class, struct, and interface) - -Example for object to dictionary: - -```csharp -var point = new { X = 2, Y = 3 }; -var dict = point.Adapt>(); -dict["Y"].ShouldBe(3); -``` - -## Record types - ->[!IMPORTANT] -> Mapster treats Record type as an immutable type. -> Only a Nondestructive mutation - creating a new object with modified properties. -> -> ```csharp -> var result = source.adapt(data) ->//equal var result = data with { X = source.X.Adapt(), ...} ->``` - -### Features and Limitations: - -# [v10.0](#tab/Records-v10) - ->[!NOTE] -> By default, all [C# Records](https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/builtin-types/record) are defined as a record type. -> Limitations by count of constructors and constructor parameters used in Mapster version 7.4.0 do not apply. - - -#### Using default value in constuctor param - -If the source type does not contain members that can be used as constructor parameters, then will be used the default values ​​for the parameter type. - -Example: - -```csharp - -class SourceData -{ - public string MyString {get; set;} -} - -record RecordDestination(int myInt, string myString); - -var result = source.Adapt() - -// equal var result = new RecordDestination (default(int),source.myString) - -``` - -#### MultiConstructor Record types - -If there is more than one constructor, by default, mapping will be performed on the constructor with the largest number of parameters. - -Example: - -```csharp -record MultiCtorRecord -{ - public MultiCtorRecord(int myInt) - { - MyInt = myInt; - } - - public MultiCtorRecord(int myInt, string myString) // This constructor will be used - : this(myInt) - { - MyString = myString; - } - -} -``` - -# [v7.4.0](#tab/Records-v7-4-0) - ->[!NOTE] ->Record type must not have a setter and have only one non-empty constructor, and all parameter names must match with properties. - -Otherwise you need to add [`MapToConstructor` configuration](xref:Mapster.Settings.ConstructorMapping#map-to-constructor). - -Example for record types: - -```csharp -class Person { - public string Name { get; } - public int Age { get; } - - public Person(string name, int age) { - this.Name = name; - this.Age = age; - } -} - -var src = new { Name = "Mapster", Age = 3 }; -var target = src.Adapt(); -``` ---- - -### Support additional mapping features: - -| Mapping features | v7.4.0 | v10.0 | -|:-----------------|:------:|:-----:| -|[Custom constructor mapping](xref:Mapster.Settings.ConstructorMapping)| - | ✅ | -|[Ignore](xref:Mapster.Settings.Custom.IgnoringMembers#ignore-extension-method)| - | ✅ | -|[IgnoreNullValues](xref:Mapster.Settings.Custom.IgnoringMembers#ignorenullvalues-extension-method)| - | ✅ | +``` \ No newline at end of file diff --git a/docs/articles/mapping/data-types/Mappable-Objects.md b/docs/articles/mapping/data-types/Mappable-Objects.md index c139ee0c..73c059d0 100644 --- a/docs/articles/mapping/data-types/Mappable-Objects.md +++ b/docs/articles/mapping/data-types/Mappable-Objects.md @@ -1,53 +1,8 @@ --- -uid: Mapster.Mapping.DataTypes -title: "Mapping - Data Types" +uid: Mapster.Mapping.DataTypes.Overview +title: "Mapping - Mappable Objects" --- -## Primitives - -Converting between primitive types (ie. int, bool, double, decimal) is supported, including when those types are nullable. For all other types, if you can cast types in c#, you can also cast in Mapster. - -```csharp -decimal i = 123.Adapt(); //equal to (decimal)123; -``` - -## Enums - -Mapster maps enums to numerics automatically, but it also maps strings to and from enums automatically in a fast manner. -The default Enum.ToString() in .NET is quite slow. The implementation in Mapster is double the speed. Likewise, a fast conversion from strings to enums is also included. If the string is null or empty, the enum will initialize to the first enum value. - -In Mapster, flagged enums are also supported. - -```csharp -var e = "Read, Write, Delete".Adapt(); -//FileShare.Read | FileShare.Write | FileShare.Delete -``` - -For enum to enum with different type, by default, Mapster will map enum by value. You can override to map enum by name by: - -```csharp -TypeAdapterConfig.GlobalSettings.Default - .EnumMappingStrategy(EnumMappingStrategy.ByName); -``` - -## Strings - -When Mapster maps other types to string, Mapster will use `ToString` method. And whenever Mapster maps string to the other types, Mapster will use `Parse` method. - -```csharp -var s = 123.Adapt(); //equal to 123.ToString(); -var i = "123".Adapt(); //equal to int.Parse("123"); -``` - -## Collections - -This includes mapping among lists, arrays, collections, dictionary including various interfaces: `IList`, `ICollection`, `IEnumerable`, `ISet`, `IDictionary` etc... - -```csharp -var list = db.Pocos.ToList(); -var target = list.Adapt>(); -``` - ## Mappable Objects Mapster can map two different objects using the following rules: @@ -93,100 +48,3 @@ var point = new { X = 2, Y = 3 }; var dict = point.Adapt>(); dict["Y"].ShouldBe(3); ``` - -## Record types - ->[!IMPORTANT] -> Mapster treats Record type as an immutable type. -> Only a Nondestructive mutation - creating a new object with modified properties. -> -> ```csharp -> var result = source.adapt(data) ->//equal var result = data with { X = source.X.Adapt(), ...} ->``` - -### Features and Limitations: - -# [v10.0](#tab/Records-v10) - ->[!NOTE] -> By default, all [C# Records](https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/builtin-types/record) are defined as a record type. -> Limitations by count of constructors and constructor parameters used in Mapster version 7.4.0 do not apply. - - -#### Using default value in constuctor param - -If the source type does not contain members that can be used as constructor parameters, then will be used the default values ​​for the parameter type. - -Example: - -```csharp - -class SourceData -{ - public string MyString {get; set;} -} - -record RecordDestination(int myInt, string myString); - -var result = source.Adapt() - -// equal var result = new RecordDestination (default(int),source.myString) - -``` - -#### MultiConstructor Record types - -If there is more than one constructor, by default, mapping will be performed on the constructor with the largest number of parameters. - -Example: - -```csharp -record MultiCtorRecord -{ - public MultiCtorRecord(int myInt) - { - MyInt = myInt; - } - - public MultiCtorRecord(int myInt, string myString) // This constructor will be used - : this(myInt) - { - MyString = myString; - } - -} -``` - -# [v7.4.0](#tab/Records-v7-4-0) - ->[!NOTE] ->Record type must not have a setter and have only one non-empty constructor, and all parameter names must match with properties. - -Otherwise you need to add [`MapToConstructor` configuration](xref:Mapster.Settings.ConstructorMapping#map-to-constructor). - -Example for record types: - -```csharp -class Person { - public string Name { get; } - public int Age { get; } - - public Person(string name, int age) { - this.Name = name; - this.Age = age; - } -} - -var src = new { Name = "Mapster", Age = 3 }; -var target = src.Adapt(); -``` ---- - -### Support additional mapping features: - -| Mapping features | v7.4.0 | v10.0 | -|:-----------------|:------:|:-----:| -|[Custom constructor mapping](xref:Mapster.Settings.ConstructorMapping)| - | ✅ | -|[Ignore](xref:Mapster.Settings.Custom.IgnoringMembers#ignore-extension-method)| - | ✅ | -|[IgnoreNullValues](xref:Mapster.Settings.Custom.IgnoringMembers#ignorenullvalues-extension-method)| - | ✅ | diff --git a/docs/articles/mapping/data-types/Mapping-types.md b/docs/articles/mapping/data-types/Mapping-types.md deleted file mode 100644 index c139ee0c..00000000 --- a/docs/articles/mapping/data-types/Mapping-types.md +++ /dev/null @@ -1,192 +0,0 @@ ---- -uid: Mapster.Mapping.DataTypes -title: "Mapping - Data Types" ---- - -## Primitives - -Converting between primitive types (ie. int, bool, double, decimal) is supported, including when those types are nullable. For all other types, if you can cast types in c#, you can also cast in Mapster. - -```csharp -decimal i = 123.Adapt(); //equal to (decimal)123; -``` - -## Enums - -Mapster maps enums to numerics automatically, but it also maps strings to and from enums automatically in a fast manner. -The default Enum.ToString() in .NET is quite slow. The implementation in Mapster is double the speed. Likewise, a fast conversion from strings to enums is also included. If the string is null or empty, the enum will initialize to the first enum value. - -In Mapster, flagged enums are also supported. - -```csharp -var e = "Read, Write, Delete".Adapt(); -//FileShare.Read | FileShare.Write | FileShare.Delete -``` - -For enum to enum with different type, by default, Mapster will map enum by value. You can override to map enum by name by: - -```csharp -TypeAdapterConfig.GlobalSettings.Default - .EnumMappingStrategy(EnumMappingStrategy.ByName); -``` - -## Strings - -When Mapster maps other types to string, Mapster will use `ToString` method. And whenever Mapster maps string to the other types, Mapster will use `Parse` method. - -```csharp -var s = 123.Adapt(); //equal to 123.ToString(); -var i = "123".Adapt(); //equal to int.Parse("123"); -``` - -## Collections - -This includes mapping among lists, arrays, collections, dictionary including various interfaces: `IList`, `ICollection`, `IEnumerable`, `ISet`, `IDictionary` etc... - -```csharp -var list = db.Pocos.ToList(); -var target = list.Adapt>(); -``` - -## Mappable Objects - -Mapster can map two different objects using the following rules: - -- Source and destination property names are the same. Ex: `dest.Name = src.Name` -- Source has get method. Ex: `dest.Name = src.GetName()` -- Source property has child object which can flatten to destination. Ex: `dest.ContactName = src.Contact.Name` or `dest.Contact_Name = src.Contact.Name` - -Example: - -```csharp -class Staff { - public string Name { get; set; } - public int GetAge() { - return (DateTime.Now - this.BirthDate).TotalDays / 365.25; - } - public Staff Supervisor { get; set; } - ... -} - -struct StaffDto { - public string Name { get; set; } - public int Age { get; set; } - public string SupervisorName { get; set; } -} - -var dto = staff.Adapt(); -//dto.Name = staff.Name, dto.Age = staff.GetAge(), dto.SupervisorName = staff.Supervisor.Name -``` - -**Mappable Object types are included:** - -- POCO classes -- POCO structs -- POCO interfaces -- Dictionary type implement `IDictionary` -- Record types (either class, struct, and interface) - -Example for object to dictionary: - -```csharp -var point = new { X = 2, Y = 3 }; -var dict = point.Adapt>(); -dict["Y"].ShouldBe(3); -``` - -## Record types - ->[!IMPORTANT] -> Mapster treats Record type as an immutable type. -> Only a Nondestructive mutation - creating a new object with modified properties. -> -> ```csharp -> var result = source.adapt(data) ->//equal var result = data with { X = source.X.Adapt(), ...} ->``` - -### Features and Limitations: - -# [v10.0](#tab/Records-v10) - ->[!NOTE] -> By default, all [C# Records](https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/builtin-types/record) are defined as a record type. -> Limitations by count of constructors and constructor parameters used in Mapster version 7.4.0 do not apply. - - -#### Using default value in constuctor param - -If the source type does not contain members that can be used as constructor parameters, then will be used the default values ​​for the parameter type. - -Example: - -```csharp - -class SourceData -{ - public string MyString {get; set;} -} - -record RecordDestination(int myInt, string myString); - -var result = source.Adapt() - -// equal var result = new RecordDestination (default(int),source.myString) - -``` - -#### MultiConstructor Record types - -If there is more than one constructor, by default, mapping will be performed on the constructor with the largest number of parameters. - -Example: - -```csharp -record MultiCtorRecord -{ - public MultiCtorRecord(int myInt) - { - MyInt = myInt; - } - - public MultiCtorRecord(int myInt, string myString) // This constructor will be used - : this(myInt) - { - MyString = myString; - } - -} -``` - -# [v7.4.0](#tab/Records-v7-4-0) - ->[!NOTE] ->Record type must not have a setter and have only one non-empty constructor, and all parameter names must match with properties. - -Otherwise you need to add [`MapToConstructor` configuration](xref:Mapster.Settings.ConstructorMapping#map-to-constructor). - -Example for record types: - -```csharp -class Person { - public string Name { get; } - public int Age { get; } - - public Person(string name, int age) { - this.Name = name; - this.Age = age; - } -} - -var src = new { Name = "Mapster", Age = 3 }; -var target = src.Adapt(); -``` ---- - -### Support additional mapping features: - -| Mapping features | v7.4.0 | v10.0 | -|:-----------------|:------:|:-----:| -|[Custom constructor mapping](xref:Mapster.Settings.ConstructorMapping)| - | ✅ | -|[Ignore](xref:Mapster.Settings.Custom.IgnoringMembers#ignore-extension-method)| - | ✅ | -|[IgnoreNullValues](xref:Mapster.Settings.Custom.IgnoringMembers#ignorenullvalues-extension-method)| - | ✅ | diff --git a/docs/articles/mapping/data-types/Primitive-types.md b/docs/articles/mapping/data-types/Primitive-types.md index c139ee0c..00750bbe 100644 --- a/docs/articles/mapping/data-types/Primitive-types.md +++ b/docs/articles/mapping/data-types/Primitive-types.md @@ -1,6 +1,6 @@ --- -uid: Mapster.Mapping.DataTypes -title: "Mapping - Data Types" +uid: Mapster.Mapping.DataTypes.Primitives +title: "Mapping - Primitive Types" --- ## Primitives @@ -37,156 +37,4 @@ When Mapster maps other types to string, Mapster will use `ToString` method. And ```csharp var s = 123.Adapt(); //equal to 123.ToString(); var i = "123".Adapt(); //equal to int.Parse("123"); -``` - -## Collections - -This includes mapping among lists, arrays, collections, dictionary including various interfaces: `IList`, `ICollection`, `IEnumerable`, `ISet`, `IDictionary` etc... - -```csharp -var list = db.Pocos.ToList(); -var target = list.Adapt>(); -``` - -## Mappable Objects - -Mapster can map two different objects using the following rules: - -- Source and destination property names are the same. Ex: `dest.Name = src.Name` -- Source has get method. Ex: `dest.Name = src.GetName()` -- Source property has child object which can flatten to destination. Ex: `dest.ContactName = src.Contact.Name` or `dest.Contact_Name = src.Contact.Name` - -Example: - -```csharp -class Staff { - public string Name { get; set; } - public int GetAge() { - return (DateTime.Now - this.BirthDate).TotalDays / 365.25; - } - public Staff Supervisor { get; set; } - ... -} - -struct StaffDto { - public string Name { get; set; } - public int Age { get; set; } - public string SupervisorName { get; set; } -} - -var dto = staff.Adapt(); -//dto.Name = staff.Name, dto.Age = staff.GetAge(), dto.SupervisorName = staff.Supervisor.Name -``` - -**Mappable Object types are included:** - -- POCO classes -- POCO structs -- POCO interfaces -- Dictionary type implement `IDictionary` -- Record types (either class, struct, and interface) - -Example for object to dictionary: - -```csharp -var point = new { X = 2, Y = 3 }; -var dict = point.Adapt>(); -dict["Y"].ShouldBe(3); -``` - -## Record types - ->[!IMPORTANT] -> Mapster treats Record type as an immutable type. -> Only a Nondestructive mutation - creating a new object with modified properties. -> -> ```csharp -> var result = source.adapt(data) ->//equal var result = data with { X = source.X.Adapt(), ...} ->``` - -### Features and Limitations: - -# [v10.0](#tab/Records-v10) - ->[!NOTE] -> By default, all [C# Records](https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/builtin-types/record) are defined as a record type. -> Limitations by count of constructors and constructor parameters used in Mapster version 7.4.0 do not apply. - - -#### Using default value in constuctor param - -If the source type does not contain members that can be used as constructor parameters, then will be used the default values ​​for the parameter type. - -Example: - -```csharp - -class SourceData -{ - public string MyString {get; set;} -} - -record RecordDestination(int myInt, string myString); - -var result = source.Adapt() - -// equal var result = new RecordDestination (default(int),source.myString) - -``` - -#### MultiConstructor Record types - -If there is more than one constructor, by default, mapping will be performed on the constructor with the largest number of parameters. - -Example: - -```csharp -record MultiCtorRecord -{ - public MultiCtorRecord(int myInt) - { - MyInt = myInt; - } - - public MultiCtorRecord(int myInt, string myString) // This constructor will be used - : this(myInt) - { - MyString = myString; - } - -} -``` - -# [v7.4.0](#tab/Records-v7-4-0) - ->[!NOTE] ->Record type must not have a setter and have only one non-empty constructor, and all parameter names must match with properties. - -Otherwise you need to add [`MapToConstructor` configuration](xref:Mapster.Settings.ConstructorMapping#map-to-constructor). - -Example for record types: - -```csharp -class Person { - public string Name { get; } - public int Age { get; } - - public Person(string name, int age) { - this.Name = name; - this.Age = age; - } -} - -var src = new { Name = "Mapster", Age = 3 }; -var target = src.Adapt(); -``` ---- - -### Support additional mapping features: - -| Mapping features | v7.4.0 | v10.0 | -|:-----------------|:------:|:-----:| -|[Custom constructor mapping](xref:Mapster.Settings.ConstructorMapping)| - | ✅ | -|[Ignore](xref:Mapster.Settings.Custom.IgnoringMembers#ignore-extension-method)| - | ✅ | -|[IgnoreNullValues](xref:Mapster.Settings.Custom.IgnoringMembers#ignorenullvalues-extension-method)| - | ✅ | +``` \ No newline at end of file diff --git a/docs/articles/mapping/data-types/Record-types.md b/docs/articles/mapping/data-types/Record-types.md index c139ee0c..c3c1734a 100644 --- a/docs/articles/mapping/data-types/Record-types.md +++ b/docs/articles/mapping/data-types/Record-types.md @@ -1,99 +1,8 @@ --- -uid: Mapster.Mapping.DataTypes -title: "Mapping - Data Types" +uid: Mapster.Mapping.DataTypes.Records +title: "Mapping - Record Types" --- -## Primitives - -Converting between primitive types (ie. int, bool, double, decimal) is supported, including when those types are nullable. For all other types, if you can cast types in c#, you can also cast in Mapster. - -```csharp -decimal i = 123.Adapt(); //equal to (decimal)123; -``` - -## Enums - -Mapster maps enums to numerics automatically, but it also maps strings to and from enums automatically in a fast manner. -The default Enum.ToString() in .NET is quite slow. The implementation in Mapster is double the speed. Likewise, a fast conversion from strings to enums is also included. If the string is null or empty, the enum will initialize to the first enum value. - -In Mapster, flagged enums are also supported. - -```csharp -var e = "Read, Write, Delete".Adapt(); -//FileShare.Read | FileShare.Write | FileShare.Delete -``` - -For enum to enum with different type, by default, Mapster will map enum by value. You can override to map enum by name by: - -```csharp -TypeAdapterConfig.GlobalSettings.Default - .EnumMappingStrategy(EnumMappingStrategy.ByName); -``` - -## Strings - -When Mapster maps other types to string, Mapster will use `ToString` method. And whenever Mapster maps string to the other types, Mapster will use `Parse` method. - -```csharp -var s = 123.Adapt(); //equal to 123.ToString(); -var i = "123".Adapt(); //equal to int.Parse("123"); -``` - -## Collections - -This includes mapping among lists, arrays, collections, dictionary including various interfaces: `IList`, `ICollection`, `IEnumerable`, `ISet`, `IDictionary` etc... - -```csharp -var list = db.Pocos.ToList(); -var target = list.Adapt>(); -``` - -## Mappable Objects - -Mapster can map two different objects using the following rules: - -- Source and destination property names are the same. Ex: `dest.Name = src.Name` -- Source has get method. Ex: `dest.Name = src.GetName()` -- Source property has child object which can flatten to destination. Ex: `dest.ContactName = src.Contact.Name` or `dest.Contact_Name = src.Contact.Name` - -Example: - -```csharp -class Staff { - public string Name { get; set; } - public int GetAge() { - return (DateTime.Now - this.BirthDate).TotalDays / 365.25; - } - public Staff Supervisor { get; set; } - ... -} - -struct StaffDto { - public string Name { get; set; } - public int Age { get; set; } - public string SupervisorName { get; set; } -} - -var dto = staff.Adapt(); -//dto.Name = staff.Name, dto.Age = staff.GetAge(), dto.SupervisorName = staff.Supervisor.Name -``` - -**Mappable Object types are included:** - -- POCO classes -- POCO structs -- POCO interfaces -- Dictionary type implement `IDictionary` -- Record types (either class, struct, and interface) - -Example for object to dictionary: - -```csharp -var point = new { X = 2, Y = 3 }; -var dict = point.Adapt>(); -dict["Y"].ShouldBe(3); -``` - ## Record types >[!IMPORTANT] From d3bc57831df667b8de42a2e1359e956202c17527 Mon Sep 17 00:00:00 2001 From: DocSvartz Date: Wed, 8 Apr 2026 10:57:28 +0500 Subject: [PATCH 53/80] fix(docs): fix links --- docs/articles/mapping/data-types/toc.yml | 13 +++++++++++++ docs/articles/mapping/toc.yml | 4 ++-- 2 files changed, 15 insertions(+), 2 deletions(-) create mode 100644 docs/articles/mapping/data-types/toc.yml diff --git a/docs/articles/mapping/data-types/toc.yml b/docs/articles/mapping/data-types/toc.yml new file mode 100644 index 00000000..cac93d44 --- /dev/null +++ b/docs/articles/mapping/data-types/toc.yml @@ -0,0 +1,13 @@ +# yaml-language-server: $schema=https://raw.githubusercontent.com/dotnet/docfx/main/schemas/toc.schema.json +- name: Mappable Objects + uid: Mapster.Mapping.DataTypes.Overview + href: Mappable-Objects.md +- name: Primitive Types + uid: Mapster.Mapping.DataTypes.Primitives + href: Primitive-types.md +- name: Collections + uid: Mapster.Mapping.DataTypes.Collections + href: Collections.md +- name: Record Types + uid: Mapster.Mapping.DataTypes.Records + href: Record-types.md \ No newline at end of file diff --git a/docs/articles/mapping/toc.yml b/docs/articles/mapping/toc.yml index ab7e7755..b86d614c 100644 --- a/docs/articles/mapping/toc.yml +++ b/docs/articles/mapping/toc.yml @@ -6,8 +6,8 @@ uid: Mapster.Mapping.Mappers href: Mappers.md - name: Data types - uid: Mapster.Mapping.DataTypes - href: Data-types.md + href: data-types/toc.yml + topicHref: xref:Mapster.Mapping.DataTypes - name: Mapping with interface uid: Mapster.Mapping.IMapFromInterface href: Mapping-Configuration-With-IMapFrom-Interface.md \ No newline at end of file From 966589e82f253ff282c374bbaf1c1836288e9168 Mon Sep 17 00:00:00 2001 From: DocSvartz Date: Wed, 8 Apr 2026 11:09:26 +0500 Subject: [PATCH 54/80] fix(docs): drop Data-types.md and fix links --- docs/api/Reference.md | 4 +- docs/articles/mapping/Data-types.md | 192 ---------------------------- docs/articles/mapping/toc.yml | 1 - 3 files changed, 2 insertions(+), 195 deletions(-) delete mode 100644 docs/articles/mapping/Data-types.md diff --git a/docs/api/Reference.md b/docs/api/Reference.md index 9626ce24..bcfe97ed 100644 --- a/docs/api/Reference.md +++ b/docs/api/Reference.md @@ -11,7 +11,7 @@ uid: Mapster.References | `src.Adapt()` | Mapping to new type | [basic](xref:Mapster.Mapping.BasicUsages) | | `src.Adapt(dest)` | Mapping to existing object | [basic](xref:Mapster.Mapping.BasicUsages) | | `query.ProjectToType()` | Mapping from queryable | [basic](xref:Mapster.Mapping.BasicUsages) | -| | Convention & Data type support | [data types](xref:Mapster.Mapping.DataTypes) | +| | Convention & Data type support | [data types](xref:Mapster.Mapping.DataTypes.Overview) | ### Mapper instance (for dependency injection) @@ -84,7 +84,7 @@ uid: Mapster.References | `BeforeMapping` | Add steps before mapping start | | [before-after](xref:Mapster.Settings.BeforeAfterMapping) | | `ConstructUsing` | Define how to create object | x | [constructor](xref:Mapster.Settings.ConstructorMapping) | | `EnableNonPublicMembers` | Mapping non-public properties | | [non-public](xref:Mapster.Settings.Custom.NonPublicMembers) | -| `EnumMappingStrategy` | Choose whether mapping enum by value or by name | | [data types](xref:Mapster.Mapping.DataTypes) | +| `EnumMappingStrategy` | Choose whether mapping enum by value or by name | | [data types](xref:Mapster.Mapping.DataTypes.Primitives) | | `Fork` | Add new settings without side effect on main config | x | [nested mapping](xref:Mapster.Configuration.NestedMapping) | | `GetMemberName` | Define how to resolve property name | x | [custom naming](xref:Mapster.Settings.Custom.NamingConvention) | | `Ignore` | Ignore specific properties | x | [ignore](xref:Mapster.Settings.Custom.IgnoringMembers) | diff --git a/docs/articles/mapping/Data-types.md b/docs/articles/mapping/Data-types.md deleted file mode 100644 index c139ee0c..00000000 --- a/docs/articles/mapping/Data-types.md +++ /dev/null @@ -1,192 +0,0 @@ ---- -uid: Mapster.Mapping.DataTypes -title: "Mapping - Data Types" ---- - -## Primitives - -Converting between primitive types (ie. int, bool, double, decimal) is supported, including when those types are nullable. For all other types, if you can cast types in c#, you can also cast in Mapster. - -```csharp -decimal i = 123.Adapt(); //equal to (decimal)123; -``` - -## Enums - -Mapster maps enums to numerics automatically, but it also maps strings to and from enums automatically in a fast manner. -The default Enum.ToString() in .NET is quite slow. The implementation in Mapster is double the speed. Likewise, a fast conversion from strings to enums is also included. If the string is null or empty, the enum will initialize to the first enum value. - -In Mapster, flagged enums are also supported. - -```csharp -var e = "Read, Write, Delete".Adapt(); -//FileShare.Read | FileShare.Write | FileShare.Delete -``` - -For enum to enum with different type, by default, Mapster will map enum by value. You can override to map enum by name by: - -```csharp -TypeAdapterConfig.GlobalSettings.Default - .EnumMappingStrategy(EnumMappingStrategy.ByName); -``` - -## Strings - -When Mapster maps other types to string, Mapster will use `ToString` method. And whenever Mapster maps string to the other types, Mapster will use `Parse` method. - -```csharp -var s = 123.Adapt(); //equal to 123.ToString(); -var i = "123".Adapt(); //equal to int.Parse("123"); -``` - -## Collections - -This includes mapping among lists, arrays, collections, dictionary including various interfaces: `IList`, `ICollection`, `IEnumerable`, `ISet`, `IDictionary` etc... - -```csharp -var list = db.Pocos.ToList(); -var target = list.Adapt>(); -``` - -## Mappable Objects - -Mapster can map two different objects using the following rules: - -- Source and destination property names are the same. Ex: `dest.Name = src.Name` -- Source has get method. Ex: `dest.Name = src.GetName()` -- Source property has child object which can flatten to destination. Ex: `dest.ContactName = src.Contact.Name` or `dest.Contact_Name = src.Contact.Name` - -Example: - -```csharp -class Staff { - public string Name { get; set; } - public int GetAge() { - return (DateTime.Now - this.BirthDate).TotalDays / 365.25; - } - public Staff Supervisor { get; set; } - ... -} - -struct StaffDto { - public string Name { get; set; } - public int Age { get; set; } - public string SupervisorName { get; set; } -} - -var dto = staff.Adapt(); -//dto.Name = staff.Name, dto.Age = staff.GetAge(), dto.SupervisorName = staff.Supervisor.Name -``` - -**Mappable Object types are included:** - -- POCO classes -- POCO structs -- POCO interfaces -- Dictionary type implement `IDictionary` -- Record types (either class, struct, and interface) - -Example for object to dictionary: - -```csharp -var point = new { X = 2, Y = 3 }; -var dict = point.Adapt>(); -dict["Y"].ShouldBe(3); -``` - -## Record types - ->[!IMPORTANT] -> Mapster treats Record type as an immutable type. -> Only a Nondestructive mutation - creating a new object with modified properties. -> -> ```csharp -> var result = source.adapt(data) ->//equal var result = data with { X = source.X.Adapt(), ...} ->``` - -### Features and Limitations: - -# [v10.0](#tab/Records-v10) - ->[!NOTE] -> By default, all [C# Records](https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/builtin-types/record) are defined as a record type. -> Limitations by count of constructors and constructor parameters used in Mapster version 7.4.0 do not apply. - - -#### Using default value in constuctor param - -If the source type does not contain members that can be used as constructor parameters, then will be used the default values ​​for the parameter type. - -Example: - -```csharp - -class SourceData -{ - public string MyString {get; set;} -} - -record RecordDestination(int myInt, string myString); - -var result = source.Adapt() - -// equal var result = new RecordDestination (default(int),source.myString) - -``` - -#### MultiConstructor Record types - -If there is more than one constructor, by default, mapping will be performed on the constructor with the largest number of parameters. - -Example: - -```csharp -record MultiCtorRecord -{ - public MultiCtorRecord(int myInt) - { - MyInt = myInt; - } - - public MultiCtorRecord(int myInt, string myString) // This constructor will be used - : this(myInt) - { - MyString = myString; - } - -} -``` - -# [v7.4.0](#tab/Records-v7-4-0) - ->[!NOTE] ->Record type must not have a setter and have only one non-empty constructor, and all parameter names must match with properties. - -Otherwise you need to add [`MapToConstructor` configuration](xref:Mapster.Settings.ConstructorMapping#map-to-constructor). - -Example for record types: - -```csharp -class Person { - public string Name { get; } - public int Age { get; } - - public Person(string name, int age) { - this.Name = name; - this.Age = age; - } -} - -var src = new { Name = "Mapster", Age = 3 }; -var target = src.Adapt(); -``` ---- - -### Support additional mapping features: - -| Mapping features | v7.4.0 | v10.0 | -|:-----------------|:------:|:-----:| -|[Custom constructor mapping](xref:Mapster.Settings.ConstructorMapping)| - | ✅ | -|[Ignore](xref:Mapster.Settings.Custom.IgnoringMembers#ignore-extension-method)| - | ✅ | -|[IgnoreNullValues](xref:Mapster.Settings.Custom.IgnoringMembers#ignorenullvalues-extension-method)| - | ✅ | diff --git a/docs/articles/mapping/toc.yml b/docs/articles/mapping/toc.yml index b86d614c..a69e162c 100644 --- a/docs/articles/mapping/toc.yml +++ b/docs/articles/mapping/toc.yml @@ -7,7 +7,6 @@ href: Mappers.md - name: Data types href: data-types/toc.yml - topicHref: xref:Mapster.Mapping.DataTypes - name: Mapping with interface uid: Mapster.Mapping.IMapFromInterface href: Mapping-Configuration-With-IMapFrom-Interface.md \ No newline at end of file From fe82ce079912ec1eac64ba02cab34a5b8f4c5eec Mon Sep 17 00:00:00 2001 From: DocSvartz Date: Wed, 8 Apr 2026 17:51:20 +0500 Subject: [PATCH 55/80] feat: add Flattening mapping for all source type only primitive --- src/Mapster.Tests/WhenFlattening.cs | 33 +++++++++++++++++-- .../Settings/ValueAccessingStrategy.cs | 3 +- 2 files changed, 32 insertions(+), 4 deletions(-) diff --git a/src/Mapster.Tests/WhenFlattening.cs b/src/Mapster.Tests/WhenFlattening.cs index da7f64d9..a823a9a6 100644 --- a/src/Mapster.Tests/WhenFlattening.cs +++ b/src/Mapster.Tests/WhenFlattening.cs @@ -1,6 +1,7 @@ -using System; -using Microsoft.VisualStudio.TestTools.UnitTesting; +using Microsoft.VisualStudio.TestTools.UnitTesting; using Shouldly; +using System; +using System.Collections.Generic; using Assert = Microsoft.VisualStudio.TestTools.UnitTesting.Assert; namespace Mapster.Tests @@ -78,12 +79,40 @@ public class ModelDto public string SubSubSubCoolProperty { get; set; } } + public class Source915 + { + public ICollection Cases { get; set; } = new List + { + new Case(), + new Case(), + new Case() + }; + } + + public class Destination915 + { + public int CasesCount { get; set; } + } #endregion + [TestClass] public class WhenFlattening { + + /// + /// https://github.com/MapsterMapper/Mapster/issues/915 + /// + [TestMethod] + public void FlatteningUsingSourceInterface() + { + var source = new Source915(); + var result = source.Adapt(); + + result.CasesCount.ShouldBe(source.Cases.Count); + } + [TestMethod] public void GetMethodTest() { diff --git a/src/Mapster/Settings/ValueAccessingStrategy.cs b/src/Mapster/Settings/ValueAccessingStrategy.cs index fd13407d..f43f5ff3 100644 --- a/src/Mapster/Settings/ValueAccessingStrategy.cs +++ b/src/Mapster/Settings/ValueAccessingStrategy.cs @@ -116,8 +116,7 @@ public static class ValueAccessingStrategy return member.GetExpression(source); var propertyType = member.Type; - if (propertyName.StartsWith(sourceMemberName) && - (propertyType.IsPoco() || propertyType.IsRecordType())) + if (propertyName.StartsWith(sourceMemberName) && !propertyType.IsMapsterPrimitive()) { var exp = member.GetExpression(source); var ifTrue = GetDeepFlattening(exp, propertyName.Substring(sourceMemberName.Length).TrimStart('_'), arg); From 4a6c544bdf9389fb0ba1f99dcf3840e0704520b0 Mon Sep 17 00:00:00 2001 From: DocSvartz Date: Tue, 14 Apr 2026 14:23:10 +0500 Subject: [PATCH 56/80] chore: Bump version to 10.0.8-pre01 --- 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 91cb6918..55b1a9e4 100644 --- a/src/Directory.Build.props +++ b/src/Directory.Build.props @@ -2,7 +2,7 @@ false - 10.0.7 + 10.0.8-pre01 netstandard2.0;net10.0;net9.0;net8.0 netstandard2.0;net10.0;net9.0;net8.0 net10.0;net9.0;net8.0 From 388602b75731286f50d3a5e70cba375878377e11 Mon Sep 17 00:00:00 2001 From: DocSvartz Date: Mon, 20 Apr 2026 10:02:23 +0500 Subject: [PATCH 57/80] fix: fix IsNotSelfCreation detect from #918 --- src/Mapster/Utils/ReflectionUtils.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Mapster/Utils/ReflectionUtils.cs b/src/Mapster/Utils/ReflectionUtils.cs index a010984a..aa798fc1 100644 --- a/src/Mapster/Utils/ReflectionUtils.cs +++ b/src/Mapster/Utils/ReflectionUtils.cs @@ -464,7 +464,7 @@ public static bool IsNotSelfCreation(this Type type) if (type == typeof(Type) || type.BaseType == typeof(MulticastDelegate)) return true; - return type.GetFieldsAndProperties().Any(it => (it.SetterModifier & (AccessModifier.Public | AccessModifier.NonPublic)) == 0); + return type.GetFieldsAndProperties().All(it => (it.SetterModifier & (AccessModifier.Public | AccessModifier.NonPublic)) == 0); } } } From 5bb4d26f8b9e7701d043d21ac8a2dda9755b1a48 Mon Sep 17 00:00:00 2001 From: DocSvartz Date: Mon, 20 Apr 2026 14:10:08 +0500 Subject: [PATCH 58/80] chore: Bump version to 10.0.8-pre02 --- 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 55b1a9e4..ea0770e0 100644 --- a/src/Directory.Build.props +++ b/src/Directory.Build.props @@ -2,7 +2,7 @@ false - 10.0.8-pre01 + 10.0.8-pre02 netstandard2.0;net10.0;net9.0;net8.0 netstandard2.0;net10.0;net9.0;net8.0 net10.0;net9.0;net8.0 From 7e83af0d4c1cbfe7c876a96d491bb46cb3ee7971 Mon Sep 17 00:00:00 2001 From: DocSvartz Date: Tue, 21 Apr 2026 10:49:45 +0500 Subject: [PATCH 59/80] fix: fix race condition when mapping to RecordType using IgnoreNullValues setting #921 --- src/Mapster/Adapters/RecordTypeAdapter.cs | 84 ++++++++++------------- 1 file changed, 36 insertions(+), 48 deletions(-) diff --git a/src/Mapster/Adapters/RecordTypeAdapter.cs b/src/Mapster/Adapters/RecordTypeAdapter.cs index 659da39c..55d20b1f 100644 --- a/src/Mapster/Adapters/RecordTypeAdapter.cs +++ b/src/Mapster/Adapters/RecordTypeAdapter.cs @@ -14,9 +14,6 @@ internal class RecordTypeAdapter : ClassAdapter private ClassMapping? ClassConverterContext; protected override int Score => -149; protected override bool UseTargetValue => false; - - private List SkipIgnoreNullValuesMemberMap = new List(); - protected override bool CanMap(PreCompileArgument arg) { return arg.DestinationType.IsRecordType(); @@ -36,8 +33,6 @@ protected override Expression CreateInlineExpression(Expression source, CompileA protected override Expression CreateInstantiationExpression(Expression source, Expression? destination, CompileArgument arg) { //new TDestination(src.Prop1, src.Prop2) - - SkipIgnoreNullValuesMemberMap.Clear(); Expression installExpr; if (arg.GetConstructUsing() != null || arg.Settings.MapToConstructor != null || arg.DestinationType == null) @@ -78,7 +73,6 @@ protected override Expression CreateInstantiationExpression(Expression source, E lines.AddRange(memberInit.Bindings); foreach (var member in members) { - if (!arg.Settings.Resolvers.Any(r => r.DestinationMemberName == member.DestinationMember.Name) && contructorMembers.Any(x => string.Equals(x.Name, member.DestinationMember.Name, StringComparison.InvariantCultureIgnoreCase))) continue; @@ -88,30 +82,27 @@ protected override Expression CreateInstantiationExpression(Expression source, E var adapt = CreateAdaptExpression(member.Getter, member.DestinationMember.Type, arg, member); - if (arg.Settings.IgnoreNullValues == true && member.Getter.CanBeNull()) // add IgnoreNullValues support + if (arg.MapType != MapType.Projection && arg.Settings.IgnoreNullValues == true && member.Getter.CanBeNull()) // add IgnoreNullValues support { - if (arg.MapType != MapType.MapToTarget) - { - SkipIgnoreNullValuesMemberMap.Add(member); - continue; - } + if(arg.MapType == MapType.Map) + continue; // skip to block handler - if (adapt is ConditionalExpression condEx) + if (arg.MapType == MapType.MapToTarget) { - if (condEx.Test is BinaryExpression { NodeType: ExpressionType.Equal } binEx && - binEx.Left == member.Getter && - binEx.Right is ConstantExpression { Value: null }) - adapt = condEx.IfFalse; + if (adapt is ConditionalExpression condEx) + { + if (condEx.Test is BinaryExpression { NodeType: ExpressionType.Equal } binEx && + binEx.Left == member.Getter && + binEx.Right is ConstantExpression { Value: null }) + adapt = condEx.IfFalse; + } + var destinationCompareNull = Expression.Equal(destination, Expression.Constant(null, destination.Type)); + var sourceCondition = Expression.NotEqual(member.Getter, Expression.Constant(null, member.Getter.Type)); + var destinationCanbeNull = Expression.Condition(destinationCompareNull, member.DestinationMember.Type.CreateDefault(), member.DestinationMember.GetExpression(destination)); + adapt = Expression.Condition(sourceCondition, adapt, destinationCanbeNull); } - var destinationCompareNull = Expression.Equal(destination, Expression.Constant(null, destination.Type)); - var sourceCondition = Expression.NotEqual(member.Getter, Expression.Constant(null, member.Getter.Type)); - var destinationCanbeNull = Expression.Condition(destinationCompareNull, member.DestinationMember.Type.CreateDefault(), member.DestinationMember.GetExpression(destination)); - adapt = Expression.Condition(sourceCondition, adapt, destinationCanbeNull); } - - - //special null property check for projection //if we don't set null to property, EF will create empty object //except collection type & complex type which cannot be null @@ -171,29 +162,6 @@ protected override Expression CreateBlockExpression(Expression source, Expressio var lines = new List(); - if (arg.MapType != MapType.MapToTarget) - { - foreach (var member in SkipIgnoreNullValuesMemberMap) - { - - var adapt = CreateAdaptExpression(member.Getter, member.DestinationMember.Type, arg, member); - - if (adapt is ConditionalExpression condEx) - { - if (condEx.Test is BinaryExpression { NodeType: ExpressionType.Equal } binEx && - binEx.Left == member.Getter && - binEx.Right is ConstantExpression { Value: null }) - adapt = condEx.IfFalse; - } - adapt = member.DestinationMember.SetExpression(destination, adapt); - var sourceCondition = Expression.NotEqual(member.Getter, Expression.Constant(null, member.Getter.Type)); - - - lines.Add(Expression.IfThen(sourceCondition, adapt)); - } - } - - foreach (var member in members) { if (member.DestinationMember.SetterModifier == AccessModifier.None && member.UseDestinationValue) @@ -203,7 +171,6 @@ protected override Expression CreateBlockExpression(Expression source, Expressio || member.DestinationMember.Type.IsMapsterPrimitive() || member.DestinationMember.Type.IsRecordType()) { - Expression adapt; if (member.DestinationMember.Type.IsRecordType()) adapt = arg.Context.Config.CreateMapInvokeExpressionBody(member.Getter.Type, member.DestinationMember.Type, member.Getter); @@ -291,6 +258,27 @@ protected override Expression CreateBlockExpression(Expression source, Expressio } } + else + { + // IgnoreNullValues to Map type mapping + if(arg.MapType == MapType.Map && arg.Settings.IgnoreNullValues == true && member.Getter.CanBeNull()) + { + var adapt = CreateAdaptExpression(member.Getter, member.DestinationMember.Type, arg, member); + + if (adapt is ConditionalExpression condEx) + { + if (condEx.Test is BinaryExpression { NodeType: ExpressionType.Equal } binEx && + binEx.Left == member.Getter && + binEx.Right is ConstantExpression { Value: null }) + adapt = condEx.IfFalse; + } + adapt = member.DestinationMember.SetExpression(destination, adapt); + var sourceCondition = Expression.NotEqual(member.Getter, Expression.Constant(null, member.Getter.Type)); + + + lines.Add(Expression.IfThen(sourceCondition, adapt)); + } + } } return lines.Count > 0 ? (Expression)Expression.Block(lines) : Expression.Empty(); From e1f9190b8f710193f89beb8fed8549b562be89b4 Mon Sep 17 00:00:00 2001 From: DocSvartz Date: Tue, 21 Apr 2026 13:17:31 +0500 Subject: [PATCH 60/80] chore: Bump version to 10.0.8-pre03 --- 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 ea0770e0..1b0404e4 100644 --- a/src/Directory.Build.props +++ b/src/Directory.Build.props @@ -2,7 +2,7 @@ false - 10.0.8-pre02 + 10.0.8-pre03 netstandard2.0;net10.0;net9.0;net8.0 netstandard2.0;net10.0;net9.0;net8.0 net10.0;net9.0;net8.0 From d1481176899f101505a2050d39d4ea8e98ffc514 Mon Sep 17 00:00:00 2001 From: DocSvartz Date: Fri, 24 Apr 2026 19:17:39 +0500 Subject: [PATCH 61/80] fix: fix IgnoreNonMapped member collector #924 --- src/Mapster/Adapters/BaseClassAdapter.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/Mapster/Adapters/BaseClassAdapter.cs b/src/Mapster/Adapters/BaseClassAdapter.cs index a8b7a4c0..1d6f6b64 100644 --- a/src/Mapster/Adapters/BaseClassAdapter.cs +++ b/src/Mapster/Adapters/BaseClassAdapter.cs @@ -300,6 +300,9 @@ protected void IgnoreNonMapped (ClassModel classModel, CompileArgument arg) foreach (var item in notMappingToIgnore) { + if (!item.ShouldMapMember(arg, MemberSide.Destination)) + continue; + arg.Settings.Ignore.TryAdd(item.Name, new IgnoreDictionary.IgnoreItem()); } } From 06535e6d5bd4cf47a58119cfb0f94fa6bd4e7629 Mon Sep 17 00:00:00 2001 From: DocSvartz Date: Mon, 27 Apr 2026 11:14:31 +0500 Subject: [PATCH 62/80] fix: #928 - not creation abstract destination if souce is null --- src/Mapster/Adapters/BaseAdapter.cs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/Mapster/Adapters/BaseAdapter.cs b/src/Mapster/Adapters/BaseAdapter.cs index dba50681..4bdddd86 100644 --- a/src/Mapster/Adapters/BaseAdapter.cs +++ b/src/Mapster/Adapters/BaseAdapter.cs @@ -204,6 +204,12 @@ protected Expression CreateBlockExpressionBody(Expression source, Expression? de blocks.Add(ifExpr); } + /// fix https://github.com/MapsterMapper/Mapster/issues/928 + /// Not create destination is abstract type if source is null + if (arg.DestinationType.IsAbstract) + blocks.Add(Expression.IfThen(Expression.Equal(source, Expression.Constant(null, arg.SourceType)), + Expression.Return(label, Expression.Default(arg.DestinationType)))); + //new TDest(); Expression transformedSource = source; var transform = TransformSource(source); From 15daf08e0ec1d51d088ab4557063c9f12aaa7eff Mon Sep 17 00:00:00 2001 From: DocSvartz Date: Mon, 27 Apr 2026 11:17:05 +0500 Subject: [PATCH 63/80] feat(test): add test from # 928 --- src/Mapster.Tests/WhenMappingDerived.cs | 71 +++++++++++++++++++++++++ 1 file changed, 71 insertions(+) diff --git a/src/Mapster.Tests/WhenMappingDerived.cs b/src/Mapster.Tests/WhenMappingDerived.cs index e6399389..2189eba6 100644 --- a/src/Mapster.Tests/WhenMappingDerived.cs +++ b/src/Mapster.Tests/WhenMappingDerived.cs @@ -85,6 +85,77 @@ public void WhenMapToTargetDerivedWithNullRegression() (container.Nested is Derived794E).ShouldBeTrue(); // is not Base794 type, MapWith is working when Polymorphic mapping to null } + /// + /// https://github.com/MapsterMapper/Mapster/issues/928 + /// + [TestMethod] + public void NullNotCreationValue() + { + var config = new TypeAdapterConfig(); + + config.Default.PreserveReference(true); + config.Default.EnumMappingStrategy(EnumMappingStrategy.ByName); + config.Default.EnableNonPublicMembers(true); + //config.Default.IgnoreNullValues(true); // not update if source in null + config.Default.MapToConstructor(true); + config.Default.ShallowCopyForSameType(false); + config.AllowImplicitSourceInheritance = true; + config.RequireDestinationMemberSource = true; + + config.ForType() + .Include(); + + var dto = new DtoFoo928 + { + Field = new DtoDerived928 { Id = "123" } + }; + + var dtoNull = new DtoFoo928 + { + Field = null + }; + + var domainnotnull = dto.Adapt(config); + var nullresult = dtoNull.Adapt(config); + + + nullresult.ShouldSatisfyAllConditions(() => + { + domainnotnull.Field.ShouldBeOfType(); + (domainnotnull.Field as DomainDerived928).Id.ShouldBe("123"); + nullresult.Field.ShouldBeNull(); + }); + + } + + public abstract class DtoBase928 + { + } + + public class DtoDerived928 : DtoBase928 + { + public string Id { get; set; } = null!; + } + + public class DtoFoo928 + { + public DtoBase928? Field { get; set; } + } + + public abstract class DomainBase928 + { + } + + public class DomainDerived928 : DomainBase928 + { + public string Id { get; set; } = null!; + } + + public class DomainFoo928 + { + public DomainBase928? Field { get; set; } + } + internal class Derived794E : Derived794 { From 8bb30519bc878699ad96ce9cec2c216367115952 Mon Sep 17 00:00:00 2001 From: DocSvartz Date: Tue, 28 Apr 2026 11:30:12 +0500 Subject: [PATCH 64/80] fix: #927 add skip if destination member is not setter --- src/Mapster/Adapters/ClassAdapter.cs | 3 ++- src/Mapster/Adapters/RecordTypeAdapter.cs | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/Mapster/Adapters/ClassAdapter.cs b/src/Mapster/Adapters/ClassAdapter.cs index 03a2eb6b..69af2101 100644 --- a/src/Mapster/Adapters/ClassAdapter.cs +++ b/src/Mapster/Adapters/ClassAdapter.cs @@ -140,7 +140,8 @@ protected override Expression CreateBlockExpression(Expression source, Expressio if (!member.UseDestinationValue) { - if (arg.Settings.IgnoreNullValues == true && member.Getter.CanBeNull()) + if (arg.Settings.IgnoreNullValues == true && member.Getter.CanBeNull() + && member.DestinationMember.SetterModifier != AccessModifier.None) { if (adapt is ConditionalExpression condEx) { diff --git a/src/Mapster/Adapters/RecordTypeAdapter.cs b/src/Mapster/Adapters/RecordTypeAdapter.cs index 55d20b1f..63c32747 100644 --- a/src/Mapster/Adapters/RecordTypeAdapter.cs +++ b/src/Mapster/Adapters/RecordTypeAdapter.cs @@ -261,7 +261,8 @@ protected override Expression CreateBlockExpression(Expression source, Expressio else { // IgnoreNullValues to Map type mapping - if(arg.MapType == MapType.Map && arg.Settings.IgnoreNullValues == true && member.Getter.CanBeNull()) + if(arg.MapType == MapType.Map && arg.Settings.IgnoreNullValues == true && member.Getter.CanBeNull() + && member.DestinationMember.SetterModifier != AccessModifier.None) { var adapt = CreateAdaptExpression(member.Getter, member.DestinationMember.Type, arg, member); From 5444cfabe8828dff99a5cbfb863b886f711e289e Mon Sep 17 00:00:00 2001 From: DocSvartz Date: Tue, 28 Apr 2026 14:04:42 +0500 Subject: [PATCH 65/80] fix: #925 - Compile config with open generic is not throw exception --- src/Mapster.Tests/WhenMappingWithOpenGenerics.cs | 2 ++ src/Mapster/TypeAdapterConfig.cs | 13 +++++++++++++ 2 files changed, 15 insertions(+) diff --git a/src/Mapster.Tests/WhenMappingWithOpenGenerics.cs b/src/Mapster.Tests/WhenMappingWithOpenGenerics.cs index 88cdc66e..7abb5e1f 100644 --- a/src/Mapster.Tests/WhenMappingWithOpenGenerics.cs +++ b/src/Mapster.Tests/WhenMappingWithOpenGenerics.cs @@ -25,6 +25,8 @@ public void Setting_From_OpenGeneric_Has_No_SideEffect() .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 diff --git a/src/Mapster/TypeAdapterConfig.cs b/src/Mapster/TypeAdapterConfig.cs index 34f22495..8386c622 100644 --- a/src/Mapster/TypeAdapterConfig.cs +++ b/src/Mapster/TypeAdapterConfig.cs @@ -659,6 +659,19 @@ public void Compile(bool failFast = true) if (key.Source == typeof(void)) continue; + if (key.Source.ContainsGenericParameters || key.Destination.ContainsGenericParameters) + { + var checkKey = new TypeTuple( + key.Source.IsGenericType ? key.Source.GetGenericTypeDefinition().MakeGenericType(typeof(object)) : key.Source, + key.Destination.IsGenericType ? key.Destination.GetGenericTypeDefinition().MakeGenericType(typeof(object)) : key.Destination + ); + + Compiler(CreateMapExpression(checkKey, MapType.Map)); + Compiler(CreateMapExpression(checkKey, MapType.MapToTarget)); + + continue; + } + _mapDict[key] = Compiler(CreateMapExpression(key, MapType.Map)); _mapToTargetDict[key] = Compiler(CreateMapExpression(key, MapType.MapToTarget)); } From f7abb5f23c16ac607c4ddfdbd7212c1e11c58398 Mon Sep 17 00:00:00 2001 From: DocSvartz Date: Tue, 28 Apr 2026 14:41:11 +0500 Subject: [PATCH 66/80] feat(test): add tests to case in #927 --- .../WhenMappingRecordRegression.cs | 55 +++++++++++++++++++ 1 file changed, 55 insertions(+) diff --git a/src/Mapster.Tests/WhenMappingRecordRegression.cs b/src/Mapster.Tests/WhenMappingRecordRegression.cs index 94518641..13ff3ea3 100644 --- a/src/Mapster.Tests/WhenMappingRecordRegression.cs +++ b/src/Mapster.Tests/WhenMappingRecordRegression.cs @@ -539,6 +539,40 @@ public void NotSelfCreationTypeMappingToSelfWithOutError() resultJ.RootElement.GetProperty("key").ToString().ShouldBe("value"); } + /// + /// https://github.com/MapsterMapper/Mapster/issues/927 + /// + [TestMethod] + public void MappingToReadOnlyInterfaceUsingIgnoreNulValuesWithoutError() + { + var config = new TypeAdapterConfig(); + config + .NewConfig() + .IgnoreNullValues(true); + + Should.NotThrow(() => { + + config.Compile(); + }); + } + + /// + /// https://github.com/MapsterMapper/Mapster/issues/927 + /// + [TestMethod] + public void MappingToReadOnlyRecordUsingIgnoreNulValuesWithoutError() + { + var config = new TypeAdapterConfig(); + config + .NewConfig() + .IgnoreNullValues(true); + + Should.NotThrow(() => { + + config.Compile(); + }); + } + #region NowNotWorking /// @@ -993,5 +1027,26 @@ class InsiderWithCtorDestYx public AutoCtorDestYx X { set; get; } } + public interface IDto927 + { + string Id { get; set; } + string Value { get; set; } + IList ValueList { get; set; } + } + + public interface IDomain934 + { + string Id { get; } + string Value { get; } + IList ValueList { get; } + } + + public record Domain927 + { + string Id { get; } + string Value { get; } + IList ValueList { get; } + } + #endregion TestClasses } From a0cd3c18849cd666ea85636c5cbf88b4f4d048cc Mon Sep 17 00:00:00 2001 From: DocSvartz Date: Tue, 28 Apr 2026 14:52:34 +0500 Subject: [PATCH 67/80] chore: Bump version to 10.0.8-pre04 --- 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 1b0404e4..d3d04ba7 100644 --- a/src/Directory.Build.props +++ b/src/Directory.Build.props @@ -2,7 +2,7 @@ false - 10.0.8-pre03 + 10.0.8-pre04 netstandard2.0;net10.0;net9.0;net8.0 netstandard2.0;net10.0;net9.0;net8.0 net10.0;net9.0;net8.0 From 1aaa504db52b8d73bd2309db5b042ef9a060f68d Mon Sep 17 00:00:00 2001 From: DocSvartz Date: Wed, 29 Apr 2026 10:21:55 +0500 Subject: [PATCH 68/80] feat: #938 - Add DirectAssignmentForSameType setting parametr --- src/Mapster.Tests/WhenConfiguringMapping.cs | 26 +++++++++++++++-- src/Mapster/Adapters/BaseAdapter.cs | 13 +++++++-- src/Mapster/TypeAdapterSetter.cs | 8 ++++++ src/Mapster/TypeAdapterSettings.cs | 7 +++++ src/Mapster/Utils/ReflectionUtils.cs | 31 +++++++++++++++++++++ 5 files changed, 81 insertions(+), 4 deletions(-) diff --git a/src/Mapster.Tests/WhenConfiguringMapping.cs b/src/Mapster.Tests/WhenConfiguringMapping.cs index 809e6387..076e5679 100644 --- a/src/Mapster.Tests/WhenConfiguringMapping.cs +++ b/src/Mapster.Tests/WhenConfiguringMapping.cs @@ -142,9 +142,9 @@ public void NewInstanceConfigurationTest() obj.Name = "Tim"; obj.Child = new TestNewInstanceF() { Name = "Kıvanç" }; - TypeAdapterConfig + TypeAdapterConfig .NewConfig() - .ShallowCopyForSameType(true); + .ShallowCopyForSameType(true); var newObj2 = TypeAdapter.Adapt(obj); @@ -156,6 +156,28 @@ public void NewInstanceConfigurationTest() Assert.IsTrue(newObj2.Child.Name == "Antalya"); } + [TestMethod] + public void WhenDirectAssignmentForSameTypeConfigurate() + { + TestNewInstanceD obj = new TestNewInstanceD(); + obj.Name = "Tim"; + obj.Child = new TestNewInstanceF() { Name = "Kıvanç" }; + + var config = new TypeAdapterConfig(); + config.NewConfig() + .DirectAssignmentForSameType(true); + + var newObj2 = TypeAdapter.Adapt(obj, config); + + Assert.IsTrue(newObj2.Name == "Tim"); + Assert.IsTrue(obj.Child.Name == newObj2.Child.Name); + + obj.Child.Name = "Antalya"; + + Assert.IsTrue(newObj2.Child.Name == "Antalya"); + } + + #region Data private Source _source; diff --git a/src/Mapster/Adapters/BaseAdapter.cs b/src/Mapster/Adapters/BaseAdapter.cs index 4bdddd86..1a8e3fd4 100644 --- a/src/Mapster/Adapters/BaseAdapter.cs +++ b/src/Mapster/Adapters/BaseAdapter.cs @@ -495,10 +495,19 @@ internal Expression CreateAdaptExpression(Expression source, Type destinationTyp if (_source.Type == destinationType && arg.MapType == MapType.Projection) return _source; + TypeAdapterRule? rule; + var tuple = new TypeTuple(_source.Type, destinationType); + arg.Context.Config.RuleMap.TryGetValue(tuple, out rule); + //adapt(_source); var notUsingDestinationValue = mapping is not { UseDestinationValue: true }; - var exp = _source.Type == destinationType && arg.Settings.ShallowCopyForSameType == true && notUsingDestinationValue && - !arg.Context.Config.HasRuleFor(_source.Type, destinationType) + + if(_source.Type == destinationType && notUsingDestinationValue + && arg.IsDirectAssignmentForSameTypeEnable(rule, tuple) && arg.IsNotCustomConverterFactory(rule)) + return _source.To(destinationType); + + var exp = _source.Type == destinationType && arg.Settings.ShallowCopyForSameType == true && notUsingDestinationValue + && rule == null ? _source : CreateAdaptExpressionCore(_source, destinationType, arg, mapping, destination); diff --git a/src/Mapster/TypeAdapterSetter.cs b/src/Mapster/TypeAdapterSetter.cs index 834f4dbe..5dd0df11 100644 --- a/src/Mapster/TypeAdapterSetter.cs +++ b/src/Mapster/TypeAdapterSetter.cs @@ -108,6 +108,14 @@ public static TSetter ShallowCopyForSameType(this TSetter setter, bool return setter; } + public static TSetter DirectAssignmentForSameType(this TSetter setter, bool value) where TSetter : TypeAdapterSetter + { + setter.CheckCompiled(); + + setter.Settings.DirectAssignmentForSameType = value; + return setter; + } + public static TSetter EnumMappingStrategy(this TSetter setter, EnumMappingStrategy strategy) where TSetter : TypeAdapterSetter { setter.CheckCompiled(); diff --git a/src/Mapster/TypeAdapterSettings.cs b/src/Mapster/TypeAdapterSettings.cs index f94172a3..38e3a53c 100644 --- a/src/Mapster/TypeAdapterSettings.cs +++ b/src/Mapster/TypeAdapterSettings.cs @@ -39,11 +39,18 @@ public bool? PreserveReference get => Get(nameof(PreserveReference)); set => Set(nameof(PreserveReference), value); } + public bool? DirectAssignmentForSameType + { + get => Get(nameof(DirectAssignmentForSameType)); + set => Set(nameof(DirectAssignmentForSameType), value); + } + public bool? ShallowCopyForSameType { get => Get(nameof(ShallowCopyForSameType)); set => Set(nameof(ShallowCopyForSameType), value); } + public bool? IgnoreNullValues { get => Get(nameof(IgnoreNullValues)); diff --git a/src/Mapster/Utils/ReflectionUtils.cs b/src/Mapster/Utils/ReflectionUtils.cs index aa798fc1..d397cff7 100644 --- a/src/Mapster/Utils/ReflectionUtils.cs +++ b/src/Mapster/Utils/ReflectionUtils.cs @@ -466,5 +466,36 @@ public static bool IsNotSelfCreation(this Type type) return type.GetFieldsAndProperties().All(it => (it.SetterModifier & (AccessModifier.Public | AccessModifier.NonPublic)) == 0); } + + public static bool IsDirectAssignmentForSameTypeEnable(this CompileArgument arg, TypeAdapterRule? rule, TypeTuple tuple) + { + if (rule != null + && rule.Settings.DirectAssignmentForSameType.HasValue) + return rule.Settings.DirectAssignmentForSameType.GetValueOrDefault(); + + if (arg.Context.Config.Rules + .Where(x => x.Settings.DestinationType == tuple.Destination) + .Select(x => x.Settings) + .Any(x => x.DirectAssignmentForSameType.HasValue && x.DirectAssignmentForSameType.GetValueOrDefault())) + return true; + + if (arg.Context.Config.Default.Settings.DirectAssignmentForSameType.GetValueOrDefault()) + return true; + + return false; + } + + public static bool IsNotCustomConverterFactory(this CompileArgument arg, TypeAdapterRule? rule) + { + if(rule != null) + { + if(arg.MapType == MapType.Map && rule.Settings.ConverterFactory != null) + return false; + if (arg.MapType == MapType.MapToTarget && rule.Settings.ConverterToTargetFactory != null) + return false; + } + + return true; + } } } From e8d12a7d6232187eee44c04f9a0a243c34b7a209 Mon Sep 17 00:00:00 2001 From: DocSvartz Date: Tue, 5 May 2026 14:09:15 +0500 Subject: [PATCH 69/80] fix: #925 disable Compile check for open generic configs --- src/Mapster/TypeAdapterConfig.cs | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/src/Mapster/TypeAdapterConfig.cs b/src/Mapster/TypeAdapterConfig.cs index 8386c622..1dce2053 100644 --- a/src/Mapster/TypeAdapterConfig.cs +++ b/src/Mapster/TypeAdapterConfig.cs @@ -660,17 +660,7 @@ public void Compile(bool failFast = true) continue; if (key.Source.ContainsGenericParameters || key.Destination.ContainsGenericParameters) - { - var checkKey = new TypeTuple( - key.Source.IsGenericType ? key.Source.GetGenericTypeDefinition().MakeGenericType(typeof(object)) : key.Source, - key.Destination.IsGenericType ? key.Destination.GetGenericTypeDefinition().MakeGenericType(typeof(object)) : key.Destination - ); - - Compiler(CreateMapExpression(checkKey, MapType.Map)); - Compiler(CreateMapExpression(checkKey, MapType.MapToTarget)); - continue; - } _mapDict[key] = Compiler(CreateMapExpression(key, MapType.Map)); _mapToTargetDict[key] = Compiler(CreateMapExpression(key, MapType.MapToTarget)); From 290417fb8cb370609518359ab5c478cba630a2c6 Mon Sep 17 00:00:00 2001 From: DocSvartz Date: Wed, 6 May 2026 09:49:16 +0500 Subject: [PATCH 70/80] fix: refactoring and fix DirectAssignmentForSameType implementation --- src/Mapster/Adapters/BaseAdapter.cs | 5 ----- src/Mapster/Adapters/ClassAdapter.cs | 13 +++++++++++++ src/Mapster/Utils/ReflectionUtils.cs | 18 ------------------ 3 files changed, 13 insertions(+), 23 deletions(-) diff --git a/src/Mapster/Adapters/BaseAdapter.cs b/src/Mapster/Adapters/BaseAdapter.cs index 1a8e3fd4..31d7541c 100644 --- a/src/Mapster/Adapters/BaseAdapter.cs +++ b/src/Mapster/Adapters/BaseAdapter.cs @@ -501,11 +501,6 @@ internal Expression CreateAdaptExpression(Expression source, Type destinationTyp //adapt(_source); var notUsingDestinationValue = mapping is not { UseDestinationValue: true }; - - if(_source.Type == destinationType && notUsingDestinationValue - && arg.IsDirectAssignmentForSameTypeEnable(rule, tuple) && arg.IsNotCustomConverterFactory(rule)) - return _source.To(destinationType); - var exp = _source.Type == destinationType && arg.Settings.ShallowCopyForSameType == true && notUsingDestinationValue && rule == null ? _source diff --git a/src/Mapster/Adapters/ClassAdapter.cs b/src/Mapster/Adapters/ClassAdapter.cs index 69af2101..042c0270 100644 --- a/src/Mapster/Adapters/ClassAdapter.cs +++ b/src/Mapster/Adapters/ClassAdapter.cs @@ -291,5 +291,18 @@ private static Expression SetValueByReflection(MemberMapping member, MemberExpre return Expression.MemberInit(newInstance, lines); } + + protected override Expression CreateExpressionBody(Expression source, Expression? destination, CompileArgument arg) + { + TypeAdapterRule? rule; + var tuple = new TypeTuple(source.Type, arg.DestinationType); + arg.Context.Config.RuleMap.TryGetValue(tuple, out rule); + + if (source.Type == arg.DestinationType && !arg.UseDestinationValue + && arg.Settings.DirectAssignmentForSameType.GetValueOrDefault() && arg.IsNotCustomConverterFactory(rule)) + return source; + + return base.CreateExpressionBody(source, destination, arg); + } } } diff --git a/src/Mapster/Utils/ReflectionUtils.cs b/src/Mapster/Utils/ReflectionUtils.cs index d397cff7..81c3e21e 100644 --- a/src/Mapster/Utils/ReflectionUtils.cs +++ b/src/Mapster/Utils/ReflectionUtils.cs @@ -467,24 +467,6 @@ public static bool IsNotSelfCreation(this Type type) return type.GetFieldsAndProperties().All(it => (it.SetterModifier & (AccessModifier.Public | AccessModifier.NonPublic)) == 0); } - public static bool IsDirectAssignmentForSameTypeEnable(this CompileArgument arg, TypeAdapterRule? rule, TypeTuple tuple) - { - if (rule != null - && rule.Settings.DirectAssignmentForSameType.HasValue) - return rule.Settings.DirectAssignmentForSameType.GetValueOrDefault(); - - if (arg.Context.Config.Rules - .Where(x => x.Settings.DestinationType == tuple.Destination) - .Select(x => x.Settings) - .Any(x => x.DirectAssignmentForSameType.HasValue && x.DirectAssignmentForSameType.GetValueOrDefault())) - return true; - - if (arg.Context.Config.Default.Settings.DirectAssignmentForSameType.GetValueOrDefault()) - return true; - - return false; - } - public static bool IsNotCustomConverterFactory(this CompileArgument arg, TypeAdapterRule? rule) { if(rule != null) From 1d8907461296bbf951c8c9a74f53ffac7bd0ad50 Mon Sep 17 00:00:00 2001 From: DocSvartz Date: Wed, 6 May 2026 15:12:30 +0500 Subject: [PATCH 71/80] chore: Bump version to 10.0.8-pre05 --- 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 d3d04ba7..346f5174 100644 --- a/src/Directory.Build.props +++ b/src/Directory.Build.props @@ -2,7 +2,7 @@ false - 10.0.8-pre04 + 10.0.8-pre05 netstandard2.0;net10.0;net9.0;net8.0 netstandard2.0;net10.0;net9.0;net8.0 net10.0;net9.0;net8.0 From 75231f0ba026a56846975b9d02a07199637ee870 Mon Sep 17 00:00:00 2001 From: DocSvartz Date: Sat, 9 May 2026 07:03:13 +0500 Subject: [PATCH 72/80] fix: refactoring fix #903 - add supporting fix to records - add currect supporting Destination Transform feature --- src/Mapster/Adapters/BaseAdapter.cs | 22 ++++++++++++++++++---- src/Mapster/Adapters/ClassAdapter.cs | 24 ++---------------------- 2 files changed, 20 insertions(+), 26 deletions(-) diff --git a/src/Mapster/Adapters/BaseAdapter.cs b/src/Mapster/Adapters/BaseAdapter.cs index 31d7541c..5291ea6d 100644 --- a/src/Mapster/Adapters/BaseAdapter.cs +++ b/src/Mapster/Adapters/BaseAdapter.cs @@ -501,10 +501,24 @@ internal Expression CreateAdaptExpression(Expression source, Type destinationTyp //adapt(_source); var notUsingDestinationValue = mapping is not { UseDestinationValue: true }; - var exp = _source.Type == destinationType && arg.Settings.ShallowCopyForSameType == true && notUsingDestinationValue - && rule == null - ? _source - : CreateAdaptExpressionCore(_source, destinationType, arg, mapping, destination); + Expression exp; + + 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); //transform(adapt(_source)); if (notUsingDestinationValue) diff --git a/src/Mapster/Adapters/ClassAdapter.cs b/src/Mapster/Adapters/ClassAdapter.cs index 042c0270..27d09d9b 100644 --- a/src/Mapster/Adapters/ClassAdapter.cs +++ b/src/Mapster/Adapters/ClassAdapter.cs @@ -114,17 +114,7 @@ protected override Expression CreateBlockExpression(Expression source, Expressio ? member.DestinationMember.GetExpression(destination) : null; - Expression adapt; - - // convert ApplyNullable Propagation for NotPrimitive Nullable types - if (member.Getter is ConditionalExpression cond && member.Getter.Type.IsNotPrimitiveNullableType() - && !member.DestinationMember.Type.IsNullable()) - { - var value = CreateAdaptExpression(cond.IfTrue.GetNotPrimitiveNullableValue(), member.DestinationMember.Type, arg, member, destMember); - adapt = Expression.Condition(cond.Test, value, member.DestinationMember.Type.CreateDefault()); - } - else - adapt = CreateAdaptExpression(member.Getter, member.DestinationMember.Type, arg, member, destMember); + var adapt = CreateAdaptExpression(member.Getter, member.DestinationMember.Type, arg, member, destMember); if (member.UseDestinationValue && member.DestinationMember.Type.IsMapsterImmutable() @@ -262,17 +252,7 @@ private static Expression SetValueByReflection(MemberMapping member, MemberExpre if (member.DestinationMember.SetterModifier == AccessModifier.None) continue; - Expression value; - - // convert ApplyNullable Propagation for NotPrimitive Nullable types - if (member.Getter is ConditionalExpression cond && member.Getter.Type.IsNotPrimitiveNullableType() - && !member.DestinationMember.Type.IsNullable()) - { - var adapt = CreateAdaptExpression(cond.IfTrue.GetNotPrimitiveNullableValue(), member.DestinationMember.Type, arg, member); - value = Expression.Condition(cond.Test, adapt, member.DestinationMember.Type.CreateDefault()); - } - else - value = CreateAdaptExpression(member.Getter, member.DestinationMember.Type, arg, member); + var value = CreateAdaptExpression(member.Getter, member.DestinationMember.Type, arg, member); //special null property check for projection //if we don't set null to property, EF will create empty object From f1f264a560582d476d7c9d745d9301990f6ab1bb Mon Sep 17 00:00:00 2001 From: DocSvartz Date: Sat, 9 May 2026 07:09:56 +0500 Subject: [PATCH 73/80] feat(test): add test to #943 - add test when nullablePropagation for ctor must currect work with AddDestinationTransform --- .../WhenCtorNullableParamMapping.cs | 29 +++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/src/Mapster.Tests/WhenCtorNullableParamMapping.cs b/src/Mapster.Tests/WhenCtorNullableParamMapping.cs index bef0b16f..e1884624 100644 --- a/src/Mapster.Tests/WhenCtorNullableParamMapping.cs +++ b/src/Mapster.Tests/WhenCtorNullableParamMapping.cs @@ -1,5 +1,6 @@ using Microsoft.VisualStudio.TestTools.UnitTesting; using Shouldly; +using System.Collections.Generic; namespace Mapster.Tests { @@ -60,6 +61,27 @@ public void Dto_To_Domain_AbstractClassNull_MapsCorrectly() } + /// + /// https://github.com/MapsterMapper/Mapster/issues/943 + /// + [TestMethod] + public void NullableCtorPropagationCurrentWorkWithDestinationTransform() + { + var config = new TypeAdapterConfig(); + + config.Default + .AddDestinationTransform(DestinationTransform.EmptyCollectionIfNull); + + // Arrange + var fooDto = new FooDto943(); + + // Act + var foo = fooDto.Adapt(config); + + // Assert + foo.Strings.ShouldNotBeNull(); + } + #region Immutable classes with private setters, map via ctors private abstract class AbstractDomainTestClass { @@ -96,6 +118,13 @@ public DomainTestClass( #endregion #region DTO classes + + class FooDto943 + { + public string[] Strings { get; set; } + } + + record Foo943(List Strings); private abstract class AbstractDtoTestClass { public string AbstractProperty { get; set; } From a87a5f87f5421cd0d1d1a799a9ff6a7ae6703215 Mon Sep 17 00:00:00 2001 From: DocSvartz Date: Sat, 9 May 2026 07:23:47 +0500 Subject: [PATCH 74/80] fix: #943 - add supporting DestinationTransforms to ApplyNullPropagationFromCtor --- src/Mapster/Adapters/BaseAdapter.cs | 2 +- src/Mapster/Adapters/BaseClassAdapter.cs | 2 +- src/Mapster/Utils/ExpressionEx.cs | 7 ++++++- 3 files changed, 8 insertions(+), 3 deletions(-) diff --git a/src/Mapster/Adapters/BaseAdapter.cs b/src/Mapster/Adapters/BaseAdapter.cs index 5291ea6d..b31a0dbe 100644 --- a/src/Mapster/Adapters/BaseAdapter.cs +++ b/src/Mapster/Adapters/BaseAdapter.cs @@ -448,7 +448,7 @@ protected virtual Expression CreateInstantiationExpression(Expression source, Ex } } - private static Expression CreateAdaptExpressionCore(Expression source, Type destinationType, CompileArgument arg, MemberMapping? mapping = null, Expression? destination = null) + internal static Expression CreateAdaptExpressionCore(Expression source, Type destinationType, CompileArgument arg, MemberMapping? mapping = null, Expression? destination = null) { var mapType = arg.MapType == MapType.MapToTarget && destination == null ? MapType.Map : mapping?.UseDestinationValue == true ? MapType.MapToTarget : diff --git a/src/Mapster/Adapters/BaseClassAdapter.cs b/src/Mapster/Adapters/BaseClassAdapter.cs index 1d6f6b64..d719409e 100644 --- a/src/Mapster/Adapters/BaseClassAdapter.cs +++ b/src/Mapster/Adapters/BaseClassAdapter.cs @@ -247,7 +247,7 @@ protected Expression CreateInstantiationExpression(Expression source, ClassMappi } else getter = member.Getter - .ApplyNullPropagationFromCtor(CreateAdaptExpression(member.Getter, member.DestinationMember.Type, arg, member), arg); + .ApplyNullPropagationFromCtor(CreateAdaptExpressionCore(member.Getter, member.DestinationMember.Type, arg, member), arg); if (member.Ignore.Condition != null) diff --git a/src/Mapster/Utils/ExpressionEx.cs b/src/Mapster/Utils/ExpressionEx.cs index 4e6af8a7..b7ffc365 100644 --- a/src/Mapster/Utils/ExpressionEx.cs +++ b/src/Mapster/Utils/ExpressionEx.cs @@ -483,7 +483,12 @@ public static Expression ApplyNullPropagationFromCtor(this Expression getter, Ex if (condition == null) return adapt; - return Expression.Condition(condition, adapt, adapt.Type.CreateDefault()); + // add supporting DestinationTransforms + var transform = arg.Settings.DestinationTransforms.Find(it => it.Condition(adapt.Type)); + if (transform != null) + return transform.TransformFunc(adapt.Type).Apply(arg.MapType, Expression.Condition(condition, adapt, Expression.Default(adapt.Type))); + + return Expression.Condition(condition, adapt, Expression.Default(adapt.Type)); } public static string? GetMemberPath(this LambdaExpression lambda, bool firstLevelOnly = false, bool noError = false) From d1dc7090d05c79ce1ba24930c8cbdec3f30fd78b Mon Sep 17 00:00:00 2001 From: DocSvartz Date: Tue, 12 May 2026 14:23:49 +0500 Subject: [PATCH 75/80] chore: Bump version to 10.0.8-pre06 --- 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 346f5174..b67bd22f 100644 --- a/src/Directory.Build.props +++ b/src/Directory.Build.props @@ -2,7 +2,7 @@ false - 10.0.8-pre05 + 10.0.8-pre06 netstandard2.0;net10.0;net9.0;net8.0 netstandard2.0;net10.0;net9.0;net8.0 net10.0;net9.0;net8.0 From e35dc294d5b3867bfc90cf64bebcd32d833fa522 Mon Sep 17 00:00:00 2001 From: Qyperion Date: Tue, 12 May 2026 17:23:42 +0300 Subject: [PATCH 76/80] feat(benchmark): fix and actualize Benchmark project --- src/.editorconfig | 6 +- .../Benchmark.Development.csproj | 38 ++-- .../Benchmarks/Config.cs | 8 +- .../Benchmarks/TestAll.cs | 6 +- .../Benchmarks/TestComplexTypes.cs | 8 +- .../Benchmarks/TestSimpleTypes.cs | 8 +- src/Benchmark.Development/Classes/Customer.cs | 4 +- src/Benchmark.Development/Classes/Foo.cs | 5 +- src/Benchmark.Development/MapsterVersion.cs | 4 +- src/Benchmark.Development/Program.cs | 4 +- src/Benchmark.Development/TestAdaptHelper.cs | 15 +- src/Benchmark/Benchmark.csproj | 97 +++++----- src/Benchmark/Benchmarks/Config.cs | 4 +- .../Benchmarks/MappingBenchmarkBase.cs | 12 ++ src/Benchmark/Benchmarks/TestAll.cs | 108 ++++++----- src/Benchmark/Benchmarks/TestComplexTypes.cs | 82 +++++---- src/Benchmark/Benchmarks/TestSimpleTypes.cs | 81 +++++---- src/Benchmark/Classes/Customer.cs | 4 +- src/Benchmark/Classes/Foo.cs | 5 +- src/Benchmark/Comparisons/FacetModels.cs | 22 +++ src/Benchmark/Comparisons/MapperlyModels.cs | 62 +++++++ src/Benchmark/CustomerMapper.g.cs | 171 +++++++++--------- src/Benchmark/CustomerMapper.tt | 4 +- src/Benchmark/FooMapper.g.cs | 131 +++++++------- src/Benchmark/FooMapper.tt | 4 +- src/Benchmark/Program.cs | 6 +- src/Benchmark/TestAdaptHelper.cs | 146 +++++++++------ 27 files changed, 605 insertions(+), 440 deletions(-) create mode 100644 src/Benchmark/Benchmarks/MappingBenchmarkBase.cs create mode 100644 src/Benchmark/Comparisons/FacetModels.cs create mode 100644 src/Benchmark/Comparisons/MapperlyModels.cs diff --git a/src/.editorconfig b/src/.editorconfig index 6600eeea..707ccf5f 100644 --- a/src/.editorconfig +++ b/src/.editorconfig @@ -1,4 +1,8 @@ -[*.cs] +[*.{csproj,props}] +indent_style = space +indent_size = 2 + +[*.cs] # S3220: Method calls should not resolve ambiguously to overloads with "params" dotnet_diagnostic.S3220.severity = suggestion diff --git a/src/Benchmark.Development/Benchmark.Development.csproj b/src/Benchmark.Development/Benchmark.Development.csproj index 2213d7a4..5b945fcf 100644 --- a/src/Benchmark.Development/Benchmark.Development.csproj +++ b/src/Benchmark.Development/Benchmark.Development.csproj @@ -3,14 +3,18 @@ Exe net10.0 - true - enable + 12.0 + enable - True + enable + Mapster.Benchmark.Development + + true Benchmark.Development.snk + True False - 7.4.0 - 12.0 + + 7.4.0 @@ -21,17 +25,17 @@ - - - - - - - - - - - + + + + + + + + - + + + + diff --git a/src/Benchmark.Development/Benchmarks/Config.cs b/src/Benchmark.Development/Benchmarks/Config.cs index 87dad0fe..9dfcf38b 100644 --- a/src/Benchmark.Development/Benchmarks/Config.cs +++ b/src/Benchmark.Development/Benchmarks/Config.cs @@ -1,14 +1,12 @@ -using Benchmark.Development; -using BenchmarkDotNet.Columns; +using BenchmarkDotNet.Columns; using BenchmarkDotNet.Configs; using BenchmarkDotNet.Diagnosers; using BenchmarkDotNet.Exporters; using BenchmarkDotNet.Exporters.Csv; using BenchmarkDotNet.Jobs; using BenchmarkDotNet.Loggers; -using Perfolizer.Models; -namespace Benchmark.Benchmarks +namespace Mapster.Benchmark.Development.Benchmarks { public class Config : ManualConfig { @@ -32,7 +30,7 @@ public Config() AddColumn(BaselineRatioColumn.RatioMean); AddColumnProvider(DefaultColumnProviders.Metrics); - + foreach (var version in MapsterVersion.Get()) diff --git a/src/Benchmark.Development/Benchmarks/TestAll.cs b/src/Benchmark.Development/Benchmarks/TestAll.cs index b3e28a76..5d0bac95 100644 --- a/src/Benchmark.Development/Benchmarks/TestAll.cs +++ b/src/Benchmark.Development/Benchmarks/TestAll.cs @@ -1,7 +1,7 @@ -using Benchmark.Classes; -using BenchmarkDotNet.Attributes; +using BenchmarkDotNet.Attributes; +using Mapster.Benchmark.Development.Classes; -namespace Benchmark.Benchmarks +namespace Mapster.Benchmark.Development.Benchmarks { public class TestAll { diff --git a/src/Benchmark.Development/Benchmarks/TestComplexTypes.cs b/src/Benchmark.Development/Benchmarks/TestComplexTypes.cs index 62d7a764..df4b3beb 100644 --- a/src/Benchmark.Development/Benchmarks/TestComplexTypes.cs +++ b/src/Benchmark.Development/Benchmarks/TestComplexTypes.cs @@ -1,7 +1,7 @@ -using Benchmark.Classes; -using BenchmarkDotNet.Attributes; +using BenchmarkDotNet.Attributes; +using Mapster.Benchmark.Development.Classes; -namespace Benchmark.Benchmarks +namespace Mapster.Benchmark.Development.Benchmarks { public class TestComplexTypes { @@ -15,7 +15,7 @@ public void MapsterTest() { TestAdaptHelper.TestMapsterAdapter(_customerInstance, Iterations); } - + [GlobalSetup(Target = nameof(MapsterTest))] public void SetupMapster() { diff --git a/src/Benchmark.Development/Benchmarks/TestSimpleTypes.cs b/src/Benchmark.Development/Benchmarks/TestSimpleTypes.cs index 8678a8ec..55aa0967 100644 --- a/src/Benchmark.Development/Benchmarks/TestSimpleTypes.cs +++ b/src/Benchmark.Development/Benchmarks/TestSimpleTypes.cs @@ -1,7 +1,7 @@ -using Benchmark.Classes; -using BenchmarkDotNet.Attributes; +using BenchmarkDotNet.Attributes; +using Mapster.Benchmark.Development.Classes; -namespace Benchmark.Benchmarks +namespace Mapster.Benchmark.Development.Benchmarks { public class TestSimpleTypes { @@ -15,7 +15,7 @@ public void MapsterTest() { TestAdaptHelper.TestMapsterAdapter(_fooInstance, Iterations); } - + [GlobalSetup(Target = nameof(MapsterTest))] public void SetupMapster() { diff --git a/src/Benchmark.Development/Classes/Customer.cs b/src/Benchmark.Development/Classes/Customer.cs index 5fac9cef..694d0b03 100644 --- a/src/Benchmark.Development/Classes/Customer.cs +++ b/src/Benchmark.Development/Classes/Customer.cs @@ -1,6 +1,4 @@ -using System.Collections.Generic; - -namespace Benchmark.Classes +namespace Mapster.Benchmark.Development.Classes { public class Address { diff --git a/src/Benchmark.Development/Classes/Foo.cs b/src/Benchmark.Development/Classes/Foo.cs index 063541b8..5e3fa21e 100644 --- a/src/Benchmark.Development/Classes/Foo.cs +++ b/src/Benchmark.Development/Classes/Foo.cs @@ -1,7 +1,4 @@ -using System; -using System.Collections.Generic; - -namespace Benchmark.Classes +namespace Mapster.Benchmark.Development.Classes { public class Foo { diff --git a/src/Benchmark.Development/MapsterVersion.cs b/src/Benchmark.Development/MapsterVersion.cs index bfca6a69..391f06d5 100644 --- a/src/Benchmark.Development/MapsterVersion.cs +++ b/src/Benchmark.Development/MapsterVersion.cs @@ -1,4 +1,4 @@ -namespace Benchmark.Development +namespace Mapster.Benchmark.Development { internal static class MapsterVersion { @@ -6,7 +6,7 @@ internal static class MapsterVersion internal static string[] Get() => [ "7.4.0", - "9.0.0-pre01" + "10.0.0" ]; } } diff --git a/src/Benchmark.Development/Program.cs b/src/Benchmark.Development/Program.cs index 32e641ed..4e560383 100644 --- a/src/Benchmark.Development/Program.cs +++ b/src/Benchmark.Development/Program.cs @@ -1,5 +1,5 @@ -using Benchmark.Benchmarks; -using BenchmarkDotNet.Running; +using BenchmarkDotNet.Running; +using Mapster.Benchmark.Development.Benchmarks; var switcher = new BenchmarkSwitcher(new[] { diff --git a/src/Benchmark.Development/TestAdaptHelper.cs b/src/Benchmark.Development/TestAdaptHelper.cs index c6e06739..9e92fa43 100644 --- a/src/Benchmark.Development/TestAdaptHelper.cs +++ b/src/Benchmark.Development/TestAdaptHelper.cs @@ -1,12 +1,11 @@ -using Benchmark.Classes; -using Mapster; +using Mapster.Benchmark.Development.Classes; using System.Linq.Expressions; -namespace Benchmark +namespace Mapster.Benchmark.Development { public static class TestAdaptHelper { - + public static Customer SetupCustomerInstance() { return new Customer @@ -64,8 +63,8 @@ private static void SetupCompiler(MapsterCompilerType type) TypeAdapterConfig.GlobalSettings.Compiler = type switch { MapsterCompilerType.Default => _defaultCompiler, - // MapsterCompilerType.Roslyn => exp => exp.CompileWithDebugInfo(), - // MapsterCompilerType.FEC => exp => exp.CompileFast(), + // MapsterCompilerType.Roslyn => exp => exp.CompileWithDebugInfo(), + // MapsterCompilerType.FEC => exp => exp.CompileFast(), _ => throw new ArgumentOutOfRangeException(nameof(type)), }; } @@ -75,14 +74,14 @@ public static void ConfigureMapster(Foo fooInstance, MapsterCompilerType type) TypeAdapterConfig.GlobalSettings.Compile(typeof(Foo), typeof(Foo)); //recompile fooInstance.Adapt(); //exercise } - + public static void ConfigureMapster(Customer customerInstance, MapsterCompilerType type) { SetupCompiler(type); TypeAdapterConfig.GlobalSettings.Compile(typeof(Customer), typeof(CustomerDTO)); //recompile customerInstance.Adapt(); //exercise } - + public static void TestMapsterAdapter(TSrc item, int iterations) where TSrc : class where TDest : class, new() diff --git a/src/Benchmark/Benchmark.csproj b/src/Benchmark/Benchmark.csproj index b6872ab8..451136bd 100644 --- a/src/Benchmark/Benchmark.csproj +++ b/src/Benchmark/Benchmark.csproj @@ -1,45 +1,54 @@ - - - - Exe - net9.0 - true - **/*.g.cs - - - - True - True - CustomerMapper.tt - - - True - True - FooMapper.tt - - - - - - - - - - - - - - - - TextTemplatingFileGenerator - CustomerMapper.g.cs - - - TextTemplatingFileGenerator - FooMapper.g.cs - - - - - + + + Exe + net10.0 + + Mapster.Benchmark + enable + + true + **/*.g.cs + + + + + + + + + + + + + + + + + + + True + True + CustomerMapper.tt + + + True + True + FooMapper.tt + + + + + + TextTemplatingFileGenerator + CustomerMapper.g.cs + + + TextTemplatingFileGenerator + FooMapper.g.cs + + + + + + \ No newline at end of file diff --git a/src/Benchmark/Benchmarks/Config.cs b/src/Benchmark/Benchmarks/Config.cs index 5779b353..fb45726c 100644 --- a/src/Benchmark/Benchmarks/Config.cs +++ b/src/Benchmark/Benchmarks/Config.cs @@ -6,7 +6,7 @@ using BenchmarkDotNet.Jobs; using BenchmarkDotNet.Loggers; -namespace Benchmark.Benchmarks +namespace Mapster.Benchmark.Benchmarks { public class Config : ManualConfig { @@ -20,12 +20,14 @@ public Config() AddDiagnoser(MemoryDiagnoser.Default); AddColumn(TargetMethodColumn.Method); + AddColumnProvider(DefaultColumnProviders.Params); AddColumn(StatisticColumn.Mean); AddColumn(StatisticColumn.StdDev); AddColumn(StatisticColumn.Error); AddColumn(BaselineRatioColumn.RatioMean); + AddColumn(BaselineAllocationRatioColumn.RatioMean); AddColumnProvider(DefaultColumnProviders.Metrics); AddJob(Job.ShortRun diff --git a/src/Benchmark/Benchmarks/MappingBenchmarkBase.cs b/src/Benchmark/Benchmarks/MappingBenchmarkBase.cs new file mode 100644 index 00000000..e5f5ab77 --- /dev/null +++ b/src/Benchmark/Benchmarks/MappingBenchmarkBase.cs @@ -0,0 +1,12 @@ +using BenchmarkDotNet.Attributes; + +namespace Mapster.Benchmark.Benchmarks +{ + public abstract class MappingBenchmarkBase + { + public IEnumerable MapOperationValues => new[] { 1_000, 10_000, 100_000, 1_000_000 }; + + [ParamsSource(nameof(MapOperationValues))] + public int MapOperations { get; set; } + } +} \ No newline at end of file diff --git a/src/Benchmark/Benchmarks/TestAll.cs b/src/Benchmark/Benchmarks/TestAll.cs index 17581ddf..f2ff5e0a 100644 --- a/src/Benchmark/Benchmarks/TestAll.cs +++ b/src/Benchmark/Benchmarks/TestAll.cs @@ -1,65 +1,69 @@ -using Benchmark.Classes; -using BenchmarkDotNet.Attributes; +using BenchmarkDotNet.Attributes; +using Mapster.Benchmark.Classes; -namespace Benchmark.Benchmarks +namespace Mapster.Benchmark.Benchmarks { - public class TestAll + public class TestAll : MappingBenchmarkBase { private Foo _fooInstance; private Customer _customerInstance; - [Params(100_000)]//, 1_000_000)] - public int Iterations { get; set; } - - [Benchmark(Description = $"Mapster {TestAdaptHelper.MapsterVersion}")] + [Benchmark(Baseline = true, Description = $"Mapster {TestAdaptHelper.MapsterVersion}")] public void MapsterTest() { - TestAdaptHelper.TestMapsterAdapter(_fooInstance, Iterations); - TestAdaptHelper.TestMapsterAdapter(_customerInstance, Iterations); + TestAdaptHelper.TestMapsterAdapter(_fooInstance, MapOperations); + TestAdaptHelper.TestMapsterAdapter(_customerInstance, MapOperations); } [Benchmark(Description = $"Mapster {TestAdaptHelper.MapsterVersion} (Roslyn)")] public void RoslynTest() { - TestAdaptHelper.TestMapsterAdapter(_fooInstance, Iterations); - TestAdaptHelper.TestMapsterAdapter(_customerInstance, Iterations); + TestAdaptHelper.TestMapsterAdapter(_fooInstance, MapOperations); + TestAdaptHelper.TestMapsterAdapter(_customerInstance, MapOperations); } [Benchmark(Description = $"Mapster {TestAdaptHelper.MapsterVersion} (FEC)")] public void FecTest() { - TestAdaptHelper.TestMapsterAdapter(_fooInstance, Iterations); - TestAdaptHelper.TestMapsterAdapter(_customerInstance, Iterations); + TestAdaptHelper.TestMapsterAdapter(_fooInstance, MapOperations); + TestAdaptHelper.TestMapsterAdapter(_customerInstance, MapOperations); } [Benchmark(Description = $"Mapster {TestAdaptHelper.MapsterVersion} (Codegen)")] public void CodegenTest() { - TestAdaptHelper.TestCodeGen(_fooInstance, Iterations); - TestAdaptHelper.TestCodeGen(_customerInstance, Iterations); + TestAdaptHelper.TestCodeGen(_fooInstance, MapOperations); + TestAdaptHelper.TestCodeGen(_customerInstance, MapOperations); + } + + [Benchmark(Description = $"AutoMapper {TestAdaptHelper.AutoMapperVersion}")] + public void AutoMapperTest() + { + TestAdaptHelper.TestAutoMapper(_fooInstance, MapOperations); + TestAdaptHelper.TestAutoMapper(_customerInstance, MapOperations); } - [Benchmark(Description = $"ExpressMapper {TestAdaptHelper.ExpressionMapperVersion}")] - public void ExpressMapperTest() + [Benchmark(Description = $"Facet {TestAdaptHelper.FacetVersion}")] + public void FacetTest() { - TestAdaptHelper.TestExpressMapper(_fooInstance, Iterations); - TestAdaptHelper.TestExpressMapper(_customerInstance, Iterations); + TestAdaptHelper.TestFacet(_fooInstance, MapOperations); + TestAdaptHelper.TestFacet(_customerInstance, MapOperations); } - //[Benchmark(Description = $"AutoMapper {TestAdaptHelper.AutoMapperVersion}")] - //public void AutoMapperTest() - //{ - // TestAdaptHelper.TestAutoMapper(_fooInstance, Iterations); - // TestAdaptHelper.TestAutoMapper(_customerInstance, Iterations); - //} + [Benchmark(Description = $"Mapperly {TestAdaptHelper.MapperlyVersion}")] + public void MapperlyTest() + { + TestAdaptHelper.TestMapperly(_fooInstance, MapOperations); + TestAdaptHelper.TestMapperly(_customerInstance, MapOperations); + } [GlobalSetup(Target = nameof(MapsterTest))] public void SetupMapster() { _fooInstance = TestAdaptHelper.SetupFooInstance(); _customerInstance = TestAdaptHelper.SetupCustomerInstance(); - TestAdaptHelper.ConfigureMapster(_fooInstance, MapsterCompilerType.Default); - TestAdaptHelper.ConfigureMapster(_customerInstance, MapsterCompilerType.Default); + TestAdaptHelper.ConfigureMapster(_fooInstance, MapsterCompilerType.Default); + TestAdaptHelper.ConfigureMapster(_customerInstance, MapsterCompilerType.Default); } [GlobalSetup(Target = nameof(RoslynTest))] @@ -67,8 +71,8 @@ public void SetupRoslyn() { _fooInstance = TestAdaptHelper.SetupFooInstance(); _customerInstance = TestAdaptHelper.SetupCustomerInstance(); - TestAdaptHelper.ConfigureMapster(_fooInstance, MapsterCompilerType.Roslyn); - TestAdaptHelper.ConfigureMapster(_customerInstance, MapsterCompilerType.Roslyn); + TestAdaptHelper.ConfigureMapster(_fooInstance, MapsterCompilerType.Roslyn); + TestAdaptHelper.ConfigureMapster(_customerInstance, MapsterCompilerType.Roslyn); } [GlobalSetup(Target = nameof(FecTest))] @@ -76,36 +80,44 @@ public void SetupFec() { _fooInstance = TestAdaptHelper.SetupFooInstance(); _customerInstance = TestAdaptHelper.SetupCustomerInstance(); - TestAdaptHelper.ConfigureMapster(_fooInstance, MapsterCompilerType.FEC); - TestAdaptHelper.ConfigureMapster(_customerInstance, MapsterCompilerType.FEC); + TestAdaptHelper.ConfigureMapster(_fooInstance, MapsterCompilerType.FEC); + TestAdaptHelper.ConfigureMapster(_customerInstance, MapsterCompilerType.FEC); } [GlobalSetup(Target = nameof(CodegenTest))] public void SetupCodegen() { - //_fooInstance = TestAdaptHelper.SetupFooInstance(); - //_customerInstance = TestAdaptHelper.SetupCustomerInstance(); - //FooMapper.Map(_fooInstance); - //CustomerMapper.Map(_customerInstance); + _fooInstance = TestAdaptHelper.SetupFooInstance(); + _customerInstance = TestAdaptHelper.SetupCustomerInstance(); + _ = FooMapper.Map(_fooInstance); + _ = CustomerMapper.Map(_customerInstance); } - [GlobalSetup(Target = nameof(ExpressMapperTest))] - public void SetupExpressMapper() + [GlobalSetup(Target = nameof(FacetTest))] + public void SetupFacet() { _fooInstance = TestAdaptHelper.SetupFooInstance(); _customerInstance = TestAdaptHelper.SetupCustomerInstance(); - TestAdaptHelper.ConfigureExpressMapper(_fooInstance); - TestAdaptHelper.ConfigureExpressMapper(_customerInstance); + TestAdaptHelper.ConfigureFacet(_fooInstance); + TestAdaptHelper.ConfigureFacet(_customerInstance); } - //[GlobalSetup(Target = nameof(AutoMapperTest))] - //public void SetupAutoMapper() - //{ - // _fooInstance = TestAdaptHelper.SetupFooInstance(); - // _customerInstance = TestAdaptHelper.SetupCustomerInstance(); - // TestAdaptHelper.ConfigureAutoMapper(_fooInstance); - // TestAdaptHelper.ConfigureAutoMapper(_customerInstance); - //} + [GlobalSetup(Target = nameof(MapperlyTest))] + public void SetupMapperly() + { + _fooInstance = TestAdaptHelper.SetupFooInstance(); + _customerInstance = TestAdaptHelper.SetupCustomerInstance(); + TestAdaptHelper.ConfigureMapperly(_fooInstance); + TestAdaptHelper.ConfigureMapperly(_customerInstance); + } + [GlobalSetup(Target = nameof(AutoMapperTest))] + public void SetupAutoMapper() + { + _fooInstance = TestAdaptHelper.SetupFooInstance(); + _customerInstance = TestAdaptHelper.SetupCustomerInstance(); + TestAdaptHelper.ConfigureAutoMapper(_fooInstance); + TestAdaptHelper.ConfigureAutoMapper(_customerInstance); + } } } \ No newline at end of file diff --git a/src/Benchmark/Benchmarks/TestComplexTypes.cs b/src/Benchmark/Benchmarks/TestComplexTypes.cs index a61ca9d3..71cb2e8a 100644 --- a/src/Benchmark/Benchmarks/TestComplexTypes.cs +++ b/src/Benchmark/Benchmarks/TestComplexTypes.cs @@ -1,91 +1,101 @@ -using Benchmark.Classes; -using BenchmarkDotNet.Attributes; +using BenchmarkDotNet.Attributes; +using Mapster.Benchmark.Classes; -namespace Benchmark.Benchmarks +namespace Mapster.Benchmark.Benchmarks { - public class TestComplexTypes + public class TestComplexTypes : MappingBenchmarkBase { private Customer _customerInstance; - [Params(1000, 10_000, 100_000, 1_000_000)] - public int Iterations { get; set; } - - [Benchmark] + [Benchmark(Baseline = true, Description = $"Mapster {TestAdaptHelper.MapsterVersion}")] public void MapsterTest() { - TestAdaptHelper.TestMapsterAdapter(_customerInstance, Iterations); + TestAdaptHelper.TestMapsterAdapter(_customerInstance, MapOperations); } - + [Benchmark(Description = $"Mapster {TestAdaptHelper.MapsterVersion} (Roslyn)")] public void RoslynTest() { - TestAdaptHelper.TestMapsterAdapter(_customerInstance, Iterations); + TestAdaptHelper.TestMapsterAdapter(_customerInstance, MapOperations); } [Benchmark(Description = $"Mapster {TestAdaptHelper.MapsterVersion} (FEC)")] public void FecTest() { - TestAdaptHelper.TestMapsterAdapter(_customerInstance, Iterations); + TestAdaptHelper.TestMapsterAdapter(_customerInstance, MapOperations); } - [Benchmark] + [Benchmark(Description = $"Mapster {TestAdaptHelper.MapsterVersion} (Codegen)")] public void CodegenTest() { - TestAdaptHelper.TestCodeGen(_customerInstance, Iterations); + TestAdaptHelper.TestCodeGen(_customerInstance, MapOperations); + } + + [Benchmark(Description = $"AutoMapper {TestAdaptHelper.AutoMapperVersion}")] + public void AutoMapperTest() + { + TestAdaptHelper.TestAutoMapper(_customerInstance, MapOperations); } - [Benchmark] - public void ExpressMapperTest() + [Benchmark(Description = $"Facet {TestAdaptHelper.FacetVersion}")] + public void FacetTest() { - TestAdaptHelper.TestExpressMapper(_customerInstance, Iterations); + TestAdaptHelper.TestFacet(_customerInstance, MapOperations); } - //[Benchmark] - //public void AutoMapperTest() - //{ - // TestAdaptHelper.TestAutoMapper(_customerInstance, Iterations); - //} + [Benchmark(Description = $"Mapperly {TestAdaptHelper.MapperlyVersion}")] + public void MapperlyTest() + { + TestAdaptHelper.TestMapperly(_customerInstance, MapOperations); + } [GlobalSetup(Target = nameof(MapsterTest))] public void SetupMapster() { _customerInstance = TestAdaptHelper.SetupCustomerInstance(); - TestAdaptHelper.ConfigureMapster(_customerInstance, MapsterCompilerType.Default); + TestAdaptHelper.ConfigureMapster(_customerInstance, MapsterCompilerType.Default); } [GlobalSetup(Target = nameof(RoslynTest))] public void SetupRoslyn() { _customerInstance = TestAdaptHelper.SetupCustomerInstance(); - TestAdaptHelper.ConfigureMapster(_customerInstance, MapsterCompilerType.Roslyn); + TestAdaptHelper.ConfigureMapster(_customerInstance, MapsterCompilerType.Roslyn); } [GlobalSetup(Target = nameof(FecTest))] public void SetupFec() { _customerInstance = TestAdaptHelper.SetupCustomerInstance(); - TestAdaptHelper.ConfigureMapster(_customerInstance, MapsterCompilerType.FEC); + TestAdaptHelper.ConfigureMapster(_customerInstance, MapsterCompilerType.FEC); } [GlobalSetup(Target = nameof(CodegenTest))] public void SetupCodegen() { - //_customerInstance = TestAdaptHelper.SetupCustomerInstance(); - //CustomerMapper.Map(_customerInstance); + _customerInstance = TestAdaptHelper.SetupCustomerInstance(); + _ = CustomerMapper.Map(_customerInstance); } - [GlobalSetup(Target = nameof(ExpressMapperTest))] - public void SetupExpressMapper() + [GlobalSetup(Target = nameof(FacetTest))] + public void SetupFacet() { _customerInstance = TestAdaptHelper.SetupCustomerInstance(); - TestAdaptHelper.ConfigureExpressMapper(_customerInstance); + TestAdaptHelper.ConfigureFacet(_customerInstance); } - //[GlobalSetup(Target = nameof(AutoMapperTest))] - //public void SetupAutoMapper() - //{ - // _customerInstance = TestAdaptHelper.SetupCustomerInstance(); - // TestAdaptHelper.ConfigureAutoMapper(_customerInstance); - //} + [GlobalSetup(Target = nameof(MapperlyTest))] + public void SetupMapperly() + { + _customerInstance = TestAdaptHelper.SetupCustomerInstance(); + TestAdaptHelper.ConfigureMapperly(_customerInstance); + } + + [GlobalSetup(Target = nameof(AutoMapperTest))] + public void SetupAutoMapper() + { + _customerInstance = TestAdaptHelper.SetupCustomerInstance(); + TestAdaptHelper.ConfigureAutoMapper(_customerInstance); + } } } \ No newline at end of file diff --git a/src/Benchmark/Benchmarks/TestSimpleTypes.cs b/src/Benchmark/Benchmarks/TestSimpleTypes.cs index 81f507e5..8f6466da 100644 --- a/src/Benchmark/Benchmarks/TestSimpleTypes.cs +++ b/src/Benchmark/Benchmarks/TestSimpleTypes.cs @@ -1,92 +1,101 @@ -using Benchmark.Classes; -using BenchmarkDotNet.Attributes; +using BenchmarkDotNet.Attributes; +using Mapster.Benchmark.Classes; -namespace Benchmark.Benchmarks +namespace Mapster.Benchmark.Benchmarks { - public class TestSimpleTypes + public class TestSimpleTypes : MappingBenchmarkBase { private Foo _fooInstance; - [Params(1000, 10_000, 100_000, 1_000_000)] - public int Iterations { get; set; } - - [Benchmark] + [Benchmark(Baseline = true, Description = $"Mapster {TestAdaptHelper.MapsterVersion}")] public void MapsterTest() { - TestAdaptHelper.TestMapsterAdapter(_fooInstance, Iterations); + TestAdaptHelper.TestMapsterAdapter(_fooInstance, MapOperations); } - + [Benchmark(Description = $"Mapster {TestAdaptHelper.MapsterVersion} (Roslyn)")] public void RoslynTest() { - TestAdaptHelper.TestMapsterAdapter(_fooInstance, Iterations); + TestAdaptHelper.TestMapsterAdapter(_fooInstance, MapOperations); } [Benchmark(Description = $"Mapster {TestAdaptHelper.MapsterVersion} (FEC)")] public void FecTest() { - TestAdaptHelper.TestMapsterAdapter(_fooInstance, Iterations); + TestAdaptHelper.TestMapsterAdapter(_fooInstance, MapOperations); } - [Benchmark] + [Benchmark(Description = $"Mapster {TestAdaptHelper.MapsterVersion} (Codegen)")] public void CodegenTest() { - TestAdaptHelper.TestCodeGen(_fooInstance, Iterations); + TestAdaptHelper.TestCodeGen(_fooInstance, MapOperations); } - [Benchmark] - public void ExpressMapperTest() + [Benchmark(Description = $"AutoMapper {TestAdaptHelper.AutoMapperVersion}")] + public void AutoMapperTest() { - TestAdaptHelper.TestExpressMapper(_fooInstance, Iterations); + TestAdaptHelper.TestAutoMapper(_fooInstance, MapOperations); } - //[Benchmark] - //public void AutoMapperTest() - //{ - // TestAdaptHelper.TestAutoMapper(_fooInstance, Iterations); - //} + [Benchmark(Description = $"Facet {TestAdaptHelper.FacetVersion}")] + public void FacetTest() + { + TestAdaptHelper.TestFacet(_fooInstance, MapOperations); + } + [Benchmark(Description = $"Mapperly {TestAdaptHelper.MapperlyVersion}")] + public void MapperlyTest() + { + TestAdaptHelper.TestMapperly(_fooInstance, MapOperations); + } [GlobalSetup(Target = nameof(MapsterTest))] public void SetupMapster() { _fooInstance = TestAdaptHelper.SetupFooInstance(); - TestAdaptHelper.ConfigureMapster(_fooInstance, MapsterCompilerType.Default); + TestAdaptHelper.ConfigureMapster(_fooInstance, MapsterCompilerType.Default); } [GlobalSetup(Target = nameof(RoslynTest))] public void SetupRoslyn() { _fooInstance = TestAdaptHelper.SetupFooInstance(); - TestAdaptHelper.ConfigureMapster(_fooInstance, MapsterCompilerType.Roslyn); + TestAdaptHelper.ConfigureMapster(_fooInstance, MapsterCompilerType.Roslyn); } [GlobalSetup(Target = nameof(FecTest))] public void SetupFec() { _fooInstance = TestAdaptHelper.SetupFooInstance(); - TestAdaptHelper.ConfigureMapster(_fooInstance, MapsterCompilerType.FEC); + TestAdaptHelper.ConfigureMapster(_fooInstance, MapsterCompilerType.FEC); } [GlobalSetup(Target = nameof(CodegenTest))] public void SetupCodegen() { - //_fooInstance = TestAdaptHelper.SetupFooInstance(); - //FooMapper.Map(_fooInstance); + _fooInstance = TestAdaptHelper.SetupFooInstance(); + _ = FooMapper.Map(_fooInstance); + } + + [GlobalSetup(Target = nameof(FacetTest))] + public void SetupFacet() + { + _fooInstance = TestAdaptHelper.SetupFooInstance(); + TestAdaptHelper.ConfigureFacet(_fooInstance); } - [GlobalSetup(Target = nameof(ExpressMapperTest))] - public void SetupExpressMapper() + [GlobalSetup(Target = nameof(MapperlyTest))] + public void SetupMapperly() { _fooInstance = TestAdaptHelper.SetupFooInstance(); - TestAdaptHelper.ConfigureExpressMapper(_fooInstance); + TestAdaptHelper.ConfigureMapperly(_fooInstance); } - //[GlobalSetup(Target = nameof(AutoMapperTest))] - //public void SetupAutoMapper() - //{ - // _fooInstance = TestAdaptHelper.SetupFooInstance(); - // TestAdaptHelper.ConfigureAutoMapper(_fooInstance); - //} + [GlobalSetup(Target = nameof(AutoMapperTest))] + public void SetupAutoMapper() + { + _fooInstance = TestAdaptHelper.SetupFooInstance(); + TestAdaptHelper.ConfigureAutoMapper(_fooInstance); + } } } \ No newline at end of file diff --git a/src/Benchmark/Classes/Customer.cs b/src/Benchmark/Classes/Customer.cs index 5fac9cef..30c832c6 100644 --- a/src/Benchmark/Classes/Customer.cs +++ b/src/Benchmark/Classes/Customer.cs @@ -1,6 +1,4 @@ -using System.Collections.Generic; - -namespace Benchmark.Classes +namespace Mapster.Benchmark.Classes { public class Address { diff --git a/src/Benchmark/Classes/Foo.cs b/src/Benchmark/Classes/Foo.cs index 063541b8..f12d3edb 100644 --- a/src/Benchmark/Classes/Foo.cs +++ b/src/Benchmark/Classes/Foo.cs @@ -1,7 +1,4 @@ -using System; -using System.Collections.Generic; - -namespace Benchmark.Classes +namespace Mapster.Benchmark.Classes { public class Foo { diff --git a/src/Benchmark/Comparisons/FacetModels.cs b/src/Benchmark/Comparisons/FacetModels.cs new file mode 100644 index 00000000..ca6330b5 --- /dev/null +++ b/src/Benchmark/Comparisons/FacetModels.cs @@ -0,0 +1,22 @@ +using Facet; +using Mapster.Benchmark.Classes; + +namespace Mapster.Benchmark.Comparisons +{ + [Facet(typeof(Foo), NestedFacets = new[] { typeof(FooFacetDto) }, MaxDepth = 2)] + public partial class FooFacetDto + { + } + + [Facet(typeof(Address))] + public partial class AddressFacetDto + { + } + + [Facet(typeof(Customer), NestedFacets = new[] { typeof(AddressFacetDto) })] + public partial class CustomerFacetDto + { + [MapFrom("Address.City")] + public string AddressCity { get; set; } + } +} \ No newline at end of file diff --git a/src/Benchmark/Comparisons/MapperlyModels.cs b/src/Benchmark/Comparisons/MapperlyModels.cs new file mode 100644 index 00000000..32fac63a --- /dev/null +++ b/src/Benchmark/Comparisons/MapperlyModels.cs @@ -0,0 +1,62 @@ +using Mapster.Benchmark.Classes; +using Riok.Mapperly.Abstractions; + +namespace Mapster.Benchmark.Comparisons +{ + public class FooMapperlyDto + { + public string Name { get; set; } + public int Int32 { get; set; } + public long Int64 { get; set; } + public int? NullInt { get; set; } + public float Floatn { get; set; } + public double Doublen { get; set; } + public DateTime DateTime { get; set; } + public FooMapperlyDto Foo1 { get; set; } + public IEnumerable Foos { get; set; } + public FooMapperlyDto[] FooArr { get; set; } + public int[] IntArr { get; set; } + public IEnumerable Ints { get; set; } + } + + public class AddressMapperlyDto + { + public int Id { get; set; } + public string Street { get; set; } + public string City { get; set; } + public string Country { get; set; } + } + + public class AddressSummaryMapperlyDto + { + public int Id { get; set; } + public string City { get; set; } + public string Country { get; set; } + } + + public class CustomerMapperlyDto + { + public int Id { get; set; } + public string Name { get; set; } + public AddressMapperlyDto Address { get; set; } + public AddressSummaryMapperlyDto HomeAddress { get; set; } + public AddressSummaryMapperlyDto[] Addresses { get; set; } + public List WorkAddresses { get; set; } + public string AddressCity { get; set; } + } + + [Riok.Mapperly.Abstractions.Mapper(UseDeepCloning = true)] + public static partial class MapperlyMappings + { + public static partial AddressMapperlyDto MapAddress(Address source); + + [MapperIgnoreSource(nameof(Address.Street))] + public static partial AddressSummaryMapperlyDto MapAddressSummary(Address source); + + public static partial FooMapperlyDto MapFoo(Foo source); + + [MapperIgnoreSource(nameof(Customer.Credit))] + [MapProperty("Address.City", nameof(CustomerMapperlyDto.AddressCity))] + public static partial CustomerMapperlyDto MapCustomer(Customer source); + } +} \ No newline at end of file diff --git a/src/Benchmark/CustomerMapper.g.cs b/src/Benchmark/CustomerMapper.g.cs index 947ff47d..7b4dbd27 100644 --- a/src/Benchmark/CustomerMapper.g.cs +++ b/src/Benchmark/CustomerMapper.g.cs @@ -1,89 +1,88 @@ - -using System.Collections.Generic; -using Benchmark.Classes; +using System.Collections.Generic; +using Mapster.Benchmark.Classes; -namespace Benchmark +namespace Mapster.Benchmark { - public static partial class CustomerMapper - { - public static CustomerDTO Map(Customer p1) - { - return p1 == null ? null : new CustomerDTO() - { - Id = p1.Id, - Name = p1.Name, - Address = p1.Address == null ? null : new Address() - { - Id = p1.Address.Id, - Street = p1.Address.Street, - City = p1.Address.City, - Country = p1.Address.Country - }, - HomeAddress = p1.HomeAddress == null ? null : new AddressDTO() - { - Id = p1.HomeAddress.Id, - City = p1.HomeAddress.City, - Country = p1.HomeAddress.Country - }, - Addresses = func1(p1.Addresses), - WorkAddresses = func2(p1.WorkAddresses), - AddressCity = p1.Address == null ? null : p1.Address.City - }; - } - - private static AddressDTO[] func1(Address[] p2) - { - if (p2 == null) - { - return null; - } - AddressDTO[] result = new AddressDTO[p2.Length]; - - int v = 0; - - int i = 0; - int len = p2.Length; - - while (i < len) - { - Address item = p2[i]; - result[v++] = item == null ? null : new AddressDTO() - { - Id = item.Id, - City = item.City, - Country = item.Country - }; - i++; - } - return result; - - } - - private static List func2(ICollection
p3) - { - if (p3 == null) - { - return null; - } - List result = new List(p3.Count); - - ICollection list = result; - - IEnumerator
enumerator = p3.GetEnumerator(); - - while (enumerator.MoveNext()) - { - Address item = enumerator.Current; - list.Add(item == null ? null : new AddressDTO() - { - Id = item.Id, - City = item.City, - Country = item.Country - }); - } - return result; - - } - } -} + public static partial class CustomerMapper + { + public static CustomerDTO Map(Customer p1) + { + return p1 == null ? null : new CustomerDTO() + { + Id = p1.Id, + Name = p1.Name, + Address = p1.Address == null ? null : new Address() + { + Id = p1.Address.Id, + Street = p1.Address.Street, + City = p1.Address.City, + Country = p1.Address.Country + }, + HomeAddress = p1.HomeAddress == null ? null : new AddressDTO() + { + Id = p1.HomeAddress.Id, + City = p1.HomeAddress.City, + Country = p1.HomeAddress.Country + }, + Addresses = func1(p1.Addresses), + WorkAddresses = func2(p1.WorkAddresses), + AddressCity = p1.Address == null ? null : p1.Address.City + }; + } + + private static AddressDTO[] func1(Address[] p2) + { + if (p2 == null) + { + return null; + } + AddressDTO[] result = new AddressDTO[p2.Length]; + + int v = 0; + + int i = 0; + int len = p2.Length; + + while (i < len) + { + Address item = p2[i]; + result[v++] = item == null ? null : new AddressDTO() + { + Id = item.Id, + City = item.City, + Country = item.Country + }; + i++; + } + return result; + + } + + private static List func2(ICollection
p3) + { + if (p3 == null) + { + return null; + } + List result = new List(p3.Count); + + ICollection list = result; + + IEnumerator
enumerator = p3.GetEnumerator(); + + while (enumerator.MoveNext()) + { + Address item = enumerator.Current; + list.Add(item == null ? null : new AddressDTO() + { + Id = item.Id, + City = item.City, + Country = item.Country + }); + } + return result; + + } + } +} \ No newline at end of file diff --git a/src/Benchmark/CustomerMapper.tt b/src/Benchmark/CustomerMapper.tt index 7b316abd..3fa01aa8 100644 --- a/src/Benchmark/CustomerMapper.tt +++ b/src/Benchmark/CustomerMapper.tt @@ -8,7 +8,7 @@ <#@ Assembly Name="$(TargetDir)/$(ProjectName).dll" #> <#@ Assembly Name="$(TargetDir)/Mapster.dll" #> <#@ Assembly Name="$(TargetDir)/ExpressionTranslator.dll" #> -<#@ import namespace="Benchmark.Classes" #> +<#@ import namespace="Mapster.Benchmark.Classes" #> <#@ import namespace="ExpressionDebugger" #> <#@ import namespace="Mapster" #> <# @@ -18,7 +18,7 @@ { IsStatic = true, MethodName = "Map", - Namespace = "Benchmark", + Namespace = "Mapster.Benchmark", TypeName = "CustomerMapper" }; var code = foo.BuildAdapter() diff --git a/src/Benchmark/FooMapper.g.cs b/src/Benchmark/FooMapper.g.cs index 21edb8f9..dcebe0af 100644 --- a/src/Benchmark/FooMapper.g.cs +++ b/src/Benchmark/FooMapper.g.cs @@ -1,72 +1,71 @@ - -using System; +using System; using System.Linq; -using Benchmark.Classes; using Mapster; +using Mapster.Benchmark.Classes; using Mapster.Utils; -namespace Benchmark +namespace Mapster.Benchmark { - public static partial class FooMapper - { - public static Foo Map(Foo p1) - { - return p1 == null ? null : new Foo() - { - Name = p1.Name, - Int32 = p1.Int32, - Int64 = p1.Int64, - NullInt = p1.NullInt, - Floatn = p1.Floatn, - Doublen = p1.Doublen, - DateTime = p1.DateTime, - Foo1 = Map(p1.Foo1), - Foos = p1.Foos == null ? null : p1.Foos.Select(func1), - FooArr = func2(p1.FooArr), - IntArr = func3(p1.IntArr), - Ints = p1.Ints == null ? null : MapsterHelper.ToEnumerable(p1.Ints) - }; - } - - private static Foo func1(Foo p2) - { - return Map(p2); - } - - private static Foo[] func2(Foo[] p3) - { - if (p3 == null) - { - return null; - } - Foo[] result = new Foo[p3.Length]; - - int v = 0; - - int i = 0; - int len = p3.Length; - - while (i < len) - { - Foo item = p3[i]; - result[v++] = Map(item); - i++; - } - return result; - - } - - private static int[] func3(int[] p4) - { - if (p4 == null) - { - return null; - } - int[] result = new int[p4.Length]; - Array.Copy(p4, 0, result, 0, p4.Length); - return result; - - } - } -} + public static partial class FooMapper + { + public static Foo Map(Foo p1) + { + return p1 == null ? null : new Foo() + { + Name = p1.Name, + Int32 = p1.Int32, + Int64 = p1.Int64, + NullInt = p1.NullInt, + Floatn = p1.Floatn, + Doublen = p1.Doublen, + DateTime = p1.DateTime, + Foo1 = Map(p1.Foo1), + Foos = p1.Foos == null ? null : p1.Foos.Select(func1), + FooArr = func2(p1.FooArr), + IntArr = func3(p1.IntArr), + Ints = p1.Ints == null ? null : MapsterHelper.ToEnumerable(p1.Ints) + }; + } + + private static Foo func1(Foo p2) + { + return Map(p2); + } + + private static Foo[] func2(Foo[] p3) + { + if (p3 == null) + { + return null; + } + Foo[] result = new Foo[p3.Length]; + + int v = 0; + + int i = 0; + int len = p3.Length; + + while (i < len) + { + Foo item = p3[i]; + result[v++] = Map(item); + i++; + } + return result; + + } + + private static int[] func3(int[] p4) + { + if (p4 == null) + { + return null; + } + int[] result = new int[p4.Length]; + Array.Copy(p4, 0, result, 0, p4.Length); + return result; + + } + } +} \ No newline at end of file diff --git a/src/Benchmark/FooMapper.tt b/src/Benchmark/FooMapper.tt index 5279368d..003e94e1 100644 --- a/src/Benchmark/FooMapper.tt +++ b/src/Benchmark/FooMapper.tt @@ -8,7 +8,7 @@ <#@ Assembly Name="$(TargetDir)/Benchmark.dll" #> <#@ Assembly Name="$(TargetDir)/Mapster.dll" #> <#@ Assembly Name="$(TargetDir)/ExpressionTranslator.dll" #> -<#@ import namespace="Benchmark.Classes" #> +<#@ import namespace="Mapster.Benchmark.Classes" #> <#@ import namespace="ExpressionDebugger" #> <#@ import namespace="Mapster" #> <# @@ -18,7 +18,7 @@ { IsStatic = true, MethodName = "Map", - Namespace = "Benchmark", + Namespace = "Mapster.Benchmark", TypeName = "FooMapper" }; var code = foo.BuildAdapter() diff --git a/src/Benchmark/Program.cs b/src/Benchmark/Program.cs index 9c11da46..80c03f9a 100644 --- a/src/Benchmark/Program.cs +++ b/src/Benchmark/Program.cs @@ -1,7 +1,7 @@ -using Benchmark.Benchmarks; -using BenchmarkDotNet.Running; +using BenchmarkDotNet.Running; +using Mapster.Benchmark.Benchmarks; -namespace Benchmark +namespace Mapster.Benchmark { class Program { diff --git a/src/Benchmark/TestAdaptHelper.cs b/src/Benchmark/TestAdaptHelper.cs index cf8918e2..3f23a6ee 100644 --- a/src/Benchmark/TestAdaptHelper.cs +++ b/src/Benchmark/TestAdaptHelper.cs @@ -1,26 +1,30 @@ -using Benchmark.Classes; +using AutoMapper; using FastExpressionCompiler; -using Mapster; -using System; -using System.Collections.Generic; +using Mapster.Benchmark.Classes; +using Mapster.Benchmark.Comparisons; using System.Linq.Expressions; -namespace Benchmark +namespace Mapster.Benchmark { public static class TestAdaptHelper { - //private static readonly IMapper _mapper = new Mapper(new MapperConfiguration(cfg => - //{ - // cfg.CreateMap(); - // cfg.CreateMap(); - // cfg.CreateMap(); - // cfg.CreateMap(); - //})); - - public const string MapsterVersion = "10.0.0"; - public const string AutoMapperVersion = "13.0.0"; - public const string ExpressionTranslatorVersion = "2.5.0"; - public const string ExpressionMapperVersion = "1.9.1"; + private static readonly MapperConfiguration AutoMapperConfiguration = new(cfg => + { + cfg.CreateMap(); + cfg.CreateMap(); + cfg.CreateMap(); + cfg.CreateMap() + .ForMember(destination => destination.AddressCity, + options => options.MapFrom(source => source.Address != null ? source.Address.City : null)); + }); + + private static readonly IMapper AutoMapperInstance = CreateAutoMapper(); + private static readonly Func DefaultCompiler = TypeAdapterConfig.GlobalSettings.Compiler; + + public const string MapsterVersion = "10.0.7"; + public const string AutoMapperVersion = "14.0.0"; + public const string FacetVersion = "6.5.5"; + public const string MapperlyVersion = "4.3.1"; public static Customer SetupCustomerInstance() { @@ -72,82 +76,112 @@ public static Foo SetupFooInstance() }; } - private static readonly Func _defaultCompiler = TypeAdapterConfig.GlobalSettings.Compiler; + private static IMapper CreateAutoMapper() + { + AutoMapperConfiguration.AssertConfigurationIsValid(); + AutoMapperConfiguration.CompileMappings(); + return AutoMapperConfiguration.CreateMapper(); + } private static void SetupCompiler(MapsterCompilerType type) { TypeAdapterConfig.GlobalSettings.Compiler = type switch { - MapsterCompilerType.Default => _defaultCompiler, - MapsterCompilerType.Roslyn => exp => exp.CompileWithDebugInfo(), - MapsterCompilerType.FEC => exp => exp.CompileFast(), + MapsterCompilerType.Default => DefaultCompiler, + MapsterCompilerType.Roslyn => expression => expression.CompileWithDebugInfo(), + MapsterCompilerType.FEC => expression => expression.CompileFast(), _ => throw new ArgumentOutOfRangeException(nameof(type)), }; } - public static void ConfigureMapster(Foo fooInstance, MapsterCompilerType type) + + public static void ConfigureMapster(TSource sourceInstance, MapsterCompilerType type) + where TSource : class + where TDestination : class { SetupCompiler(type); - TypeAdapterConfig.GlobalSettings.Compile(typeof(Foo), typeof(Foo)); //recompile - fooInstance.Adapt(); //exercise + TypeAdapterConfig.GlobalSettings.Compile(typeof(TSource), typeof(TDestination)); + _ = sourceInstance.Adapt(); + } + + public static void ConfigureAutoMapper(TSource sourceInstance) + where TSource : class + { + _ = AutoMapperInstance.Map(sourceInstance); } - public static void ConfigureExpressMapper(Foo fooInstance) + + public static void ConfigureFacet(Foo sourceInstance) { - //ExpressMapper.Mapper.Map(fooInstance); //exercise + _ = new FooFacetDto(sourceInstance); } - //public static void ConfigureAutoMapper(Foo fooInstance) - //{ - // _mapper.Map(fooInstance); //exercise - //} - public static void ConfigureMapster(Customer customerInstance, MapsterCompilerType type) + public static void ConfigureFacet(Customer sourceInstance) { - SetupCompiler(type); - TypeAdapterConfig.GlobalSettings.Compile(typeof(Customer), typeof(CustomerDTO)); //recompile - customerInstance.Adapt(); //exercise + _ = new CustomerFacetDto(sourceInstance); + } + + public static void ConfigureMapperly(Foo sourceInstance) + { + _ = MapperlyMappings.MapFoo(sourceInstance); } - public static void ConfigureExpressMapper(Customer customerInstance) + + public static void ConfigureMapperly(Customer sourceInstance) { - //ExpressMapper.Mapper.Map(customerInstance); //exercise + _ = MapperlyMappings.MapCustomer(sourceInstance); } - //public static void ConfigureAutoMapper(Customer customerInstance) - //{ - // _mapper.Map(customerInstance); //exercise - //} - public static void TestMapsterAdapter(TSrc item, int iterations) + public static void TestMapsterAdapter(TSrc item, int mapOperations) where TSrc : class where TDest : class, new() { - Loop(item, get => get.Adapt(), iterations); + Loop(item, source => source.Adapt(), mapOperations); } - public static void TestExpressMapper(TSrc item, int iterations) + public static void TestAutoMapper(TSrc item, int mapOperations) where TSrc : class where TDest : class, new() { - //Loop(item, get => ExpressMapper.Mapper.Map(get), iterations); + Loop(item, source => AutoMapperInstance.Map(source), mapOperations); + } + + public static void TestFacet(Foo item, int mapOperations) + { + Loop(item, source => new FooFacetDto(source), mapOperations); + } + + public static void TestFacet(Customer item, int mapOperations) + { + Loop(item, source => new CustomerFacetDto(source), mapOperations); } - //public static void TestAutoMapper(TSrc item, int iterations) - // where TSrc : class - // where TDest : class, new() - //{ - // Loop(item, get => _mapper.Map(get), iterations); - //} + public static void TestMapperly(Foo item, int mapOperations) + { + Loop(item, MapperlyMappings.MapFoo, mapOperations); + } - public static void TestCodeGen(Foo item, int iterations) + public static void TestMapperly(Customer item, int mapOperations) { - //Loop(item, get => FooMapper.Map(get), iterations); + Loop(item, MapperlyMappings.MapCustomer, mapOperations); } - public static void TestCodeGen(Customer item, int iterations) + public static void TestCodeGen(Foo item, int mapOperations) { - //Loop(item, get => CustomerMapper.Map(get), iterations); + Loop(item, FooMapper.Map, mapOperations); } - private static void Loop(T item, Action action, int iterations) + public static void TestCodeGen(Customer item, int mapOperations) { - for (var i = 0; i < iterations; i++) action(item); + Loop(item, CustomerMapper.Map, mapOperations); + } + + private static void Loop(TSource item, Func map, int mapOperations) + { + TDestination result = default!; + for (var i = 0; i < mapOperations; i++) + { + result = map(item); + } + + GC.KeepAlive(result); } } From b72cc01a6fc1315836f91d627fd5e4c82408dfd0 Mon Sep 17 00:00:00 2001 From: Qyperion Date: Tue, 12 May 2026 17:25:09 +0300 Subject: [PATCH 77/80] docs: actualize benchmark result in docs --- README.md | 24 ++++++++++--------- docs/articles/packages/ExpressionDebugging.md | 10 +++----- .../packages/FastExpressionCompiler.md | 12 ++++++---- 3 files changed, 23 insertions(+), 23 deletions(-) diff --git a/README.md b/README.md index 9f1c924c..5f9a107a 100644 --- a/README.md +++ b/README.md @@ -141,21 +141,23 @@ public static class StudentMapper { ### Performance & Memory efficient -Mapster was designed to be efficient on both speed and memory. You could gain a 4x performance improvement whilst using only 1/3 of memory. -And you could gain up to 12x faster performance with: +Mapster was designed to be efficient on both speed and memory. The repository includes a benchmark project in [`src/Benchmark`](src/Benchmark) that compares the local Mapster build, its compiler variants, and other modern mapping libraries like AutoMapper, Mapperly, and Facet. - [Roslyn Compiler](https://mapstermapper.github.io/Mapster/articles/packages/ExpressionDebugging.html) - [FEC](https://mapstermapper.github.io/Mapster/articles/packages/FastExpressionCompiler.html) - Code generation - -| Method | Mean | StdDev | Error | Gen 0 | Gen 1 | Gen 2 | Allocated | -|-------------------------- |----------:|----------:|----------:|-----------:|------:|------:|----------:| -| 'Mapster 6.0.0' | 108.59 ms | 1.198 ms | 1.811 ms | 31000.0000 | - | - | 124.36 MB | -| 'Mapster 6.0.0 (Roslyn)' | 38.45 ms | 0.494 ms | 0.830 ms | 31142.8571 | - | - | 124.36 MB | -| 'Mapster 6.0.0 (FEC)' | 37.03 ms | 0.281 ms | 0.472 ms | 29642.8571 | - | - | 118.26 MB | -| 'Mapster 6.0.0 (Codegen)' | 34.16 ms | 0.209 ms | 0.316 ms | 31133.3333 | - | - | 124.36 MB | -| 'ExpressMapper 1.9.1' | 205.78 ms | 5.357 ms | 8.098 ms | 59000.0000 | - | - | 236.51 MB | -| 'AutoMapper 10.0.0' | 420.97 ms | 23.266 ms | 35.174 ms | 87000.0000 | - | - | 350.95 MB | +- Facet +- Mapperly + +| Method | MapOperations | Mean | StdDev | Error | Ratio | Gen0 | Gen1 | Allocated | Alloc Ratio | +| -------- | -------------- | -----: | -------: | ------: | ------: | -----: | -----: | ----------: | ----------: | +| `Mapster 10.0.7` | 1000000 | 412,534 us | 2,704 us | 4,543 us | 1.00 | 77000 | - | 1243.59 MB | 1.00 | +| `Mapster 10.0.7 (Roslyn)` | 1000000 | 397,028 us | 5,174 us | 8,695 us | 0.96 | 75000 | - | 1205.44 MB | 0.97 | +| `Mapster 10.0.7 (FEC)` | 1000000 | 124,374 us | 1,290 us | 2,466 us | 0.30 | 74000 | - | 1182.56 MB | 0.95 | +| `Mapster 10.0.7 (Codegen)` | 1000000 | 105,214 us | 1,312 us | 2,206 us | 0.26 | 75500 | 166 | 1205.44 MB | 0.97 | +| `AutoMapper 14.0.0` | 1000000 | 600,077 us | 63,170 us | 95,505 us | 1.45 | 197000 | 1000 | 3158.59 MB | 2.54 | +| `Facet 6.5.5` | 1000000 | 628,280 us | 7,326 us | 11,076 us | 1.52 | 325000 | 1000 | 5187.99 MB | 4.17 | +| `Mapperly 4.3.1` | 1000000 | 128,521 us | 1,453 us | 2,442 us | 0.31 | 94500 | 250 | 1510.62 MB | 1.21 | ### Step into debugging diff --git a/docs/articles/packages/ExpressionDebugging.md b/docs/articles/packages/ExpressionDebugging.md index e32f41dd..189d3ce9 100644 --- a/docs/articles/packages/ExpressionDebugging.md +++ b/docs/articles/packages/ExpressionDebugging.md @@ -50,12 +50,8 @@ TypeAdapterConfig.GlobalSettings.Compiler = exp => exp.CompileWithDebugInfo(opt) var dto = poco.Adapt(); //<-- you can step-into this function!! ``` -### Do not worry about performance +### Performance notes -In `RELEASE` mode, Roslyn compiler is actually faster than default dynamic compilation by 2x. -Here is the result: +In modern .NET runtimes, the Roslyn compiler path is mostly useful for step-into debugging and inspecting generated mapping code. In the current benchmark snapshot it performs close to the default Mapster compiler in steady-state execution. -| Method | Mean | StdDev | Error | Gen 0 | Gen 1 | Gen 2 | Allocated | -|-------------------------- |---------------:|-------------:|-------------:|------------:|------:|------:|-----------:| -| 'Mapster 4.1.1' | 115.31 ms | 0.849 ms | 1.426 ms | 31000.0000 | - | - | 124.36 MB | -| 'Mapster 4.1.1 (Roslyn)' | 53.55 ms | 0.342 ms | 0.654 ms | 31100.0000 | - | - | 124.36 MB | +See the [benchmark snapshot in README](../../../README.md#performance--memory-efficient) for current numbers. diff --git a/docs/articles/packages/FastExpressionCompiler.md b/docs/articles/packages/FastExpressionCompiler.md index a20df828..037d16aa 100644 --- a/docs/articles/packages/FastExpressionCompiler.md +++ b/docs/articles/packages/FastExpressionCompiler.md @@ -19,9 +19,11 @@ Then add following code on start up TypeAdapterConfig.GlobalSettings.Compiler = exp => exp.CompileFast(); ``` -That's it. Now your code will enjoy performance boost. Here is result. +That's it. Now your code will enjoy performance boost. Here is a current benchmark snapshot: -| Method | Mean | StdDev | Error | Gen 0 | Gen 1 | Gen 2 | Allocated | -|-------------------------- |---------------:|-------------:|-------------:|------------:|------:|------:|-----------:| -| 'Mapster 4.1.1' | 115.31 ms | 0.849 ms | 1.426 ms | 31000.0000 | - | - | 124.36 MB | -| 'Mapster 4.1.1 (FEC)' | 54.70 ms | 1.023 ms | 1.546 ms | 29600.0000 | - | - | 118.26 MB | +| Method | MapOperations | Mean | StdDev | Error | Ratio | Gen0 | Gen1 | Allocated | Alloc Ratio | +| -------- | -------------- | -----: | -------: | ------: | ------: | -----: | -----: | ----------: | ----------: | +| `Mapster 10.0.7` | 1000000 | 412,534 us | 2,704 us | 4,543 us | 1.00 | 77000 | - | 1243.59 MB | 1.00 | +| `Mapster 10.0.7 (FEC)` | 1000000 | 124,374 us | 1,290 us | 2,466 us | 0.30 | 74000 | - | 1182.56 MB | 0.95 | + +See the [benchmark snapshot in README](../../../README.md#performance--memory-efficient) for the full comparison. From d856d0ee6cb4405241e021b1e19606e268e028cc Mon Sep 17 00:00:00 2001 From: Qyperion Date: Sat, 16 May 2026 00:22:18 +0300 Subject: [PATCH 78/80] feat(benchmark): refactor benchmarks: add flat, recursive, complex scenarios --- src/Benchmark/Benchmark.csproj | 13 +- src/Benchmark/Benchmarks/Config.cs | 3 + .../Benchmarks/MappingBenchmarkBase.cs | 2 +- src/Benchmark/Benchmarks/PerMapColumn.cs | 86 ++++++ src/Benchmark/Benchmarks/ScenarioColumn.cs | 37 +++ src/Benchmark/Benchmarks/TestAll.cs | 123 -------- src/Benchmark/Benchmarks/TestComplexTypes.cs | 90 +++--- src/Benchmark/Benchmarks/TestFlatTypes.cs | 87 ++++++ .../Benchmarks/TestRecursiveTypes.cs | 86 ++++++ src/Benchmark/Benchmarks/TestSimpleTypes.cs | 101 ------- src/Benchmark/Benchmarks/TestTotalAllTypes.cs | 143 ++++++++++ src/Benchmark/Classes/Customer.cs | 32 ++- src/Benchmark/Classes/Foo.cs | 29 ++ src/Benchmark/Classes/Person.cs | 28 ++ src/Benchmark/Comparisons/FacetModels.cs | 5 + src/Benchmark/Comparisons/MapperlyModels.cs | 14 + src/Benchmark/FooMapper.g.cs | 16 +- src/Benchmark/FooMapper.tt | 4 +- src/Benchmark/PersonMapper.g.cs | 23 ++ src/Benchmark/PersonMapper.tt | 25 ++ src/Benchmark/Program.cs | 5 +- src/Benchmark/TestAdaptHelper.cs | 263 +++++++----------- 22 files changed, 743 insertions(+), 472 deletions(-) create mode 100644 src/Benchmark/Benchmarks/PerMapColumn.cs create mode 100644 src/Benchmark/Benchmarks/ScenarioColumn.cs delete mode 100644 src/Benchmark/Benchmarks/TestAll.cs create mode 100644 src/Benchmark/Benchmarks/TestFlatTypes.cs create mode 100644 src/Benchmark/Benchmarks/TestRecursiveTypes.cs delete mode 100644 src/Benchmark/Benchmarks/TestSimpleTypes.cs create mode 100644 src/Benchmark/Benchmarks/TestTotalAllTypes.cs create mode 100644 src/Benchmark/Classes/Person.cs create mode 100644 src/Benchmark/PersonMapper.g.cs create mode 100644 src/Benchmark/PersonMapper.tt diff --git a/src/Benchmark/Benchmark.csproj b/src/Benchmark/Benchmark.csproj index 451136bd..400a510f 100644 --- a/src/Benchmark/Benchmark.csproj +++ b/src/Benchmark/Benchmark.csproj @@ -12,10 +12,12 @@ - + + + @@ -35,6 +37,11 @@ True FooMapper.tt + + True + True + PersonMapper.tt + @@ -46,6 +53,10 @@ TextTemplatingFileGenerator FooMapper.g.cs + + TextTemplatingFileGenerator + PersonMapper.g.cs + diff --git a/src/Benchmark/Benchmarks/Config.cs b/src/Benchmark/Benchmarks/Config.cs index fb45726c..82bff156 100644 --- a/src/Benchmark/Benchmarks/Config.cs +++ b/src/Benchmark/Benchmarks/Config.cs @@ -19,16 +19,19 @@ public Config() AddExporter(HtmlExporter.Default); AddDiagnoser(MemoryDiagnoser.Default); + AddColumn(ScenarioColumn.Default); AddColumn(TargetMethodColumn.Method); AddColumnProvider(DefaultColumnProviders.Params); AddColumn(StatisticColumn.Mean); + AddColumn(PerMapColumn.Nanoseconds); AddColumn(StatisticColumn.StdDev); AddColumn(StatisticColumn.Error); AddColumn(BaselineRatioColumn.RatioMean); AddColumn(BaselineAllocationRatioColumn.RatioMean); AddColumnProvider(DefaultColumnProviders.Metrics); + AddColumn(PerMapColumn.Bytes); AddJob(Job.ShortRun .WithLaunchCount(1) diff --git a/src/Benchmark/Benchmarks/MappingBenchmarkBase.cs b/src/Benchmark/Benchmarks/MappingBenchmarkBase.cs index e5f5ab77..932dbafa 100644 --- a/src/Benchmark/Benchmarks/MappingBenchmarkBase.cs +++ b/src/Benchmark/Benchmarks/MappingBenchmarkBase.cs @@ -4,7 +4,7 @@ namespace Mapster.Benchmark.Benchmarks { public abstract class MappingBenchmarkBase { - public IEnumerable MapOperationValues => new[] { 1_000, 10_000, 100_000, 1_000_000 }; + public IEnumerable MapOperationValues => new[] { 10_000, 100_000, 1_000_000 }; [ParamsSource(nameof(MapOperationValues))] public int MapOperations { get; set; } diff --git a/src/Benchmark/Benchmarks/PerMapColumn.cs b/src/Benchmark/Benchmarks/PerMapColumn.cs new file mode 100644 index 00000000..2eebfef2 --- /dev/null +++ b/src/Benchmark/Benchmarks/PerMapColumn.cs @@ -0,0 +1,86 @@ +using BenchmarkDotNet.Columns; +using BenchmarkDotNet.Reports; +using BenchmarkDotNet.Running; + +namespace Mapster.Benchmark.Benchmarks +{ + public abstract class PerMapColumn : IColumn + { + public static readonly IColumn Nanoseconds = new NanosecondsPerMapColumn(); + public static readonly IColumn Bytes = new BytesPerMapColumn(); + + public abstract string Id { get; } + public abstract string ColumnName { get; } + public abstract string Legend { get; } + public abstract ColumnCategory Category { get; } + + public UnitType UnitType => UnitType.Dimensionless; + public int PriorityInCategory => 100; + public bool IsNumeric => true; + public bool AlwaysShow => true; + public bool IsAvailable(Summary summary) => true; + public bool IsDefault(Summary summary, BenchmarkCase benchmarkCase) => false; + + public abstract string GetValue(Summary summary, BenchmarkCase benchmarkCase, SummaryStyle style); + + public string GetValue(Summary summary, BenchmarkCase benchmarkCase) + => GetValue(summary, benchmarkCase, summary.Style); + + protected static long GetLogicalMapCount(BenchmarkCase benchmarkCase) + { + var mapOperations = 1; + var parameter = benchmarkCase.Parameters.Items + .FirstOrDefault(p => p.Name == nameof(MappingBenchmarkBase.MapOperations)); + + if (parameter?.Value is int value && value > 0) + mapOperations = value; + + //TestTotalAllTypes includes 3 separate mapping calls per benchmark call + var mappingsPerBenchmarkCall = benchmarkCase.Descriptor.Type == typeof(TestTotalAllTypes) ? 3 : 1; + return (long)mapOperations * mappingsPerBenchmarkCall; + } + + protected static string Format(double value, SummaryStyle style) + => value.ToString("0.###", style.CultureInfo); + + public override string ToString() => ColumnName; + + private sealed class NanosecondsPerMapColumn : PerMapColumn + { + public override string Id => nameof(NanosecondsPerMapColumn); + public override string ColumnName => "Ns/Map"; + public override string Legend => "Mean nanoseconds per single mapping call"; + public override ColumnCategory Category => ColumnCategory.Statistics; + + public override string GetValue(Summary summary, BenchmarkCase benchmarkCase, SummaryStyle style) + { + if (!summary.HasReport(benchmarkCase)) + return "?"; + + var meanNanoseconds = summary[benchmarkCase].ResultStatistics?.Mean; + return meanNanoseconds.HasValue + ? Format(meanNanoseconds.Value / GetLogicalMapCount(benchmarkCase), style) + : "?"; + } + } + + private sealed class BytesPerMapColumn : PerMapColumn + { + public override string Id => nameof(BytesPerMapColumn); + public override string ColumnName => "Bytes/Map"; + public override string Legend => "Allocated bytes per single mapping call"; + public override ColumnCategory Category => ColumnCategory.Metric; + + public override string GetValue(Summary summary, BenchmarkCase benchmarkCase, SummaryStyle style) + { + if (!summary.HasReport(benchmarkCase)) + return "?"; + + var allocatedBytesPerBenchmarkCall = summary[benchmarkCase].GcStats.GetBytesAllocatedPerOperation(benchmarkCase); + return allocatedBytesPerBenchmarkCall.HasValue + ? Format((double)allocatedBytesPerBenchmarkCall.Value / GetLogicalMapCount(benchmarkCase), style) + : "?"; + } + } + } +} \ No newline at end of file diff --git a/src/Benchmark/Benchmarks/ScenarioColumn.cs b/src/Benchmark/Benchmarks/ScenarioColumn.cs new file mode 100644 index 00000000..67362598 --- /dev/null +++ b/src/Benchmark/Benchmarks/ScenarioColumn.cs @@ -0,0 +1,37 @@ +using BenchmarkDotNet.Columns; +using BenchmarkDotNet.Reports; +using BenchmarkDotNet.Running; + +namespace Mapster.Benchmark.Benchmarks +{ + /// + /// Adds a "Scenario" column to the joined summary so each row clearly indicates which benchmark class + /// (TestFlatTypes / TestRecursiveTypes / TestComplexTypes / TestTotalAllTypes) produced it. + /// + public class ScenarioColumn : IColumn + { + public static readonly IColumn Default = new ScenarioColumn(); + + public string Id => nameof(ScenarioColumn); + public string ColumnName => "Scenario"; + public string Legend => "Benchmark class the row belongs to"; + public UnitType UnitType => UnitType.Dimensionless; + public ColumnCategory Category => ColumnCategory.Job; + public int PriorityInCategory => -10; + public bool IsNumeric => false; + public bool AlwaysShow => true; + public bool IsAvailable(Summary summary) => true; + public bool IsDefault(Summary summary, BenchmarkCase benchmarkCase) => false; + + public string GetValue(Summary summary, BenchmarkCase benchmarkCase) + { + var name = benchmarkCase.Descriptor.Type.Name; + return name.StartsWith("Test") ? name[4..] : name; + } + + public string GetValue(Summary summary, BenchmarkCase benchmarkCase, SummaryStyle style) + => GetValue(summary, benchmarkCase); + + public override string ToString() => ColumnName; + } +} diff --git a/src/Benchmark/Benchmarks/TestAll.cs b/src/Benchmark/Benchmarks/TestAll.cs deleted file mode 100644 index f2ff5e0a..00000000 --- a/src/Benchmark/Benchmarks/TestAll.cs +++ /dev/null @@ -1,123 +0,0 @@ -using BenchmarkDotNet.Attributes; -using Mapster.Benchmark.Classes; - -namespace Mapster.Benchmark.Benchmarks -{ - public class TestAll : MappingBenchmarkBase - { - private Foo _fooInstance; - private Customer _customerInstance; - - [Benchmark(Baseline = true, Description = $"Mapster {TestAdaptHelper.MapsterVersion}")] - public void MapsterTest() - { - TestAdaptHelper.TestMapsterAdapter(_fooInstance, MapOperations); - TestAdaptHelper.TestMapsterAdapter(_customerInstance, MapOperations); - } - - [Benchmark(Description = $"Mapster {TestAdaptHelper.MapsterVersion} (Roslyn)")] - public void RoslynTest() - { - TestAdaptHelper.TestMapsterAdapter(_fooInstance, MapOperations); - TestAdaptHelper.TestMapsterAdapter(_customerInstance, MapOperations); - } - - [Benchmark(Description = $"Mapster {TestAdaptHelper.MapsterVersion} (FEC)")] - public void FecTest() - { - TestAdaptHelper.TestMapsterAdapter(_fooInstance, MapOperations); - TestAdaptHelper.TestMapsterAdapter(_customerInstance, MapOperations); - } - - [Benchmark(Description = $"Mapster {TestAdaptHelper.MapsterVersion} (Codegen)")] - public void CodegenTest() - { - TestAdaptHelper.TestCodeGen(_fooInstance, MapOperations); - TestAdaptHelper.TestCodeGen(_customerInstance, MapOperations); - } - - [Benchmark(Description = $"AutoMapper {TestAdaptHelper.AutoMapperVersion}")] - public void AutoMapperTest() - { - TestAdaptHelper.TestAutoMapper(_fooInstance, MapOperations); - TestAdaptHelper.TestAutoMapper(_customerInstance, MapOperations); - } - - [Benchmark(Description = $"Facet {TestAdaptHelper.FacetVersion}")] - public void FacetTest() - { - TestAdaptHelper.TestFacet(_fooInstance, MapOperations); - TestAdaptHelper.TestFacet(_customerInstance, MapOperations); - } - - [Benchmark(Description = $"Mapperly {TestAdaptHelper.MapperlyVersion}")] - public void MapperlyTest() - { - TestAdaptHelper.TestMapperly(_fooInstance, MapOperations); - TestAdaptHelper.TestMapperly(_customerInstance, MapOperations); - } - - [GlobalSetup(Target = nameof(MapsterTest))] - public void SetupMapster() - { - _fooInstance = TestAdaptHelper.SetupFooInstance(); - _customerInstance = TestAdaptHelper.SetupCustomerInstance(); - TestAdaptHelper.ConfigureMapster(_fooInstance, MapsterCompilerType.Default); - TestAdaptHelper.ConfigureMapster(_customerInstance, MapsterCompilerType.Default); - } - - [GlobalSetup(Target = nameof(RoslynTest))] - public void SetupRoslyn() - { - _fooInstance = TestAdaptHelper.SetupFooInstance(); - _customerInstance = TestAdaptHelper.SetupCustomerInstance(); - TestAdaptHelper.ConfigureMapster(_fooInstance, MapsterCompilerType.Roslyn); - TestAdaptHelper.ConfigureMapster(_customerInstance, MapsterCompilerType.Roslyn); - } - - [GlobalSetup(Target = nameof(FecTest))] - public void SetupFec() - { - _fooInstance = TestAdaptHelper.SetupFooInstance(); - _customerInstance = TestAdaptHelper.SetupCustomerInstance(); - TestAdaptHelper.ConfigureMapster(_fooInstance, MapsterCompilerType.FEC); - TestAdaptHelper.ConfigureMapster(_customerInstance, MapsterCompilerType.FEC); - } - - [GlobalSetup(Target = nameof(CodegenTest))] - public void SetupCodegen() - { - _fooInstance = TestAdaptHelper.SetupFooInstance(); - _customerInstance = TestAdaptHelper.SetupCustomerInstance(); - _ = FooMapper.Map(_fooInstance); - _ = CustomerMapper.Map(_customerInstance); - } - - [GlobalSetup(Target = nameof(FacetTest))] - public void SetupFacet() - { - _fooInstance = TestAdaptHelper.SetupFooInstance(); - _customerInstance = TestAdaptHelper.SetupCustomerInstance(); - TestAdaptHelper.ConfigureFacet(_fooInstance); - TestAdaptHelper.ConfigureFacet(_customerInstance); - } - - [GlobalSetup(Target = nameof(MapperlyTest))] - public void SetupMapperly() - { - _fooInstance = TestAdaptHelper.SetupFooInstance(); - _customerInstance = TestAdaptHelper.SetupCustomerInstance(); - TestAdaptHelper.ConfigureMapperly(_fooInstance); - TestAdaptHelper.ConfigureMapperly(_customerInstance); - } - - [GlobalSetup(Target = nameof(AutoMapperTest))] - public void SetupAutoMapper() - { - _fooInstance = TestAdaptHelper.SetupFooInstance(); - _customerInstance = TestAdaptHelper.SetupCustomerInstance(); - TestAdaptHelper.ConfigureAutoMapper(_fooInstance); - TestAdaptHelper.ConfigureAutoMapper(_customerInstance); - } - } -} \ No newline at end of file diff --git a/src/Benchmark/Benchmarks/TestComplexTypes.cs b/src/Benchmark/Benchmarks/TestComplexTypes.cs index 71cb2e8a..4f27a54b 100644 --- a/src/Benchmark/Benchmarks/TestComplexTypes.cs +++ b/src/Benchmark/Benchmarks/TestComplexTypes.cs @@ -1,101 +1,87 @@ -using BenchmarkDotNet.Attributes; +using BenchmarkDotNet.Attributes; using Mapster.Benchmark.Classes; +using Mapster.Benchmark.Comparisons; namespace Mapster.Benchmark.Benchmarks { + // Customer/CustomerDTO: nested object of different type, two collection shape changes + // (Address[] -> AddressDTO[], ICollection
-> List) and a flattening rule (AddressCity <- Address.City). public class TestComplexTypes : MappingBenchmarkBase { - private Customer _customerInstance; + private static readonly Func CustomerFacetCompiled = + CustomerFacetDto.Projection.Compile(); + + private Customer _customer; [Benchmark(Baseline = true, Description = $"Mapster {TestAdaptHelper.MapsterVersion}")] public void MapsterTest() - { - TestAdaptHelper.TestMapsterAdapter(_customerInstance, MapOperations); - } + => TestAdaptHelper.Loop(_customer, src => src.Adapt(), MapOperations); [Benchmark(Description = $"Mapster {TestAdaptHelper.MapsterVersion} (Roslyn)")] public void RoslynTest() - { - TestAdaptHelper.TestMapsterAdapter(_customerInstance, MapOperations); - } + => TestAdaptHelper.Loop(_customer, src => src.Adapt(), MapOperations); [Benchmark(Description = $"Mapster {TestAdaptHelper.MapsterVersion} (FEC)")] public void FecTest() - { - TestAdaptHelper.TestMapsterAdapter(_customerInstance, MapOperations); - } + => TestAdaptHelper.Loop(_customer, src => src.Adapt(), MapOperations); [Benchmark(Description = $"Mapster {TestAdaptHelper.MapsterVersion} (Codegen)")] public void CodegenTest() - { - TestAdaptHelper.TestCodeGen(_customerInstance, MapOperations); - } + => TestAdaptHelper.Loop(_customer, CustomerMapper.Map, MapOperations); [Benchmark(Description = $"AutoMapper {TestAdaptHelper.AutoMapperVersion}")] public void AutoMapperTest() - { - TestAdaptHelper.TestAutoMapper(_customerInstance, MapOperations); - } + => TestAdaptHelper.Loop(_customer, src => TestAdaptHelper.AutoMapper.Map(src), MapOperations); [Benchmark(Description = $"Facet {TestAdaptHelper.FacetVersion}")] public void FacetTest() - { - TestAdaptHelper.TestFacet(_customerInstance, MapOperations); - } + => TestAdaptHelper.Loop(_customer, src => new CustomerFacetDto(src), MapOperations); + + [Benchmark(Description = $"Facet {TestAdaptHelper.FacetVersion} (Compiled Projection)")] + public void FacetCompiledTest() + => TestAdaptHelper.Loop(_customer, CustomerFacetCompiled, MapOperations); [Benchmark(Description = $"Mapperly {TestAdaptHelper.MapperlyVersion}")] public void MapperlyTest() - { - TestAdaptHelper.TestMapperly(_customerInstance, MapOperations); - } + => TestAdaptHelper.Loop(_customer, MapperlyMappings.MapCustomer, MapOperations); [GlobalSetup(Target = nameof(MapsterTest))] public void SetupMapster() { - _customerInstance = TestAdaptHelper.SetupCustomerInstance(); - TestAdaptHelper.ConfigureMapster(_customerInstance, MapsterCompilerType.Default); + _customer = TestAdaptHelper.SetupCustomerInstance(); + TestAdaptHelper.UseMapsterCompiler(MapsterCompilerType.Default); + _ = _customer.Adapt(); } [GlobalSetup(Target = nameof(RoslynTest))] public void SetupRoslyn() { - _customerInstance = TestAdaptHelper.SetupCustomerInstance(); - TestAdaptHelper.ConfigureMapster(_customerInstance, MapsterCompilerType.Roslyn); + _customer = TestAdaptHelper.SetupCustomerInstance(); + TestAdaptHelper.UseMapsterCompiler(MapsterCompilerType.Roslyn); + _ = _customer.Adapt(); } [GlobalSetup(Target = nameof(FecTest))] public void SetupFec() { - _customerInstance = TestAdaptHelper.SetupCustomerInstance(); - TestAdaptHelper.ConfigureMapster(_customerInstance, MapsterCompilerType.FEC); + _customer = TestAdaptHelper.SetupCustomerInstance(); + TestAdaptHelper.UseMapsterCompiler(MapsterCompilerType.FEC); + _ = _customer.Adapt(); } [GlobalSetup(Target = nameof(CodegenTest))] - public void SetupCodegen() - { - _customerInstance = TestAdaptHelper.SetupCustomerInstance(); - _ = CustomerMapper.Map(_customerInstance); - } + public void SetupCodegen() => _customer = TestAdaptHelper.SetupCustomerInstance(); + + [GlobalSetup(Target = nameof(AutoMapperTest))] + public void SetupAutoMapper() => _customer = TestAdaptHelper.SetupCustomerInstance(); [GlobalSetup(Target = nameof(FacetTest))] - public void SetupFacet() - { - _customerInstance = TestAdaptHelper.SetupCustomerInstance(); - TestAdaptHelper.ConfigureFacet(_customerInstance); - } + public void SetupFacet() => _customer = TestAdaptHelper.SetupCustomerInstance(); - [GlobalSetup(Target = nameof(MapperlyTest))] - public void SetupMapperly() - { - _customerInstance = TestAdaptHelper.SetupCustomerInstance(); - TestAdaptHelper.ConfigureMapperly(_customerInstance); - } + [GlobalSetup(Target = nameof(FacetCompiledTest))] + public void SetupFacetCompiled() => _customer = TestAdaptHelper.SetupCustomerInstance(); - [GlobalSetup(Target = nameof(AutoMapperTest))] - public void SetupAutoMapper() - { - _customerInstance = TestAdaptHelper.SetupCustomerInstance(); - TestAdaptHelper.ConfigureAutoMapper(_customerInstance); - } + [GlobalSetup(Target = nameof(MapperlyTest))] + public void SetupMapperly() => _customer = TestAdaptHelper.SetupCustomerInstance(); } -} \ No newline at end of file +} diff --git a/src/Benchmark/Benchmarks/TestFlatTypes.cs b/src/Benchmark/Benchmarks/TestFlatTypes.cs new file mode 100644 index 00000000..f860894f --- /dev/null +++ b/src/Benchmark/Benchmarks/TestFlatTypes.cs @@ -0,0 +1,87 @@ +using BenchmarkDotNet.Attributes; +using Mapster.Benchmark.Classes; +using Mapster.Benchmark.Comparisons; + +namespace Mapster.Benchmark.Benchmarks +{ + // FlatType DTO: simple property-to-property copy, no nesting, no collections. + // Highlights pure per-call overhead (delegate dispatch, allocation rate, IL quality of property copy). + public class TestFlatTypes : MappingBenchmarkBase + { + private static readonly Func PersonFacetCompiled = + PersonFacetDto.Projection.Compile(); + + private Person _person; + + [Benchmark(Baseline = true, Description = $"Mapster {TestAdaptHelper.MapsterVersion}")] + public void MapsterTest() + => TestAdaptHelper.Loop(_person, src => src.Adapt(), MapOperations); + + [Benchmark(Description = $"Mapster {TestAdaptHelper.MapsterVersion} (Roslyn)")] + public void RoslynTest() + => TestAdaptHelper.Loop(_person, src => src.Adapt(), MapOperations); + + [Benchmark(Description = $"Mapster {TestAdaptHelper.MapsterVersion} (FEC)")] + public void FecTest() + => TestAdaptHelper.Loop(_person, src => src.Adapt(), MapOperations); + + [Benchmark(Description = $"Mapster {TestAdaptHelper.MapsterVersion} (Codegen)")] + public void CodegenTest() + => TestAdaptHelper.Loop(_person, PersonMapper.Map, MapOperations); + + [Benchmark(Description = $"AutoMapper {TestAdaptHelper.AutoMapperVersion}")] + public void AutoMapperTest() + => TestAdaptHelper.Loop(_person, src => TestAdaptHelper.AutoMapper.Map(src), MapOperations); + + [Benchmark(Description = $"Facet {TestAdaptHelper.FacetVersion}")] + public void FacetTest() + => TestAdaptHelper.Loop(_person, src => new PersonFacetDto(src), MapOperations); + + [Benchmark(Description = $"Facet {TestAdaptHelper.FacetVersion} (Compiled Projection)")] + public void FacetCompiledTest() + => TestAdaptHelper.Loop(_person, PersonFacetCompiled, MapOperations); + + [Benchmark(Description = $"Mapperly {TestAdaptHelper.MapperlyVersion}")] + public void MapperlyTest() + => TestAdaptHelper.Loop(_person, MapperlyMappings.MapPerson, MapOperations); + + [GlobalSetup(Target = nameof(MapsterTest))] + public void SetupMapster() + { + _person = TestAdaptHelper.SetupPersonInstance(); + TestAdaptHelper.UseMapsterCompiler(MapsterCompilerType.Default); + _ = _person.Adapt(); + } + + [GlobalSetup(Target = nameof(RoslynTest))] + public void SetupRoslyn() + { + _person = TestAdaptHelper.SetupPersonInstance(); + TestAdaptHelper.UseMapsterCompiler(MapsterCompilerType.Roslyn); + _ = _person.Adapt(); + } + + [GlobalSetup(Target = nameof(FecTest))] + public void SetupFec() + { + _person = TestAdaptHelper.SetupPersonInstance(); + TestAdaptHelper.UseMapsterCompiler(MapsterCompilerType.FEC); + _ = _person.Adapt(); + } + + [GlobalSetup(Target = nameof(CodegenTest))] + public void SetupCodegen() => _person = TestAdaptHelper.SetupPersonInstance(); + + [GlobalSetup(Target = nameof(AutoMapperTest))] + public void SetupAutoMapper() => _person = TestAdaptHelper.SetupPersonInstance(); + + [GlobalSetup(Target = nameof(FacetTest))] + public void SetupFacet() => _person = TestAdaptHelper.SetupPersonInstance(); + + [GlobalSetup(Target = nameof(FacetCompiledTest))] + public void SetupFacetCompiled() => _person = TestAdaptHelper.SetupPersonInstance(); + + [GlobalSetup(Target = nameof(MapperlyTest))] + public void SetupMapperly() => _person = TestAdaptHelper.SetupPersonInstance(); + } +} \ No newline at end of file diff --git a/src/Benchmark/Benchmarks/TestRecursiveTypes.cs b/src/Benchmark/Benchmarks/TestRecursiveTypes.cs new file mode 100644 index 00000000..b6693bdc --- /dev/null +++ b/src/Benchmark/Benchmarks/TestRecursiveTypes.cs @@ -0,0 +1,86 @@ +using BenchmarkDotNet.Attributes; +using Mapster.Benchmark.Classes; +using Mapster.Benchmark.Comparisons; + +namespace Mapster.Benchmark.Benchmarks +{ + // Self-recursive graph with nested references and collections. + // Source: Foo, Destination: FooDTO. + public class TestRecursiveTypes : MappingBenchmarkBase + { + private static readonly Func FooFacetCompiled = FooFacetDto.Projection.Compile(); + + private Foo _foo; + + [Benchmark(Baseline = true, Description = $"Mapster {TestAdaptHelper.MapsterVersion}")] + public void MapsterTest() + => TestAdaptHelper.Loop(_foo, src => src.Adapt(), MapOperations); + + [Benchmark(Description = $"Mapster {TestAdaptHelper.MapsterVersion} (Roslyn)")] + public void RoslynTest() + => TestAdaptHelper.Loop(_foo, src => src.Adapt(), MapOperations); + + [Benchmark(Description = $"Mapster {TestAdaptHelper.MapsterVersion} (FEC)")] + public void FecTest() + => TestAdaptHelper.Loop(_foo, src => src.Adapt(), MapOperations); + + [Benchmark(Description = $"Mapster {TestAdaptHelper.MapsterVersion} (Codegen)")] + public void CodegenTest() + => TestAdaptHelper.Loop(_foo, FooMapper.Map, MapOperations); + + [Benchmark(Description = $"AutoMapper {TestAdaptHelper.AutoMapperVersion}")] + public void AutoMapperTest() + => TestAdaptHelper.Loop(_foo, src => TestAdaptHelper.AutoMapper.Map(src), MapOperations); + + [Benchmark(Description = $"Facet {TestAdaptHelper.FacetVersion}")] + public void FacetTest() + => TestAdaptHelper.Loop(_foo, src => new FooFacetDto(src), MapOperations); + + [Benchmark(Description = $"Facet {TestAdaptHelper.FacetVersion} (Compiled Projection)")] + public void FacetCompiledTest() + => TestAdaptHelper.Loop(_foo, FooFacetCompiled, MapOperations); + + [Benchmark(Description = $"Mapperly {TestAdaptHelper.MapperlyVersion}")] + public void MapperlyTest() + => TestAdaptHelper.Loop(_foo, MapperlyMappings.MapFoo, MapOperations); + + [GlobalSetup(Target = nameof(MapsterTest))] + public void SetupMapster() + { + _foo = TestAdaptHelper.SetupFooInstance(); + TestAdaptHelper.UseMapsterCompiler(MapsterCompilerType.Default); + _ = _foo.Adapt(); + } + + [GlobalSetup(Target = nameof(RoslynTest))] + public void SetupRoslyn() + { + _foo = TestAdaptHelper.SetupFooInstance(); + TestAdaptHelper.UseMapsterCompiler(MapsterCompilerType.Roslyn); + _ = _foo.Adapt(); + } + + [GlobalSetup(Target = nameof(FecTest))] + public void SetupFec() + { + _foo = TestAdaptHelper.SetupFooInstance(); + TestAdaptHelper.UseMapsterCompiler(MapsterCompilerType.FEC); + _ = _foo.Adapt(); + } + + [GlobalSetup(Target = nameof(CodegenTest))] + public void SetupCodegen() => _foo = TestAdaptHelper.SetupFooInstance(); + + [GlobalSetup(Target = nameof(AutoMapperTest))] + public void SetupAutoMapper() => _foo = TestAdaptHelper.SetupFooInstance(); + + [GlobalSetup(Target = nameof(FacetTest))] + public void SetupFacet() => _foo = TestAdaptHelper.SetupFooInstance(); + + [GlobalSetup(Target = nameof(FacetCompiledTest))] + public void SetupFacetCompiled() => _foo = TestAdaptHelper.SetupFooInstance(); + + [GlobalSetup(Target = nameof(MapperlyTest))] + public void SetupMapperly() => _foo = TestAdaptHelper.SetupFooInstance(); + } +} \ No newline at end of file diff --git a/src/Benchmark/Benchmarks/TestSimpleTypes.cs b/src/Benchmark/Benchmarks/TestSimpleTypes.cs deleted file mode 100644 index 8f6466da..00000000 --- a/src/Benchmark/Benchmarks/TestSimpleTypes.cs +++ /dev/null @@ -1,101 +0,0 @@ -using BenchmarkDotNet.Attributes; -using Mapster.Benchmark.Classes; - -namespace Mapster.Benchmark.Benchmarks -{ - public class TestSimpleTypes : MappingBenchmarkBase - { - private Foo _fooInstance; - - [Benchmark(Baseline = true, Description = $"Mapster {TestAdaptHelper.MapsterVersion}")] - public void MapsterTest() - { - TestAdaptHelper.TestMapsterAdapter(_fooInstance, MapOperations); - } - - [Benchmark(Description = $"Mapster {TestAdaptHelper.MapsterVersion} (Roslyn)")] - public void RoslynTest() - { - TestAdaptHelper.TestMapsterAdapter(_fooInstance, MapOperations); - } - - [Benchmark(Description = $"Mapster {TestAdaptHelper.MapsterVersion} (FEC)")] - public void FecTest() - { - TestAdaptHelper.TestMapsterAdapter(_fooInstance, MapOperations); - } - - [Benchmark(Description = $"Mapster {TestAdaptHelper.MapsterVersion} (Codegen)")] - public void CodegenTest() - { - TestAdaptHelper.TestCodeGen(_fooInstance, MapOperations); - } - - [Benchmark(Description = $"AutoMapper {TestAdaptHelper.AutoMapperVersion}")] - public void AutoMapperTest() - { - TestAdaptHelper.TestAutoMapper(_fooInstance, MapOperations); - } - - [Benchmark(Description = $"Facet {TestAdaptHelper.FacetVersion}")] - public void FacetTest() - { - TestAdaptHelper.TestFacet(_fooInstance, MapOperations); - } - - [Benchmark(Description = $"Mapperly {TestAdaptHelper.MapperlyVersion}")] - public void MapperlyTest() - { - TestAdaptHelper.TestMapperly(_fooInstance, MapOperations); - } - - [GlobalSetup(Target = nameof(MapsterTest))] - public void SetupMapster() - { - _fooInstance = TestAdaptHelper.SetupFooInstance(); - TestAdaptHelper.ConfigureMapster(_fooInstance, MapsterCompilerType.Default); - } - - [GlobalSetup(Target = nameof(RoslynTest))] - public void SetupRoslyn() - { - _fooInstance = TestAdaptHelper.SetupFooInstance(); - TestAdaptHelper.ConfigureMapster(_fooInstance, MapsterCompilerType.Roslyn); - } - - [GlobalSetup(Target = nameof(FecTest))] - public void SetupFec() - { - _fooInstance = TestAdaptHelper.SetupFooInstance(); - TestAdaptHelper.ConfigureMapster(_fooInstance, MapsterCompilerType.FEC); - } - - [GlobalSetup(Target = nameof(CodegenTest))] - public void SetupCodegen() - { - _fooInstance = TestAdaptHelper.SetupFooInstance(); - _ = FooMapper.Map(_fooInstance); - } - - [GlobalSetup(Target = nameof(FacetTest))] - public void SetupFacet() - { - _fooInstance = TestAdaptHelper.SetupFooInstance(); - TestAdaptHelper.ConfigureFacet(_fooInstance); - } - - [GlobalSetup(Target = nameof(MapperlyTest))] - public void SetupMapperly() - { - _fooInstance = TestAdaptHelper.SetupFooInstance(); - TestAdaptHelper.ConfigureMapperly(_fooInstance); - } - - [GlobalSetup(Target = nameof(AutoMapperTest))] - public void SetupAutoMapper() - { - _fooInstance = TestAdaptHelper.SetupFooInstance(); - TestAdaptHelper.ConfigureAutoMapper(_fooInstance); - } - } -} \ No newline at end of file diff --git a/src/Benchmark/Benchmarks/TestTotalAllTypes.cs b/src/Benchmark/Benchmarks/TestTotalAllTypes.cs new file mode 100644 index 00000000..22c1c295 --- /dev/null +++ b/src/Benchmark/Benchmarks/TestTotalAllTypes.cs @@ -0,0 +1,143 @@ +using BenchmarkDotNet.Attributes; +using Mapster.Benchmark.Classes; +using Mapster.Benchmark.Comparisons; + +namespace Mapster.Benchmark.Benchmarks +{ + /// + /// Total benchmark across all three sample shapes: + /// + /// -> (FlatType DTO) + /// -> (self-recursive graph) + /// -> (nested + collections + flattening) + /// + /// Each [Benchmark] iteration runs all three scenarios via + /// loops, so the reported Mean is their total time. + /// + public class TestTotalAllTypes : MappingBenchmarkBase + { + private static readonly Func FooFacetCompiled = FooFacetDto.Projection.Compile(); + private static readonly Func CustomerFacetCompiled = CustomerFacetDto.Projection.Compile(); + private static readonly Func PersonFacetCompiled = PersonFacetDto.Projection.Compile(); + + private Person _person; + private Foo _foo; + private Customer _customer; + + [Benchmark(Baseline = true, Description = $"Mapster {TestAdaptHelper.MapsterVersion}")] + public void MapsterTest() + { + TestAdaptHelper.Loop(_person, src => src.Adapt(), MapOperations); + TestAdaptHelper.Loop(_foo, src => src.Adapt(), MapOperations); + TestAdaptHelper.Loop(_customer, src => src.Adapt(), MapOperations); + } + + [Benchmark(Description = $"Mapster {TestAdaptHelper.MapsterVersion} (Roslyn)")] + public void RoslynTest() + { + TestAdaptHelper.Loop(_person, src => src.Adapt(), MapOperations); + TestAdaptHelper.Loop(_foo, src => src.Adapt(), MapOperations); + TestAdaptHelper.Loop(_customer, src => src.Adapt(), MapOperations); + } + + [Benchmark(Description = $"Mapster {TestAdaptHelper.MapsterVersion} (FEC)")] + public void FecTest() + { + TestAdaptHelper.Loop(_person, src => src.Adapt(), MapOperations); + TestAdaptHelper.Loop(_foo, src => src.Adapt(), MapOperations); + TestAdaptHelper.Loop(_customer, src => src.Adapt(), MapOperations); + } + + [Benchmark(Description = $"Mapster {TestAdaptHelper.MapsterVersion} (Codegen)")] + public void CodegenTest() + { + TestAdaptHelper.Loop(_person, PersonMapper.Map, MapOperations); + TestAdaptHelper.Loop(_foo, FooMapper.Map, MapOperations); + TestAdaptHelper.Loop(_customer, CustomerMapper.Map, MapOperations); + } + + [Benchmark(Description = $"AutoMapper {TestAdaptHelper.AutoMapperVersion}")] + public void AutoMapperTest() + { + TestAdaptHelper.Loop(_person, src => TestAdaptHelper.AutoMapper.Map(src), MapOperations); + TestAdaptHelper.Loop(_foo, src => TestAdaptHelper.AutoMapper.Map(src), MapOperations); + TestAdaptHelper.Loop(_customer, src => TestAdaptHelper.AutoMapper.Map(src), MapOperations); + } + + [Benchmark(Description = $"Facet {TestAdaptHelper.FacetVersion}")] + public void FacetTest() + { + TestAdaptHelper.Loop(_person, src => new PersonFacetDto(src), MapOperations); + TestAdaptHelper.Loop(_foo, src => new FooFacetDto(src), MapOperations); + TestAdaptHelper.Loop(_customer, src => new CustomerFacetDto(src), MapOperations); + } + + [Benchmark(Description = $"Facet {TestAdaptHelper.FacetVersion} (Compiled Projection)")] + public void FacetCompiledTest() + { + TestAdaptHelper.Loop(_person, PersonFacetCompiled, MapOperations); + TestAdaptHelper.Loop(_foo, FooFacetCompiled, MapOperations); + TestAdaptHelper.Loop(_customer, CustomerFacetCompiled, MapOperations); + } + + [Benchmark(Description = $"Mapperly {TestAdaptHelper.MapperlyVersion}")] + public void MapperlyTest() + { + TestAdaptHelper.Loop(_person, MapperlyMappings.MapPerson, MapOperations); + TestAdaptHelper.Loop(_foo, MapperlyMappings.MapFoo, MapOperations); + TestAdaptHelper.Loop(_customer, MapperlyMappings.MapCustomer, MapOperations); + } + + [GlobalSetup(Target = nameof(MapsterTest))] + public void SetupMapster() + { + SetupInstances(); + TestAdaptHelper.UseMapsterCompiler(MapsterCompilerType.Default); + _ = _person.Adapt(); + _ = _foo.Adapt(); + _ = _customer.Adapt(); + } + + [GlobalSetup(Target = nameof(RoslynTest))] + public void SetupRoslyn() + { + SetupInstances(); + TestAdaptHelper.UseMapsterCompiler(MapsterCompilerType.Roslyn); + _ = _person.Adapt(); + _ = _foo.Adapt(); + _ = _customer.Adapt(); + } + + [GlobalSetup(Target = nameof(FecTest))] + public void SetupFec() + { + SetupInstances(); + TestAdaptHelper.UseMapsterCompiler(MapsterCompilerType.FEC); + _ = _person.Adapt(); + _ = _foo.Adapt(); + _ = _customer.Adapt(); + } + + [GlobalSetup(Target = nameof(CodegenTest))] + public void SetupCodegen() => SetupInstances(); + + [GlobalSetup(Target = nameof(AutoMapperTest))] + public void SetupAutoMapper() => SetupInstances(); + + [GlobalSetup(Target = nameof(FacetTest))] + public void SetupFacet() => SetupInstances(); + + [GlobalSetup(Target = nameof(FacetCompiledTest))] + public void SetupFacetCompiled() => SetupInstances(); + + [GlobalSetup(Target = nameof(MapperlyTest))] + public void SetupMapperly() => SetupInstances(); + + private void SetupInstances() + { + _person = TestAdaptHelper.SetupPersonInstance(); + _foo = TestAdaptHelper.SetupFooInstance(); + _customer = TestAdaptHelper.SetupCustomerInstance(); + } + } +} \ No newline at end of file diff --git a/src/Benchmark/Classes/Customer.cs b/src/Benchmark/Classes/Customer.cs index 30c832c6..d78c6ca3 100644 --- a/src/Benchmark/Classes/Customer.cs +++ b/src/Benchmark/Classes/Customer.cs @@ -1,20 +1,6 @@ namespace Mapster.Benchmark.Classes { - public class Address - { - public int Id { get; set; } - public string Street { get; set; } - public string City { get; set; } - public string Country { get; set; } - } - - public class AddressDTO - { - public int Id { get; set; } - public string City { get; set; } - public string Country { get; set; } - } - + // Nested object and collection mapping source. public class Customer { public int Id { get; set; } @@ -26,6 +12,7 @@ public class Customer public ICollection
WorkAddresses { get; set; } } + // DTO with flattening and collection shape changes. public class CustomerDTO { public int Id { get; set; } @@ -36,4 +23,19 @@ public class CustomerDTO public List WorkAddresses { get; set; } public string AddressCity { get; set; } } + + public class Address + { + public int Id { get; set; } + public string Street { get; set; } + public string City { get; set; } + public string Country { get; set; } + } + + public class AddressDTO + { + public int Id { get; set; } + public string City { get; set; } + public string Country { get; set; } + } } diff --git a/src/Benchmark/Classes/Foo.cs b/src/Benchmark/Classes/Foo.cs index f12d3edb..87c7f685 100644 --- a/src/Benchmark/Classes/Foo.cs +++ b/src/Benchmark/Classes/Foo.cs @@ -1,5 +1,6 @@ namespace Mapster.Benchmark.Classes { + // Deep recursive graph with collections. public class Foo { public string Name { get; set; } @@ -26,4 +27,32 @@ public class Foo public IEnumerable Ints { get; set; } } + + // DTO copy of Foo. + public class FooDTO + { + public string Name { get; set; } + + public int Int32 { get; set; } + + public long Int64 { get; set; } + + public int? NullInt { get; set; } + + public float Floatn { get; set; } + + public double Doublen { get; set; } + + public DateTime DateTime { get; set; } + + public FooDTO Foo1 { get; set; } + + public IEnumerable Foos { get; set; } + + public FooDTO[] FooArr { get; set; } + + public int[] IntArr { get; set; } + + public IEnumerable Ints { get; set; } + } } diff --git a/src/Benchmark/Classes/Person.cs b/src/Benchmark/Classes/Person.cs new file mode 100644 index 00000000..37173a91 --- /dev/null +++ b/src/Benchmark/Classes/Person.cs @@ -0,0 +1,28 @@ +namespace Mapster.Benchmark.Classes +{ + // FlatType POCO: no nested types, no collections. Used to exercise the "best case" + // mapping path - a simple property-to-property copy with primitive/string values. + public class Person + { + public int Id { get; set; } + public string FirstName { get; set; } + public string LastName { get; set; } + public string Email { get; set; } + public int Age { get; set; } + public DateTime BirthDate { get; set; } + public decimal Salary { get; set; } + public bool IsActive { get; set; } + } + + public class PersonDTO + { + public int Id { get; set; } + public string FirstName { get; set; } + public string LastName { get; set; } + public string Email { get; set; } + public int Age { get; set; } + public DateTime BirthDate { get; set; } + public decimal Salary { get; set; } + public bool IsActive { get; set; } + } +} diff --git a/src/Benchmark/Comparisons/FacetModels.cs b/src/Benchmark/Comparisons/FacetModels.cs index ca6330b5..d557e1e8 100644 --- a/src/Benchmark/Comparisons/FacetModels.cs +++ b/src/Benchmark/Comparisons/FacetModels.cs @@ -19,4 +19,9 @@ public partial class CustomerFacetDto [MapFrom("Address.City")] public string AddressCity { get; set; } } + + [Facet(typeof(Person))] + public partial class PersonFacetDto + { + } } \ No newline at end of file diff --git a/src/Benchmark/Comparisons/MapperlyModels.cs b/src/Benchmark/Comparisons/MapperlyModels.cs index 32fac63a..3552a8d5 100644 --- a/src/Benchmark/Comparisons/MapperlyModels.cs +++ b/src/Benchmark/Comparisons/MapperlyModels.cs @@ -45,6 +45,18 @@ public class CustomerMapperlyDto public string AddressCity { get; set; } } + public class PersonMapperlyDto + { + public int Id { get; set; } + public string FirstName { get; set; } + public string LastName { get; set; } + public string Email { get; set; } + public int Age { get; set; } + public DateTime BirthDate { get; set; } + public decimal Salary { get; set; } + public bool IsActive { get; set; } + } + [Riok.Mapperly.Abstractions.Mapper(UseDeepCloning = true)] public static partial class MapperlyMappings { @@ -58,5 +70,7 @@ public static partial class MapperlyMappings [MapperIgnoreSource(nameof(Customer.Credit))] [MapProperty("Address.City", nameof(CustomerMapperlyDto.AddressCity))] public static partial CustomerMapperlyDto MapCustomer(Customer source); + + public static partial PersonMapperlyDto MapPerson(Person source); } } \ No newline at end of file diff --git a/src/Benchmark/FooMapper.g.cs b/src/Benchmark/FooMapper.g.cs index dcebe0af..0eabd97f 100644 --- a/src/Benchmark/FooMapper.g.cs +++ b/src/Benchmark/FooMapper.g.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Linq; using Mapster; using Mapster.Benchmark.Classes; @@ -9,9 +9,9 @@ namespace Mapster.Benchmark { public static partial class FooMapper { - public static Foo Map(Foo p1) + public static FooDTO Map(Foo p1) { - return p1 == null ? null : new Foo() + return p1 == null ? null : new FooDTO() { Name = p1.Name, Int32 = p1.Int32, @@ -21,25 +21,25 @@ public static Foo Map(Foo p1) Doublen = p1.Doublen, DateTime = p1.DateTime, Foo1 = Map(p1.Foo1), - Foos = p1.Foos == null ? null : p1.Foos.Select(func1), + Foos = p1.Foos == null ? null : p1.Foos.Select(func1), FooArr = func2(p1.FooArr), IntArr = func3(p1.IntArr), Ints = p1.Ints == null ? null : MapsterHelper.ToEnumerable(p1.Ints) }; } - private static Foo func1(Foo p2) + private static FooDTO func1(Foo p2) { return Map(p2); } - private static Foo[] func2(Foo[] p3) + private static FooDTO[] func2(Foo[] p3) { if (p3 == null) { return null; } - Foo[] result = new Foo[p3.Length]; + FooDTO[] result = new FooDTO[p3.Length]; int v = 0; @@ -68,4 +68,4 @@ private static int[] func3(int[] p4) } } -} \ No newline at end of file +} diff --git a/src/Benchmark/FooMapper.tt b/src/Benchmark/FooMapper.tt index 003e94e1..bb1e41b3 100644 --- a/src/Benchmark/FooMapper.tt +++ b/src/Benchmark/FooMapper.tt @@ -22,8 +22,8 @@ TypeName = "FooMapper" }; var code = foo.BuildAdapter() - .CreateMapExpression() + .CreateMapExpression() .ToScript(def); - code = code.Replace("TypeAdapter.Map.Invoke", "Map"); + code = code.Replace("TypeAdapter.Map.Invoke", "Map"); WriteLine(code); #> \ No newline at end of file diff --git a/src/Benchmark/PersonMapper.g.cs b/src/Benchmark/PersonMapper.g.cs new file mode 100644 index 00000000..5060ae02 --- /dev/null +++ b/src/Benchmark/PersonMapper.g.cs @@ -0,0 +1,23 @@ +using Mapster.Benchmark.Classes; + + +namespace Mapster.Benchmark +{ + public static partial class PersonMapper + { + public static PersonDTO Map(Person p1) + { + return p1 == null ? null : new PersonDTO() + { + Id = p1.Id, + FirstName = p1.FirstName, + LastName = p1.LastName, + Email = p1.Email, + Age = p1.Age, + BirthDate = p1.BirthDate, + Salary = p1.Salary, + IsActive = p1.IsActive + }; + } + } +} \ No newline at end of file diff --git a/src/Benchmark/PersonMapper.tt b/src/Benchmark/PersonMapper.tt new file mode 100644 index 00000000..58c609ef --- /dev/null +++ b/src/Benchmark/PersonMapper.tt @@ -0,0 +1,25 @@ +<#@ template debug="true" language="C#" #> +<#@ output extension=".g.cs" #> +using Mapster.Benchmark.Classes; + + +namespace Mapster.Benchmark +{ + public static partial class PersonMapper + { + public static PersonDTO Map(Person p1) + { + return p1 == null ? null : new PersonDTO() + { + Id = p1.Id, + FirstName = p1.FirstName, + LastName = p1.LastName, + Email = p1.Email, + Age = p1.Age, + BirthDate = p1.BirthDate, + Salary = p1.Salary, + IsActive = p1.IsActive + }; + } + } +} \ No newline at end of file diff --git a/src/Benchmark/Program.cs b/src/Benchmark/Program.cs index 80c03f9a..7da13343 100644 --- a/src/Benchmark/Program.cs +++ b/src/Benchmark/Program.cs @@ -9,9 +9,10 @@ static void Main(string[] args) { var switcher = new BenchmarkSwitcher(new[] { - typeof(TestSimpleTypes), + typeof(TestFlatTypes), + typeof(TestRecursiveTypes), typeof(TestComplexTypes), - typeof(TestAll), + typeof(TestTotalAllTypes), }); switcher.Run(args, new Config()); diff --git a/src/Benchmark/TestAdaptHelper.cs b/src/Benchmark/TestAdaptHelper.cs index 3f23a6ee..c1221113 100644 --- a/src/Benchmark/TestAdaptHelper.cs +++ b/src/Benchmark/TestAdaptHelper.cs @@ -1,194 +1,123 @@ -using AutoMapper; +using AutoMapper; using FastExpressionCompiler; using Mapster.Benchmark.Classes; -using Mapster.Benchmark.Comparisons; using System.Linq.Expressions; namespace Mapster.Benchmark { - public static class TestAdaptHelper + public enum MapsterCompilerType { - private static readonly MapperConfiguration AutoMapperConfiguration = new(cfg => - { - cfg.CreateMap(); - cfg.CreateMap(); - cfg.CreateMap(); - cfg.CreateMap() - .ForMember(destination => destination.AddressCity, - options => options.MapFrom(source => source.Address != null ? source.Address.City : null)); - }); - - private static readonly IMapper AutoMapperInstance = CreateAutoMapper(); - private static readonly Func DefaultCompiler = TypeAdapterConfig.GlobalSettings.Compiler; + Default, + Roslyn, + FEC, + } + /// + /// Minimal shared helper for the comparison benchmarks. Contains only setup data, + /// the AutoMapper instance, a Mapster compiler switch and a generic hot-loop driver. + /// + public static class TestAdaptHelper + { public const string MapsterVersion = "10.0.7"; public const string AutoMapperVersion = "14.0.0"; public const string FacetVersion = "6.5.5"; public const string MapperlyVersion = "4.3.1"; - public static Customer SetupCustomerInstance() - { - return new Customer - { - Address = new Address { City = "istanbul", Country = "turkey", Id = 1, Street = "istiklal cad." }, - HomeAddress = new Address { City = "istanbul", Country = "turkey", Id = 2, Street = "istiklal cad." }, - Id = 1, - Name = "Eduardo Najera", - Credit = 234.7m, - WorkAddresses = new List
- { - new Address {City = "istanbul", Country = "turkey", Id = 5, Street = "istiklal cad."}, - new Address {City = "izmir", Country = "turkey", Id = 6, Street = "konak"} - }, - Addresses = new[] - { - new Address {City = "istanbul", Country = "turkey", Id = 3, Street = "istiklal cad."}, - new Address {City = "izmir", Country = "turkey", Id = 4, Street = "konak"} - } - }; - } + private static readonly Func DefaultMapsterCompiler = + TypeAdapterConfig.GlobalSettings.Compiler; - public static Foo SetupFooInstance() + public static readonly IMapper AutoMapper = new MapperConfiguration(cfg => { - return new Foo + cfg.CreateMap(); + cfg.CreateMap(); + cfg.CreateMap(); + cfg.CreateMap() + .ForMember(d => d.AddressCity, + o => o.MapFrom(s => s.Address != null ? s.Address.City : null)); + cfg.CreateMap(); + }).CreateMapper(); + + public static Foo SetupFooInstance() => new Foo + { + Name = "foo", + Int32 = 12, + Int64 = 123123, + NullInt = 16, + DateTime = DateTime.Now, + Doublen = 2312112, + Foo1 = new Foo { Name = "foo one" }, + Foos = new List { - Name = "foo", - Int32 = 12, - Int64 = 123123, - NullInt = 16, - DateTime = DateTime.Now, - Doublen = 2312112, - Foo1 = new Foo { Name = "foo one" }, - Foos = new List - { - new Foo {Name = "j1", Int64 = 123, NullInt = 321}, - new Foo {Name = "j2", Int32 = 12345, NullInt = 54321}, - new Foo {Name = "j3", Int32 = 12345, NullInt = 54321} - }, - FooArr = new[] - { - new Foo {Name = "a1"}, - new Foo {Name = "a2"}, - new Foo {Name = "a3"} - }, - IntArr = new[] { 1, 2, 3, 4, 5 }, - Ints = new[] { 7, 8, 9 } - }; - } - - private static IMapper CreateAutoMapper() - { - AutoMapperConfiguration.AssertConfigurationIsValid(); - AutoMapperConfiguration.CompileMappings(); - return AutoMapperConfiguration.CreateMapper(); - } - - private static void SetupCompiler(MapsterCompilerType type) + new Foo { Name = "j1", Int64 = 123, NullInt = 321 }, + new Foo { Name = "j2", Int32 = 12345, NullInt = 54321 }, + new Foo { Name = "j3", Int32 = 12345, NullInt = 54321 }, + }, + FooArr = new[] + { + new Foo { Name = "a1" }, + new Foo { Name = "a2" }, + new Foo { Name = "a3" }, + }, + IntArr = new[] { 1, 2, 3, 4, 5 }, + Ints = new[] { 7, 8, 9 }, + }; + + public static Customer SetupCustomerInstance() => new Customer + { + Id = 1, + Name = "Eduardo Najera", + Credit = 234.7m, + Address = new Address { Id = 1, City = "istanbul", Country = "turkey", Street = "istiklal cad." }, + HomeAddress = new Address { Id = 2, City = "istanbul", Country = "turkey", Street = "istiklal cad." }, + Addresses = new[] + { + new Address { Id = 3, City = "istanbul", Country = "turkey", Street = "istiklal cad." }, + new Address { Id = 4, City = "izmir", Country = "turkey", Street = "konak" }, + }, + WorkAddresses = new List
+ { + new Address { Id = 5, City = "istanbul", Country = "turkey", Street = "istiklal cad." }, + new Address { Id = 6, City = "izmir", Country = "turkey", Street = "konak" }, + }, + }; + + public static Person SetupPersonInstance() => new Person + { + Id = 42, + FirstName = "Eduardo", + LastName = "Najera", + Email = "eduardo@example.com", + Age = 39, + BirthDate = new DateTime(1986, 7, 11), + Salary = 12345.67m, + IsActive = true, + }; + + /// + /// Switches Mapster's global expression compiler. Call this from a [GlobalSetup] before warming up the mapping. + /// + public static void UseMapsterCompiler(MapsterCompilerType type) { TypeAdapterConfig.GlobalSettings.Compiler = type switch { - MapsterCompilerType.Default => DefaultCompiler, - MapsterCompilerType.Roslyn => expression => expression.CompileWithDebugInfo(), - MapsterCompilerType.FEC => expression => expression.CompileFast(), + MapsterCompilerType.Default => DefaultMapsterCompiler, + MapsterCompilerType.Roslyn => e => e.CompileWithDebugInfo(), + MapsterCompilerType.FEC => e => e.CompileFast(), _ => throw new ArgumentOutOfRangeException(nameof(type)), }; } - public static void ConfigureMapster(TSource sourceInstance, MapsterCompilerType type) - where TSource : class - where TDestination : class - { - SetupCompiler(type); - TypeAdapterConfig.GlobalSettings.Compile(typeof(TSource), typeof(TDestination)); - _ = sourceInstance.Adapt(); - } - - public static void ConfigureAutoMapper(TSource sourceInstance) - where TSource : class - { - _ = AutoMapperInstance.Map(sourceInstance); - } - - public static void ConfigureFacet(Foo sourceInstance) + /// + /// Hot loop: invokes on times. + /// Keeps the last result alive so the JIT can't dead-code-eliminate the call. + /// + public static void Loop(TSrc src, Func map, int count) { - _ = new FooFacetDto(sourceInstance); - } + TDest r = default!; + for (var i = 0; i < count; i++) + r = map(src); - public static void ConfigureFacet(Customer sourceInstance) - { - _ = new CustomerFacetDto(sourceInstance); - } - - public static void ConfigureMapperly(Foo sourceInstance) - { - _ = MapperlyMappings.MapFoo(sourceInstance); + GC.KeepAlive(r); } - - public static void ConfigureMapperly(Customer sourceInstance) - { - _ = MapperlyMappings.MapCustomer(sourceInstance); - } - - public static void TestMapsterAdapter(TSrc item, int mapOperations) - where TSrc : class - where TDest : class, new() - { - Loop(item, source => source.Adapt(), mapOperations); - } - - public static void TestAutoMapper(TSrc item, int mapOperations) - where TSrc : class - where TDest : class, new() - { - Loop(item, source => AutoMapperInstance.Map(source), mapOperations); - } - - public static void TestFacet(Foo item, int mapOperations) - { - Loop(item, source => new FooFacetDto(source), mapOperations); - } - - public static void TestFacet(Customer item, int mapOperations) - { - Loop(item, source => new CustomerFacetDto(source), mapOperations); - } - - public static void TestMapperly(Foo item, int mapOperations) - { - Loop(item, MapperlyMappings.MapFoo, mapOperations); - } - - public static void TestMapperly(Customer item, int mapOperations) - { - Loop(item, MapperlyMappings.MapCustomer, mapOperations); - } - - public static void TestCodeGen(Foo item, int mapOperations) - { - Loop(item, FooMapper.Map, mapOperations); - } - - public static void TestCodeGen(Customer item, int mapOperations) - { - Loop(item, CustomerMapper.Map, mapOperations); - } - - private static void Loop(TSource item, Func map, int mapOperations) - { - TDestination result = default!; - for (var i = 0; i < mapOperations; i++) - { - result = map(item); - } - - GC.KeepAlive(result); - } - } - - public enum MapsterCompilerType - { - Default, - Roslyn, - FEC, } -} \ No newline at end of file +} From c89a232c576192c64ed3cf66af31eadbdefeda3e Mon Sep 17 00:00:00 2001 From: Qyperion Date: Sat, 16 May 2026 00:56:03 +0300 Subject: [PATCH 79/80] docs: add benchmark docs page and actualize results --- README.md | 24 +++++---- docs/articles/_Sidebar.md | 5 ++ docs/articles/benchmarks.md | 103 ++++++++++++++++++++++++++++++++++++ docs/articles/toc.yml | 4 +- 4 files changed, 126 insertions(+), 10 deletions(-) create mode 100644 docs/articles/benchmarks.md diff --git a/README.md b/README.md index 5f9a107a..ea8b65c3 100644 --- a/README.md +++ b/README.md @@ -149,15 +149,21 @@ Mapster was designed to be efficient on both speed and memory. The repository in - Facet - Mapperly -| Method | MapOperations | Mean | StdDev | Error | Ratio | Gen0 | Gen1 | Allocated | Alloc Ratio | -| -------- | -------------- | -----: | -------: | ------: | ------: | -----: | -----: | ----------: | ----------: | -| `Mapster 10.0.7` | 1000000 | 412,534 us | 2,704 us | 4,543 us | 1.00 | 77000 | - | 1243.59 MB | 1.00 | -| `Mapster 10.0.7 (Roslyn)` | 1000000 | 397,028 us | 5,174 us | 8,695 us | 0.96 | 75000 | - | 1205.44 MB | 0.97 | -| `Mapster 10.0.7 (FEC)` | 1000000 | 124,374 us | 1,290 us | 2,466 us | 0.30 | 74000 | - | 1182.56 MB | 0.95 | -| `Mapster 10.0.7 (Codegen)` | 1000000 | 105,214 us | 1,312 us | 2,206 us | 0.26 | 75500 | 166 | 1205.44 MB | 0.97 | -| `AutoMapper 14.0.0` | 1000000 | 600,077 us | 63,170 us | 95,505 us | 1.45 | 197000 | 1000 | 3158.59 MB | 2.54 | -| `Facet 6.5.5` | 1000000 | 628,280 us | 7,326 us | 11,076 us | 1.52 | 325000 | 1000 | 5187.99 MB | 4.17 | -| `Mapperly 4.3.1` | 1000000 | 128,521 us | 1,453 us | 2,442 us | 0.31 | 94500 | 250 | 1510.62 MB | 1.21 | +The snapshot below shows the `FlatTypes` scenario (`Person -> PersonDTO`), a best-case DTO with simple property-to-property mapping and no nested objects or collections. + +> [!NOTE] +> More complex object shapes can change the relative results. See the [complete benchmark results](https://mapstermapper.github.io/Mapster/articles/benchmarks.html) for `ComplexTypes`, `RecursiveTypes`, and `TotalAllTypes`. + +| Method | MapOperations | Mean | StdDev | Error | Ns/Map | Ratio | Gen0 | Allocated | Alloc Ratio | Bytes/Map | +|------------------------------------ |-------------- |----------:|----------:|----------:|-------:|------:|-----:|----------:|------------:|----------:| +| `Mapster 10.0.7` | 1000000 | 6.849 ms | 0.6851 ms | 1.0358 ms | 6.849 | 1.01 | 4781 | 76.29 MB | 1.00 | 80 | +| `Mapster 10.0.7 (Roslyn)` | 1000000 | 6.579 ms | 0.2782 ms | 0.4206 ms | 6.579 | 0.97 | 4781 | 76.29 MB | 1.00 | 80 | +| `Mapster 10.0.7 (FEC)` | 1000000 | 6.549 ms | 0.9130 ms | 1.3803 ms | 6.549 | 0.97 | 4781 | 76.29 MB | 1.00 | 80 | +| `Mapster 10.0.7 (Codegen)` | 1000000 | 5.868 ms | 0.3266 ms | 0.5488 ms | 5.868 | 0.86 | 4781 | 76.29 MB | 1.00 | 80 | +| `AutoMapper 14.0.0` | 1000000 | 29.645 ms | 0.8963 ms | 1.5062 ms | 29.645 | 4.37 | 4750 | 76.29 MB | 1.00 | 80 | +| `Facet 6.5.5` | 1000000 | 7.801 ms | 1.0231 ms | 1.5467 ms | 7.801 | 1.15 | 8601 | 137.33 MB | 1.80 | 144 | +| `Facet 6.5.5 (Compiled Projection)` | 1000000 | 5.508 ms | 0.7064 ms | 1.0679 ms | 5.508 | 0.81 | 4781 | 76.29 MB | 1.00 | 80 | +| `Mapperly 4.3.1` | 1000000 | 6.521 ms | 0.8369 ms | 1.2652 ms | 6.521 | 0.96 | 4781 | 76.29 MB | 1.00 | 80 | ### Step into debugging diff --git a/docs/articles/_Sidebar.md b/docs/articles/_Sidebar.md index 22c58675..dec82b38 100644 --- a/docs/articles/_Sidebar.md +++ b/docs/articles/_Sidebar.md @@ -54,3 +54,8 @@ * [Fluent API](https://github.com/MapsterMapper/Mapster/wiki/Fluent-API-Code-generation) * [Attributes](https://github.com/MapsterMapper/Mapster/wiki/Attribute-base-Code-generation) * [Interfaces](https://github.com/MapsterMapper/Mapster/wiki/Interface-base-Code-generation) + +## Benchmarks + +* [Benchmark results](https://mapstermapper.github.io/Mapster/articles/benchmarks.html) +* \ No newline at end of file diff --git a/docs/articles/benchmarks.md b/docs/articles/benchmarks.md new file mode 100644 index 00000000..7d524544 --- /dev/null +++ b/docs/articles/benchmarks.md @@ -0,0 +1,103 @@ +--- +uid: Mapster.Benchmarks +title: "Benchmark results" +--- + +Mapster includes a benchmark project in [`src/Benchmark`](https://github.com/MapsterMapper/Mapster/tree/master/src/Benchmark) that compares the local Mapster build with AutoMapper, Facet, and Mapperly across several object shapes. + +This page is a May 2026 snapshot of those benchmarks. Treat the numbers as a comparison point for this environment and benchmark configuration rather than an absolute guarantee for every machine or application. + +## How to read the tables + +- `Mean` is the total time for one BenchmarkDotNet benchmark invocation. Each invocation runs a manual hot loop with `MapOperations = 1,000,000`. +- `Ns/Map` normalizes `Mean` to the approximate cost of one logical mapping call. +- `Allocated` is the memory allocated by one benchmark invocation. +- `Bytes/Map` normalizes `Allocated` to one logical mapping call. +- `Ratio` and `Alloc Ratio` are relative to the default Mapster benchmark in the same scenario. +- `TotalAllTypes` runs three mapping scenarios in one benchmark invocation, so its `Ns/Map` and `Bytes/Map` values are divided by `MapOperations * 3`. + +## Benchmark scenarios + +### FlatTypes + +`FlatTypes` maps `Person -> PersonDTO`. It is a flat DTO shape: simple property-to-property copy, no nested objects, and no collections. This scenario mostly highlights mapper call overhead, generated IL quality, and allocation rate for a best-case DTO. + +### ComplexTypes + +`ComplexTypes` maps `Customer -> CustomerDTO`. It includes nested address mapping, array/list shape changes, and a flattening rule (`AddressCity <- Address.City`). This scenario is useful for typical DTOs that combine nested objects, collections, and a small amount of custom member mapping. + +### RecursiveTypes + +`RecursiveTypes` maps `Foo -> FooDTO`. The type shape is self-recursive: a `Foo` can contain another `Foo`, an enumerable of `Foo`, and an array of `Foo`. The sample data does not intentionally create a back-reference cycle, but the mapping graph is deeper and allocates more nested DTOs than the flat or customer scenarios. + +### TotalAllTypes + +`TotalAllTypes` runs `FlatTypes`, `RecursiveTypes`, and `ComplexTypes` sequentially in one benchmark method. `Mean` is therefore the total batch time for all three scenarios; use `Ns/Map` and `Bytes/Map` for normalized per-map interpretation. + +## Compared methods + +- `Mapster` uses the default Mapster expression compiler. +- `Mapster (Roslyn)` uses the Roslyn/debug-info compiler path. +- `Mapster (FEC)` uses FastExpressionCompiler. +- `Mapster (Codegen)` uses generated mapping code. +- `AutoMapper`, `Facet`, and `Mapperly` are included as external comparison points. +- `Facet (Compiled Projection)` uses Facet's compiled projection path, which can behave very differently from the constructor path depending on the object shape. + +## Results + +### FlatTypes + +| Scenario | Method | MapOperations | Mean | StdDev | Error | Ns/Map | Ratio | Gen0 | Allocated | Alloc Ratio | Bytes/Map | +|---------- |------------------------------------ |-------------- |----------:|----------:|----------:|-------:|------:|-----:|----------:|------------:|----------:| +| FlatTypes | `Mapster 10.0.7` | 1000000 | 6.849 ms | 0.6851 ms | 1.0358 ms | 6.849 | 1.01 | 4781 | 76.29 MB | 1.00 | 80 | +| FlatTypes | `Mapster 10.0.7 (Roslyn)` | 1000000 | 6.579 ms | 0.2782 ms | 0.4206 ms | 6.579 | 0.97 | 4781 | 76.29 MB | 1.00 | 80 | +| FlatTypes | `Mapster 10.0.7 (FEC)` | 1000000 | 6.549 ms | 0.9130 ms | 1.3803 ms | 6.549 | 0.97 | 4781 | 76.29 MB | 1.00 | 80 | +| FlatTypes | `Mapster 10.0.7 (Codegen)` | 1000000 | 5.868 ms | 0.3266 ms | 0.5488 ms | 5.868 | 0.86 | 4781 | 76.29 MB | 1.00 | 80 | +| FlatTypes | `AutoMapper 14.0.0` | 1000000 | 29.645 ms | 0.8963 ms | 1.5062 ms | 29.645 | 4.37 | 4750 | 76.29 MB | 1.00 | 80 | +| FlatTypes | `Facet 6.5.5` | 1000000 | 7.801 ms | 1.0231 ms | 1.5467 ms | 7.801 | 1.15 | 8601 | 137.33 MB | 1.80 | 144 | +| FlatTypes | `Facet 6.5.5 (Compiled Projection)` | 1000000 | 5.508 ms | 0.7064 ms | 1.0679 ms | 5.508 | 0.81 | 4781 | 76.29 MB | 1.00 | 80 | +| FlatTypes | `Mapperly 4.3.1` | 1000000 | 6.521 ms | 0.8369 ms | 1.2652 ms | 6.521 | 0.96 | 4781 | 76.29 MB | 1.00 | 80 | + +### ComplexTypes + +| Scenario | Method | MapOperations | Mean | StdDev | Error | Ns/Map | Ratio | Gen0 | Gen1 | Allocated | Alloc Ratio | Bytes/Map | +|------------- |------------------------------------ |-------------- |----------:|----------:|----------:|--------:|------:|-------:|-----:|-----------:|------------:|----------:| +| ComplexTypes | `Mapster 10.0.7` | 1000000 | 78.59 ms | 1.519 ms | 2.553 ms | 78.586 | 1.00 | 28111 | - | 450.13 MB | 1.00 | 472 | +| ComplexTypes | `Mapster 10.0.7 (Roslyn)` | 1000000 | 53.48 ms | 1.760 ms | 2.958 ms | 53.475 | 0.68 | 25818 | - | 411.99 MB | 0.92 | 432 | +| ComplexTypes | `Mapster 10.0.7 (FEC)` | 1000000 | 69.29 ms | 1.539 ms | 2.326 ms | 69.288 | 0.88 | 28200 | - | 450.13 MB | 1.00 | 472 | +| ComplexTypes | `Mapster 10.0.7 (Codegen)` | 1000000 | 51.86 ms | 2.514 ms | 3.801 ms | 51.863 | 0.66 | 25750 | - | 411.99 MB | 0.92 | 432 | +| ComplexTypes | `AutoMapper 14.0.0` | 1000000 | 112.27 ms | 4.516 ms | 7.590 ms | 112.270 | 1.43 | 29142 | - | 465.39 MB | 1.03 | 488 | +| ComplexTypes | `Facet 6.5.5` | 1000000 | 446.72 ms | 51.706 ms | 78.172 ms | 446.725 | 5.69 | 175000 | 500 | 2792.36 MB | 6.20 | 2928 | +| ComplexTypes | `Facet 6.5.5 (Compiled Projection)` | 1000000 | 678.87 ms | 42.646 ms | 64.474 ms | 678.868 | 8.64 | 44000 | - | 709.53 MB | 1.58 | 744 | +| ComplexTypes | `Mapperly 4.3.1` | 1000000 | 52.81 ms | 1.134 ms | 1.714 ms | 52.812 | 0.67 | 25769 | - | 411.99 MB | 0.92 | 432 | + +### RecursiveTypes + +| Scenario | Method | MapOperations | Mean | StdDev | Error | Ns/Map | Ratio | Gen0 | Gen1 | Allocated | Alloc Ratio | Bytes/Map | +|--------------- |------------------------------------ |-------------- |----------:|----------:|----------:|--------:|------:|-------:|-----:|-----------:|------------:|----------:| +| RecursiveTypes | `Mapster 10.0.7` | 1000000 | 404.49 ms | 63.593 ms | 96.14 ms | 404.486 | 1.02 | 49000 | - | 793.46 MB | 1.00 | 832 | +| RecursiveTypes | `Mapster 10.0.7 (Roslyn)` | 1000000 | 383.00 ms | 34.563 ms | 52.25 ms | 382.999 | 0.97 | 49000 | - | 793.46 MB | 1.00 | 832 | +| RecursiveTypes | `Mapster 10.0.7 (FEC)` | 1000000 | 84.82 ms | 10.777 ms | 16.29 ms | 84.816 | 0.21 | 45875 | 125 | 732.42 MB | 0.92 | 768 | +| RecursiveTypes | `Mapster 10.0.7 (Codegen)` | 1000000 | 82.81 ms | 11.104 ms | 16.79 ms | 82.806 | 0.21 | 49625 | 125 | 793.46 MB | 1.00 | 832 | +| RecursiveTypes | `AutoMapper 14.0.0` | 1000000 | 585.42 ms | 88.660 ms | 134.04 ms | 585.425 | 1.48 | 168000 | 1000 | 2693.18 MB | 3.39 | 2824 | +| RecursiveTypes | `Facet 6.5.5` | 1000000 | 302.24 ms | 9.444 ms | 18.06 ms | 302.241 | 0.76 | 150000 | 666 | 2395.63 MB | 3.02 | 2512 | +| RecursiveTypes | `Facet 6.5.5 (Compiled Projection)` | 1000000 | 708.77 ms | 58.207 ms | 88.00 ms | 708.770 | 1.79 | 73000 | - | 1174.93 MB | 1.48 | 1232 | +| RecursiveTypes | `Mapperly 4.3.1` | 1000000 | 116.05 ms | 16.639 ms | 25.16 ms | 116.055 | 0.29 | 68833 | 166 | 1098.63 MB | 1.38 | 1152 | + +### TotalAllTypes + +| Scenario | Method | MapOperations | Mean | StdDev | Error | Ns/Map | Ratio | Gen0 | Gen1 | Allocated | Alloc Ratio | Bytes/Map | +|-------------- |------------------------------------ |-------------- |-----------:|---------:|----------:|-------:|------:|-------:|-----:|----------:|------------:|----------:| +| TotalAllTypes | `Mapster 10.0.7` | 1000000 | 413.9 ms | 16.88 ms | 25.52 ms | 137.97 | 1.00 | 82000 | - | 1.29 GB | 1.00 | 461 | +| TotalAllTypes | `Mapster 10.0.7 (Roslyn)` | 1000000 | 546.4 ms | 29.01 ms | 43.86 ms | 182.12 | 1.32 | 80000 | - | 1.25 GB | 0.97 | 448 | +| TotalAllTypes | `Mapster 10.0.7 (FEC)` | 1000000 | 155.7 ms | 24.48 ms | 37.01 ms | 51.91 | 0.38 | 78800 | - | 1.23 GB | 0.95 | 440 | +| TotalAllTypes | `Mapster 10.0.7 (Codegen)` | 1000000 | 117.4 ms | 8.56 ms | 12.94 ms | 39.14 | 0.28 | 80250 | - | 1.25 GB | 0.97 | 448 | +| TotalAllTypes | `AutoMapper 14.0.0` | 1000000 | 681.4 ms | 76.25 ms | 115.27 ms | 227.13 | 1.65 | 202000 | 1000 | 3.16 GB | 2.45 | 1130 | +| TotalAllTypes | `Facet 6.5.5` | 1000000 | 677.4 ms | 33.30 ms | 55.96 ms | 225.80 | 1.64 | 329000 | 1000 | 5.14 GB | 3.99 | 1840 | +| TotalAllTypes | `Facet 6.5.5 (Compiled Projection)` | 1000000 | 1,337.4 ms | 42.95 ms | 64.94 ms | 445.81 | 3.24 | 122000 | - | 1.91 GB | 1.49 | 685 | +| TotalAllTypes | `Mapperly 4.3.1` | 1000000 | 141.5 ms | 3.04 ms | 5.81 ms | 47.10 | 0.34 | 99250 | 250 | 1.55 GB | 1.20 | 554 | + +## Interpretation notes + +- The fastest method depends on the object shape. Flat DTOs mostly measure low-level call overhead; recursive graphs and collection-heavy DTOs shift the bottleneck toward nested object creation and collection mapping. +- `Facet` constructor and compiled-projection paths are shown separately because they exercise different APIs and can trade CPU time for allocation behavior differently across scenarios. diff --git a/docs/articles/toc.yml b/docs/articles/toc.yml index 245cfb26..d218f825 100644 --- a/docs/articles/toc.yml +++ b/docs/articles/toc.yml @@ -15,4 +15,6 @@ items: topicHref: xref:Mapster.Packages.Async - name: Tools href: tools/toc.yml - topicHref: xref:Mapster.Tools.MapsterTool.Overview \ No newline at end of file + topicHref: xref:Mapster.Tools.MapsterTool.Overview +- name: Benchmarks + href: benchmarks.md \ No newline at end of file From 0562073dfd832d86acb0141458066351651ee1ae Mon Sep 17 00:00:00 2001 From: Qyperion Date: Sun, 17 May 2026 12:33:25 +0300 Subject: [PATCH 80/80] fix(docs): replace Benchmark project link in README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index ea8b65c3..c73ff524 100644 --- a/README.md +++ b/README.md @@ -141,7 +141,7 @@ public static class StudentMapper { ### Performance & Memory efficient -Mapster was designed to be efficient on both speed and memory. The repository includes a benchmark project in [`src/Benchmark`](src/Benchmark) that compares the local Mapster build, its compiler variants, and other modern mapping libraries like AutoMapper, Mapperly, and Facet. +Mapster was designed to be efficient on both speed and memory. The repository includes a benchmark project in [`src/Benchmark`](https://github.com/MapsterMapper/Mapster/tree/master/src/Benchmark) that compares the local Mapster build, its compiler variants, and other modern mapping libraries like AutoMapper, Mapperly, and Facet. - [Roslyn Compiler](https://mapstermapper.github.io/Mapster/articles/packages/ExpressionDebugging.html) - [FEC](https://mapstermapper.github.io/Mapster/articles/packages/FastExpressionCompiler.html)