diff --git a/src/Directory.Build.props b/src/Directory.Build.props index ab75d96d..99ef89c6 100644 --- a/src/Directory.Build.props +++ b/src/Directory.Build.props @@ -2,7 +2,7 @@ false - 10.1.0-pre02 + 10.0.12 netstandard2.0;net10.0;net9.0;net8.0 netstandard2.0;net10.0;net9.0;net8.0 net10.0;net9.0;net8.0 diff --git a/src/Mapster.Tests/WhenIgnoringConditionally.cs b/src/Mapster.Tests/WhenIgnoringConditionally.cs index 9014d133..b8292609 100644 --- a/src/Mapster.Tests/WhenIgnoringConditionally.cs +++ b/src/Mapster.Tests/WhenIgnoringConditionally.cs @@ -172,6 +172,43 @@ public void IgnoreIf_Apply_To_RecordType() dto.Name.ShouldBeNull(); } + // Regression test for https://github.com/MapsterMapper/Mapster/issues/1007 + // Unlike SimpleRecord above (which maps every member through the primary + // constructor), NativeRecord is a real C# `record` whose Name property is + // NOT part of the primary constructor — it's a plain settable auto-property. + // Members mapped that way go through RecordTypeAdapter.RecordInlineExpression, + // a separate code path from constructor-argument mapping, which used to + // ignore member.Ignore.Condition entirely. + [TestMethod] + public void IgnoreIf_Apply_To_RecordType_Property_Not_In_Constructor_Map() + { + TypeAdapterConfig.NewConfig() + .IgnoreIf((src, dest) => src.Name == "TestName", dest => dest.Name) + .Compile(); + + var poco = new SimplePoco { Id = 1, Name = "TestName" }; + var dto = TypeAdapter.Adapt(poco); + + dto.Id.ShouldBe(1); + dto.Name.ShouldBeNull(); + } + + [TestMethod] + public void IgnoreIf_Apply_To_RecordType_Property_Not_In_Constructor_MapToTarget() + { + TypeAdapterConfig.NewConfig() + .IgnoreIf((src, dest) => src.Name == "TestName", dest => dest.Name) + .Compile(); + + var poco = new SimplePoco { Id = 1, Name = "TestName" }; + var dto = new NativeRecord { Id = 999, Name = "DtoName" }; + + var result = TypeAdapter.Adapt(poco, dto); + + result.Id.ShouldBe(1); + result.Name.ShouldBe("DtoName"); + } + #endregion @@ -202,6 +239,12 @@ public SimpleRecord(int id, string name) } } + public record NativeRecord + { + public int Id { get; set; } + public string Name { get; set; } + } + #endregion } diff --git a/src/Mapster/Adapters/RecordTypeAdapter.cs b/src/Mapster/Adapters/RecordTypeAdapter.cs index 9a5bdb31..18954ef2 100644 --- a/src/Mapster/Adapters/RecordTypeAdapter.cs +++ b/src/Mapster/Adapters/RecordTypeAdapter.cs @@ -103,6 +103,28 @@ protected override Expression CreateInstantiationExpression(Expression source, E } } + // IgnoreIf(...) support — unlike ClassAdapter (which drops the whole + // member statement into an `if (!condition) { ... }` block), record + // members are bound inline inside a MemberInit, so the condition is + // expressed as a ternary: keep the computed value when the condition + // is false, otherwise fall back to whatever the member should be left + // as (the existing destination value for MapToTarget, or the type's + // default value when constructing a brand-new instance). + if (arg.MapType != MapType.Projection && member.Ignore.Condition != null) + { + var conditionDestination = destination ?? arg.DestinationType.CreateDefault(); + var conditionBody = member.Ignore.IsChildPath + ? member.Ignore.Condition.Body + : member.Ignore.Condition.Apply(arg.MapType, source, conditionDestination); + var notIgnored = ExpressionEx.Not(conditionBody); + + var fallback = arg.MapType == MapType.MapToTarget && destination != null + ? member.DestinationMember.GetExpression(destination) + : member.DestinationMember.Type.CreateDefault(); + + adapt = Expression.Condition(notIgnored, adapt, fallback); + } + //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