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)