From 45499acae60e8eba6c6c9f9a7323a3e56eb560ef Mon Sep 17 00:00:00 2001 From: wuyangfan Date: Mon, 25 May 2026 16:01:05 +0800 Subject: [PATCH 1/3] fix: honor per-type ShallowCopyForSameType overrides (#938) Use merged mapping settings when deciding same-type shallow copy so a global default can be overridden per explicit config without disabling nested same-type shallow copies for other types. Co-authored-by: Cursor --- .../WhenPerTypeShallowCopyOverride.cs | 108 ++++++++++++++++++ src/Mapster/Adapters/BaseAdapter.cs | 8 +- 2 files changed, 114 insertions(+), 2 deletions(-) create mode 100644 src/Mapster.Tests/WhenPerTypeShallowCopyOverride.cs diff --git a/src/Mapster.Tests/WhenPerTypeShallowCopyOverride.cs b/src/Mapster.Tests/WhenPerTypeShallowCopyOverride.cs new file mode 100644 index 00000000..695c2796 --- /dev/null +++ b/src/Mapster.Tests/WhenPerTypeShallowCopyOverride.cs @@ -0,0 +1,108 @@ +using Mapster.Models; +using MapsterMapper; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Shouldly; + +namespace Mapster.Tests +{ + /// + /// https://github.com/MapsterMapper/Mapster/issues/938 + /// + [TestClass] + public class WhenPerTypeShallowCopyOverride + { + [TestMethod] + public void GlobalShallowCopy_WithPerTypeDeepCopy_ShouldRestoreViaMapToTarget() + { + var cfg = new TypeAdapterConfig(); + cfg.RequireExplicitMapping = true; + cfg.Default.ShallowCopyForSameType(true); + cfg.Default.AvoidInlineMapping(true); + + cfg.NewConfig(); + cfg.NewConfig(); + + cfg.NewConfig() + .ShallowCopyForSameType(false); + + cfg.GetMergedSettings(new TypeTuple(typeof(MyFailStuff), typeof(MyFailStuff)), MapType.Map) + .ShallowCopyForSameType.ShouldBe(false); + + cfg.Compile(); + + var mapper = new Mapper(cfg); + + var dynamicStuff = new MyFailStuff(); + dynamicStuff.Item1 = new RandomObject1(); + dynamicStuff.Item1.SampleName = "SN1"; + dynamicStuff.Item2 = new RandomObject2(); + dynamicStuff.Item2.SampleNumber = 2; + + var originalStuff = mapper.Map(dynamicStuff); + + dynamicStuff.Item1.SampleName = "SN1CHANGED"; + dynamicStuff.Item2.SampleNumber = 3; + + mapper.Map(originalStuff, dynamicStuff); + + dynamicStuff.Item1.SampleName.ShouldBe("SN1"); + dynamicStuff.Item2.SampleNumber.ShouldBe(2); + ReferenceEquals(dynamicStuff.Item1, originalStuff.Item1).ShouldBeFalse(); + ReferenceEquals(dynamicStuff.Item2, originalStuff.Item2).ShouldBeFalse(); + } + + [TestMethod] + public void ParentDeepCopyOverride_ShouldNotDisableImplicitNestedShallowCopyForSameType() + { + var cfg = new TypeAdapterConfig(); + cfg.Default.ShallowCopyForSameType(true); + cfg.NewConfig() + .ShallowCopyForSameType(false); + + cfg.Compile(); + + var src = new Container { Child = new NestedChild { Value = 1 } }; + var dest = src.Adapt(cfg); + + ReferenceEquals(src.Child, dest.Child).ShouldBeTrue(); + } + + public class Container + { + public NestedChild? Child { get; set; } + } + + public class NestedChild + { + public int Value { get; set; } + } + + public class MyFailStuff + { + public MyFailStuff() + { + CreateEmptyEntities(); + } + + public void CreateEmptyEntities() + { + Item1 ??= new RandomObject1(); + Item2 ??= new RandomObject2(); + } + + public RandomObject1? Item1 { get; set; } + + public RandomObject2? Item2 { get; set; } + } + + public class RandomObject1 + { + public string? SampleName { get; set; } + } + + public class RandomObject2 + { + public int SampleNumber { get; set; } + } + } +} diff --git a/src/Mapster/Adapters/BaseAdapter.cs b/src/Mapster/Adapters/BaseAdapter.cs index b31a0dbe..479095ec 100644 --- a/src/Mapster/Adapters/BaseAdapter.cs +++ b/src/Mapster/Adapters/BaseAdapter.cs @@ -503,8 +503,12 @@ internal Expression CreateAdaptExpression(Expression source, Type destinationTyp var notUsingDestinationValue = mapping is not { UseDestinationValue: true }; Expression exp; - if (_source.Type == destinationType && arg.Settings.ShallowCopyForSameType == true - && notUsingDestinationValue && rule == null) + var shallowCopySettings = _source.Type == destinationType + ? arg.Context.Config.GetMergedSettings(tuple, arg.MapType) + : arg.Settings; + + if (_source.Type == destinationType && shallowCopySettings.ShallowCopyForSameType == true + && notUsingDestinationValue) exp = _source; else if (source is ConditionalExpression cond && mapping != null) { From 44b1479b9ca1deaa51496d27915b92b0a8fee458 Mon Sep 17 00:00:00 2001 From: wuyangfan Date: Wed, 27 May 2026 01:04:12 +0800 Subject: [PATCH 2/3] fix: keep shallow copy limited to implicit same-type mappings Restore the rule == null guard alongside merged settings so explicit type configs still map nested members on MapToTarget instead of reusing source references. Co-authored-by: Cursor --- 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 479095ec..f7de684e 100644 --- a/src/Mapster/Adapters/BaseAdapter.cs +++ b/src/Mapster/Adapters/BaseAdapter.cs @@ -508,7 +508,7 @@ internal Expression CreateAdaptExpression(Expression source, Type destinationTyp : arg.Settings; if (_source.Type == destinationType && shallowCopySettings.ShallowCopyForSameType == true - && notUsingDestinationValue) + && notUsingDestinationValue && rule == null) exp = _source; else if (source is ConditionalExpression cond && mapping != null) { From 669a53df822fa0eb6cebf677d50b2f43c591de27 Mon Sep 17 00:00:00 2001 From: wuyangfan Date: Wed, 27 May 2026 09:48:13 +0800 Subject: [PATCH 3/3] fix: preserve inherited ShallowCopyForSameType on nested same-type maps Only substitute GetMergedSettings when the same-type pair has an explicit rule, or when inherited settings are not already shallow-copy enabled. Fixes NewInstanceConfigurationTest while keeping #938 overrides. Co-authored-by: Cursor --- src/Mapster/Adapters/BaseAdapter.cs | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/src/Mapster/Adapters/BaseAdapter.cs b/src/Mapster/Adapters/BaseAdapter.cs index f7de684e..41c43e28 100644 --- a/src/Mapster/Adapters/BaseAdapter.cs +++ b/src/Mapster/Adapters/BaseAdapter.cs @@ -503,9 +503,14 @@ internal Expression CreateAdaptExpression(Expression source, Type destinationTyp var notUsingDestinationValue = mapping is not { UseDestinationValue: true }; Expression exp; - var shallowCopySettings = _source.Type == destinationType - ? arg.Context.Config.GetMergedSettings(tuple, arg.MapType) - : arg.Settings; + var shallowCopySettings = arg.Settings; + if (_source.Type == destinationType) + { + if (arg.Context.Config.RuleMap.ContainsKey(tuple)) + shallowCopySettings = arg.Context.Config.GetMergedSettings(tuple, arg.MapType); + else if (arg.Settings.ShallowCopyForSameType != true) + shallowCopySettings = arg.Context.Config.GetMergedSettings(tuple, arg.MapType); + } if (_source.Type == destinationType && shallowCopySettings.ShallowCopyForSameType == true && notUsingDestinationValue && rule == null)