diff --git a/src/Mapster.Core/Utils/RecordTypeIdentityHelper.cs b/src/Mapster.Core/Utils/RecordTypeIdentityHelper.cs
new file mode 100644
index 00000000..4e5aa197
--- /dev/null
+++ b/src/Mapster.Core/Utils/RecordTypeIdentityHelper.cs
@@ -0,0 +1,47 @@
+using System;
+using System.Linq;
+using System.Reflection;
+
+namespace Mapster.Utils
+{
+ ///
+ /// CheckTools from Distinctive features of RecordType according to specification:
+ /// https://github.com/dotnet/docs/blob/main/docs/csharp/language-reference/builtin-types/record.md
+ ///
+ public static class RecordTypeIdentityHelper
+ {
+ private static bool IsRecordСonstructor(Type type)
+ {
+ var ctors = type.GetConstructors(BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public).ToList();
+
+ if (ctors.Count < 2)
+ return false;
+
+ var isRecordTypeCtor = type.GetConstructors(BindingFlags.Instance | BindingFlags.NonPublic)
+ .Where(x => x.IsFamily == true || (type.IsSealed && x.IsPrivate == true)) // add target from Sealed record
+ .Any(x => x.GetParameters()
+ .Any(y => y.ParameterType == type));
+
+ if (isRecordTypeCtor)
+ return true;
+
+ return false;
+ }
+
+ private static bool IsIncludedRecordCloneMethod(Type type)
+ {
+ if( type.GetMethod("$")?.MethodImplementationFlags.HasFlag(MethodImplAttributes.IL) == true)
+ return true;
+
+ return false;
+ }
+
+ public static bool IsRecordType(Type type)
+ {
+ if (IsRecordСonstructor(type) && IsIncludedRecordCloneMethod(type))
+ return true;
+
+ return false;
+ }
+ }
+}
diff --git a/src/Mapster.Tests/WhenIgnoringConditionally.cs b/src/Mapster.Tests/WhenIgnoringConditionally.cs
index a92106f0..d7377a37 100644
--- a/src/Mapster.Tests/WhenIgnoringConditionally.cs
+++ b/src/Mapster.Tests/WhenIgnoringConditionally.cs
@@ -160,6 +160,7 @@ public void IgnoreIf_Can_Be_Combined()
public void IgnoreIf_Apply_To_RecordType()
{
TypeAdapterConfig.NewConfig()
+ .EnableNonPublicMembers(true) // add or
.IgnoreIf((src, dest) => src.Name == "TestName", dest => dest.Name)
.Compile();
@@ -187,7 +188,7 @@ public class SimpleDto
public string Name { get; set; }
}
- public class SimpleRecord
+ public class SimpleRecord // or Replace on record
{
public int Id { get; }
public string Name { get; }
diff --git a/src/Mapster.Tests/WhenMappingRecordRegression.cs b/src/Mapster.Tests/WhenMappingRecordRegression.cs
new file mode 100644
index 00000000..f5ef6add
--- /dev/null
+++ b/src/Mapster.Tests/WhenMappingRecordRegression.cs
@@ -0,0 +1,441 @@
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+using Shouldly;
+using System;
+using System.Collections.Generic;
+
+namespace Mapster.Tests
+{
+ ///
+ /// Tests for https://github.com/MapsterMapper/Mapster/issues/537
+ ///
+ [TestClass]
+ public class WhenMappingRecordRegression
+ {
+ [TestMethod]
+ public void AdaptRecordToRecord()
+ {
+ var _source = new TestRecord() { X = 700 };
+ var _destination = new TestRecord() { X = 500 };
+ var _result = _source.Adapt(_destination);
+
+ _result.X.ShouldBe(700);
+ object.ReferenceEquals(_result, _destination).ShouldBeFalse();
+ }
+
+ [TestMethod]
+ public void AdaptPositionalRecordToPositionalRecord()
+ {
+ var _sourcePositional = new TestRecordPositional(600);
+ var _destinationPositional = new TestRecordPositional(900);
+ var _positionalResult = _sourcePositional.Adapt(_destinationPositional);
+
+ _positionalResult.X.ShouldBe(600);
+ object.ReferenceEquals(_destinationPositional, _positionalResult).ShouldBeFalse();
+ }
+
+ [TestMethod]
+ public void AdaptRecordStructToRecordStruct()
+ {
+ var _sourceStruct = new TestRecordStruct() { X = 1000 };
+ var _destinationStruct = new TestRecordStruct() { X = 800 };
+ var _structResult = _sourceStruct.Adapt(_destinationStruct);
+
+ _structResult.X.ShouldBe(1000);
+ object.ReferenceEquals(_destinationStruct, _structResult).ShouldBeFalse();
+ }
+
+ [TestMethod]
+ public void AdaptRecordToClass()
+ {
+ var _sourсe = new TestRecordPositional(200);
+ var _destination = new TestClassProtectedCtr(400);
+ var _result = _sourсe.Adapt(_destination);
+
+ _destination.ShouldBeOfType();
+ _destination.X.ShouldBe(200);
+ object.ReferenceEquals(_destination, _result).ShouldBeTrue();
+ }
+
+ [TestMethod]
+ public void AdaptClassToRecord()
+ {
+ var _sourсe = new TestClassProtectedCtr(200);
+ var _destination = new TestRecordPositional(400);
+ var _result = _sourсe.Adapt(_destination);
+
+ _destination.ShouldBeOfType();
+ _result.X.ShouldBe(200);
+ object.ReferenceEquals(_destination, _result).ShouldBeFalse();
+ }
+
+ [TestMethod]
+ public void AdaptToSealtedRecord()
+ {
+ var _sourceRecord = new TestRecord() { X = 2000 };
+ var _destinationSealtedRecord = new TestSealedRecord() { X = 3000 };
+ var _RecordResult = _sourceRecord.Adapt(_destinationSealtedRecord);
+
+ _RecordResult.X.ShouldBe(2000);
+ object.ReferenceEquals(_destinationSealtedRecord, _RecordResult).ShouldBeFalse();
+ }
+
+ [TestMethod]
+ public void AdaptToSealtedPositionalRecord()
+ {
+ var _sourceRecord = new TestRecord() { X = 2000 };
+ var _destinationSealtedPositionalRecord = new TestSealedRecordPositional(4000);
+ var _RecordResult = _sourceRecord.Adapt(_destinationSealtedPositionalRecord);
+
+ _RecordResult.X.ShouldBe(2000);
+ object.ReferenceEquals(_destinationSealtedPositionalRecord, _RecordResult).ShouldBeFalse();
+ }
+
+ [TestMethod]
+ public void AdaptClassToClassPublicCtrIsNotInstanse()
+ {
+ var _source = new TestClassPublicCtr(200);
+ var _destination = new TestClassPublicCtr(400);
+ var _result = _source.Adapt(_destination);
+
+ _destination.ShouldBeOfType();
+ _destination.X.ShouldBe(200);
+ object.ReferenceEquals(_destination, _result).ShouldBeTrue();
+ }
+
+ [TestMethod]
+ public void AdaptClassToClassProtectdCtrIsNotInstanse()
+ {
+ var _source = new TestClassPublicCtr(200);
+ var _destination = new TestClassProtectedCtr(400);
+ var _result = _source.Adapt(_destination);
+
+ _destination.ShouldBeOfType();
+ _destination.X.ShouldBe(200);
+ object.ReferenceEquals(_destination, _result).ShouldBeTrue();
+ }
+
+ ///
+ /// https://github.com/MapsterMapper/Mapster/issues/615
+ ///
+ [TestMethod]
+ public void AdaptClassIncludeStruct()
+ {
+ TypeAdapterConfig
+ .ForType()
+ .Map(x => x.TestStruct, x => x.SourceWithStruct.TestStruct);
+
+ var source = new SourceWithClass
+ {
+ SourceWithStruct = new SourceWithStruct
+ {
+ TestStruct = new TestStruct("A")
+ }
+ };
+
+ var destination = source.Adapt();
+ destination.TestStruct.Property.ShouldBe("A");
+ }
+
+ ///
+ /// https://github.com/MapsterMapper/Mapster/issues/482
+ ///
+ [TestMethod]
+ public void AdaptClassToClassFromPrivatePropertyIsNotInstanse()
+ {
+ var _source = new TestClassPublicCtr(200);
+ var _destination = new TestClassProtectedCtrPrivateProperty(400, "Me");
+ var _result = _source.Adapt(_destination);
+
+ _destination.ShouldBeOfType();
+ _destination.X.ShouldBe(200);
+ _destination.Name.ShouldBe("Me");
+ object.ReferenceEquals(_destination, _result).ShouldBeTrue();
+ }
+
+ ///
+ /// https://github.com/MapsterMapper/Mapster/issues/427
+ ///
+ [TestMethod]
+ public void UpdateNullable()
+ {
+ var _source = new UserAccount("123", "123@gmail.com", new DateTime(2023, 9, 24));
+ var _update = new UpdateUser
+ {
+ Id = "123",
+ };
+ var configDate = new TypeAdapterConfig();
+
+ configDate.ForType()
+ .Map(dest => dest.Modified, src => new DateTime(2025, 9, 24))
+ .IgnoreNullValues(true);
+
+ _update.Adapt(_source, configDate);
+
+ var _sourceEmailUpdate = new UserAccount("123", "123@gmail.com", new DateTime(2023, 9, 24));
+ var _updateEmail = new UpdateUser
+ {
+ Email = "245@gmail.com",
+ };
+
+ var config = new TypeAdapterConfig();
+ config.ForType()
+ .IgnoreNullValues(true);
+
+ var _resultEmail = _updateEmail.Adapt(_sourceEmailUpdate, config);
+
+ _source.Id.ShouldBe("123");
+ _source.Created.ShouldBe(new DateTime(2023, 9, 24));
+ _source.Modified.ShouldBe(new DateTime(2025, 9, 24));
+ _source.Email.ShouldBe("123@gmail.com");
+ _sourceEmailUpdate.Id.ShouldBe("123");
+ _sourceEmailUpdate.Created.ShouldBe(new DateTime(2023, 9, 24));
+ _sourceEmailUpdate.Modified.ShouldBe(null);
+ _sourceEmailUpdate.Email.ShouldBe("245@gmail.com");
+
+ }
+
+ ///
+ /// https://github.com/MapsterMapper/Mapster/issues/524
+ ///
+ [TestMethod]
+ public void TSousreIsObjectUpdateUseDynamicCast()
+ {
+ var source = new TestClassPublicCtr { X = 123 };
+ var _result = SomemapWithDynamic(source);
+
+ _result.X.ShouldBe(123);
+ }
+
+ TestClassPublicCtr SomemapWithDynamic(object source)
+ {
+ var dest = new TestClassPublicCtr { X = 321 };
+ var dest1 = source.Adapt(dest,source.GetType(),dest.GetType());
+
+ return dest;
+ }
+
+ ///
+ /// https://github.com/MapsterMapper/Mapster/issues/569
+ ///
+ [TestMethod]
+ public void ImplicitOperatorCurrentWorkFromClass()
+ {
+ var guid = Guid.NewGuid();
+ var pocoWithGuid1 = new PocoWithGuid { Id = guid };
+ var pocoWithId2 = new PocoWithId { Id = new Id(guid) };
+
+ var pocoWithId1 = pocoWithGuid1.Adapt();
+ var pocoWithGuid2 = pocoWithId2.Adapt();
+
+ pocoWithId1.Id.ToString().Equals(guid.ToString()).ShouldBeTrue();
+ pocoWithGuid2.Id.Equals(guid).ShouldBeTrue();
+
+ var _result = pocoWithId1.Adapt(pocoWithGuid2);
+
+ _result.Id.ToString().Equals(guid.ToString()).ShouldBeTrue(); // Guid value transmitted
+ object.ReferenceEquals(_result, pocoWithGuid2).ShouldBeTrue(); // Not created new instanse from class pocoWithGuid2
+ _result.ShouldBeOfType();
+
+ }
+
+ [TestMethod]
+ public void DetectFakeRecord()
+ {
+ var _source = new TestClassPublicCtr(200);
+ var _destination = new FakeRecord { X = 300 };
+ var _result = _source.Adapt(_destination);
+ _destination.X.ShouldBe(200);
+ object.ReferenceEquals(_destination, _result).ShouldBeTrue();
+ }
+
+ #region NowNotWorking
+
+ ///
+ /// https://github.com/MapsterMapper/Mapster/issues/430
+ ///
+ [Ignore]
+ [TestMethod]
+ public void CollectionUpdate()
+ {
+ List sources = new()
+ {
+ new(541),
+ new(234)
+ };
+ var destination = new List();
+ var _result = sources.Adapt(destination);
+
+ destination.Count.ShouldBe(_result.Count);
+ }
+
+ ///
+ /// https://github.com/MapsterMapper/Mapster/issues/524
+ /// Not work. Already has a special overload:
+ /// .Adapt(this object source, object destination, Type sourceType, Type destinationType)
+ ///
+ [Ignore]
+ [TestMethod]
+ public void TSousreIsObjectUpdate()
+ {
+ var source = new TestClassPublicCtr { X = 123 };
+ var _result = Somemap(source);
+
+ _result.X.ShouldBe(123);
+ }
+
+ TestClassPublicCtr Somemap(object source)
+ {
+ var dest = new TestClassPublicCtr { X = 321 };
+ var dest1 = source.Adapt(dest); // typeof(TSource) always return Type as Object. Need use dynamic or Cast to Runtime Type before Adapt
+
+ return dest;
+ }
+
+ #endregion NowNotWorking
+
+ }
+
+
+ #region TestClasses
+
+ class PocoWithGuid
+ {
+ public Guid Id { get; init; }
+ }
+
+ class PocoWithId
+ {
+ public Id Id { get; init; }
+ }
+
+ class Id
+ {
+ private readonly Guid _guid;
+ public Id(Guid id) => _guid = id;
+
+ public static implicit operator Id(Guid value) => new(value);
+ public static implicit operator Guid(Id value) => value._guid;
+
+ public override string ToString() => _guid.ToString();
+ }
+
+ public class FakeRecord
+ {
+ protected FakeRecord(FakeRecord fake) { }
+ public FakeRecord() { }
+
+ public int X { get; set; }
+ }
+
+ class UserAccount
+ {
+ public UserAccount(string id, string email, DateTime created)
+ {
+ Id = id;
+ Email = email;
+ Created = created;
+ }
+ protected UserAccount() { }
+
+ public string Id { get; set; }
+ public string? Email { get; set; }
+ public DateTime Created { get; set; }
+ public DateTime? Modified { get; set; }
+ }
+
+ class UpdateUser
+ {
+ public string? Id { get; set; }
+ public string? Email { get; set; }
+ public DateTime? Created { get; set; }
+ public DateTime? Modified { get; set; }
+ }
+
+ class DestinationWithStruct
+ {
+ public TestStruct TestStruct { get; set; }
+ }
+
+ class SourceWithClass
+ {
+ public SourceWithStruct SourceWithStruct { get; set; }
+ }
+
+ class SourceWithStruct
+ {
+ public TestStruct TestStruct { get; set; }
+ }
+
+ struct TestStruct
+ {
+ public string Property { get; }
+ public TestStruct(string property) : this()
+ {
+ Property = property;
+ }
+ }
+
+ class TestClassPublicCtr
+ {
+ public TestClassPublicCtr() { }
+
+ public TestClassPublicCtr(int x)
+ {
+ X = x;
+ }
+
+ public int X { get; set; }
+ }
+
+ class TestClassProtectedCtr
+ {
+ protected TestClassProtectedCtr() { }
+
+ public TestClassProtectedCtr(int x)
+ {
+ X = x;
+ }
+
+ public int X { get; set; }
+ }
+
+ class TestClassProtectedCtrPrivateProperty
+ {
+ protected TestClassProtectedCtrPrivateProperty() { }
+
+ public TestClassProtectedCtrPrivateProperty(int x, string name)
+ {
+ X = x;
+ Name = name;
+ }
+
+ public int X { get; private set; }
+
+ public string Name { get; private set; }
+ }
+
+ record TestRecord()
+ {
+ public int X { set; get; }
+ }
+
+ record TestRecordPositional(int X);
+
+ record struct TestRecordStruct
+ {
+ public int X { set; get; }
+ }
+
+ ///
+ /// Different Checked Constructor Attribute From Spec
+ /// https://learn.microsoft.com/ru-ru/dotnet/csharp/language-reference/proposals/csharp-9.0/records#copy-and-clone-members
+ ///
+ sealed record TestSealedRecord()
+ {
+ public int X { get; set; }
+ }
+
+ sealed record TestSealedRecordPositional(int X);
+
+ #endregion TestClasses
+}
diff --git a/src/Mapster/Adapters/ClassAdapter.cs b/src/Mapster/Adapters/ClassAdapter.cs
index cc803d8d..49027a49 100644
--- a/src/Mapster/Adapters/ClassAdapter.cs
+++ b/src/Mapster/Adapters/ClassAdapter.cs
@@ -54,7 +54,28 @@ protected override Expression CreateInstantiationExpression(Expression source, E
{
//new TDestination(src.Prop1, src.Prop2)
- if (arg.GetConstructUsing() != null || arg.Settings.MapToConstructor == null)
+ ///
+ bool IsEnableNonPublicMembersAndNotPublicCtorWithoutParams(CompileArgument arg)
+ {
+ if (arg.Settings.EnableNonPublicMembers == null)
+ return false;
+ if (arg.Settings.EnableNonPublicMembers == false)
+ return false;
+ else
+ {
+ if (arg.DestinationType.GetConstructors().Any(x => x.GetParameters() != null))
+ {
+ return true;
+ }
+ }
+
+
+ return false;
+ }
+
+
+
+ if ((arg.GetConstructUsing() != null || arg.Settings.MapToConstructor == null) && !IsEnableNonPublicMembersAndNotPublicCtorWithoutParams(arg))
return base.CreateInstantiationExpression(source, destination, arg);
ClassMapping? classConverter;
diff --git a/src/Mapster/Adapters/RecordTypeAdapter.cs b/src/Mapster/Adapters/RecordTypeAdapter.cs
index e5812e16..a0034829 100644
--- a/src/Mapster/Adapters/RecordTypeAdapter.cs
+++ b/src/Mapster/Adapters/RecordTypeAdapter.cs
@@ -4,7 +4,7 @@
namespace Mapster.Adapters
{
- internal class RecordTypeAdapter : BaseClassAdapter
+ internal class RecordTypeAdapter : ClassAdapter
{
protected override int Score => -149;
protected override bool UseTargetValue => false;
@@ -34,12 +34,12 @@ protected override Expression CreateInstantiationExpression(Expression source, E
protected override Expression CreateBlockExpression(Expression source, Expression destination, CompileArgument arg)
{
- return Expression.Empty();
+ return base.CreateBlockExpression(source, destination, arg);
}
protected override Expression CreateInlineExpression(Expression source, CompileArgument arg)
{
- return CreateInstantiationExpression(source, arg);
+ return base.CreateInstantiationExpression(source, arg);
}
}
diff --git a/src/Mapster/Utils/ReflectionUtils.cs b/src/Mapster/Utils/ReflectionUtils.cs
index 5959daa5..fe44e790 100644
--- a/src/Mapster/Utils/ReflectionUtils.cs
+++ b/src/Mapster/Utils/ReflectionUtils.cs
@@ -170,27 +170,46 @@ public static bool IsRecordType(this Type type)
var props = type.GetFieldsAndProperties().ToList();
+
+ #region SupportingСurrentBehavior for Config Clone and Fork
+
+ if (type == typeof(MulticastDelegate))
+ return true;
+
+ if (type == typeof(TypeAdapterSetter))
+ return true;
+
+ // if (type == typeof(TypeAdapterRule))
+ // return true;
+
+ if (type == typeof(TypeAdapterSettings))
+ return true;
+
+ if (type.IsValueType && type?.GetConstructors().Length != 0)
+ {
+ var test = type.GetConstructors()[0].GetParameters();
+ var param = type.GetConstructors()[0].GetParameters().ToArray();
+
+ if (param[0]?.ParameterType == typeof(TypeTuple) && param[1]?.ParameterType == typeof(TypeAdapterRule))
+ return true;
+ }
+
+ if (type == typeof(TypeTuple))
+ return true;
+
+ #endregion SupportingСurrentBehavior for Config Clone and Fork
+
+
//interface with readonly props
- if (type.GetTypeInfo().IsInterface &&
+ if (type.GetTypeInfo().IsInterface &&
props.Any(p => p.SetterModifier != AccessModifier.Public))
return true;
- //1 constructor
- var ctors = type.GetConstructors().ToList();
- if (ctors.Count != 1)
- return false;
+ if(RecordTypeIdentityHelper.IsRecordType(type))
+ return true;
- //ctor must not empty
- var ctorParams = ctors[0].GetParameters();
- if (ctorParams.Length == 0)
- return false;
- //all parameters should match getter
- return props.All(prop =>
- {
- var name = prop.Name.ToPascalCase();
- return ctorParams.Any(p => p.ParameterType == prop.Type && p.Name?.ToPascalCase() == name);
- });
+ return false;
}
public static bool IsConvertible(this Type type)