diff --git a/src/Mapster.Core/Attributes/AdaptWithAttribute.cs b/src/Mapster.Core/Attributes/AdaptWithAttribute.cs new file mode 100644 index 00000000..e12a4aea --- /dev/null +++ b/src/Mapster.Core/Attributes/AdaptWithAttribute.cs @@ -0,0 +1,17 @@ +using System; + +namespace Mapster +{ + [AttributeUsage(AttributeTargets.Class + | AttributeTargets.Struct + | AttributeTargets.Property + | AttributeTargets.Field, AllowMultiple = true)] + public class AdaptWithAttribute : Attribute + { + public AdaptDirectives AdaptDirective { get; set; } + public AdaptWithAttribute(AdaptDirectives directive) + { + AdaptDirective = directive; + } + } +} diff --git a/src/Mapster.Core/Enums/AdaptDirectives.cs b/src/Mapster.Core/Enums/AdaptDirectives.cs new file mode 100644 index 00000000..a273b1d6 --- /dev/null +++ b/src/Mapster.Core/Enums/AdaptDirectives.cs @@ -0,0 +1,8 @@ +namespace Mapster +{ + public enum AdaptDirectives + { + None = 0, + DestinationAsRecord = 1 + } +} diff --git a/src/Mapster.Core/Utils/RecordTypeIdentityHelper.cs b/src/Mapster.Core/Utils/RecordTypeIdentityHelper.cs index 4e5aa197..b65d3742 100644 --- a/src/Mapster.Core/Utils/RecordTypeIdentityHelper.cs +++ b/src/Mapster.Core/Utils/RecordTypeIdentityHelper.cs @@ -17,10 +17,11 @@ public static class RecordTypeIdentityHelper if (ctors.Count < 2) return false; - var isRecordTypeCtor = type.GetConstructors(BindingFlags.Instance | BindingFlags.NonPublic) + var isRecordTypeCtor = + ctors .Where(x => x.IsFamily == true || (type.IsSealed && x.IsPrivate == true)) // add target from Sealed record .Any(x => x.GetParameters() - .Any(y => y.ParameterType == type)); + .Any(y => y.ParameterType == type)); if (isRecordTypeCtor) return true; @@ -43,5 +44,17 @@ public static bool IsRecordType(Type type) return false; } + + public static bool IsDirectiveTagret(Type type) + { + var arrt = type.GetCustomAttributes()?.FirstOrDefault()?.AdaptDirective; + + if (arrt == null) + return false; + if (arrt == AdaptDirectives.DestinationAsRecord) + return true; + + return false; + } } } diff --git a/src/Mapster.Tests/WhenIgnoringConditionally.cs b/src/Mapster.Tests/WhenIgnoringConditionally.cs index d7377a37..471760b8 100644 --- a/src/Mapster.Tests/WhenIgnoringConditionally.cs +++ b/src/Mapster.Tests/WhenIgnoringConditionally.cs @@ -160,7 +160,6 @@ 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(); @@ -188,7 +187,8 @@ public class SimpleDto public string Name { get; set; } } - public class SimpleRecord // or Replace on record + [AdaptWith(AdaptDirectives.DestinationAsRecord)] + public class SimpleRecord { public int Id { get; } public string Name { get; } diff --git a/src/Mapster.Tests/WhenMappingPrivateFieldsAndProperties.cs b/src/Mapster.Tests/WhenMappingPrivateFieldsAndProperties.cs index c51ce675..c87dcb07 100644 --- a/src/Mapster.Tests/WhenMappingPrivateFieldsAndProperties.cs +++ b/src/Mapster.Tests/WhenMappingPrivateFieldsAndProperties.cs @@ -61,6 +61,7 @@ public void Should_Map_Private_Field_To_New_Object_Correctly() dto.Name.ShouldBe(customerName); } + [TestMethod] public void Should_Map_Private_Property_To_New_Object_Correctly() { @@ -78,21 +79,20 @@ public void Should_Map_Private_Property_To_New_Object_Correctly() } [TestMethod] - public void Should_Map_To_Private_Fields_Correctly() + public void Should_Map_To_Private_Fields_Correctly() { - SetUpMappingNonPublicFields(); - - var dto = new CustomerDTO + SetUpMappingNonPublicFields(); + + var dto = new CustomerDTOWithPrivateGet { Id = 1, Name = "Customer 1" }; - var customer = dto.Adapt(); + var customer = dto.Adapt(); - Assert.IsNotNull(customer); - Assert.IsTrue(customer.HasId(dto.Id)); - customer.Name.ShouldBe(dto.Name); + customer.HasId().ShouldBe(1); + customer.Name.ShouldBe("Customer 1"); } [TestMethod] @@ -108,9 +108,8 @@ public void Should_Map_To_Private_Properties_Correctly() var customer = dto.Adapt(); - Assert.IsNotNull(customer); - customer.Id.ShouldBe(dto.Id); - Assert.IsTrue(customer.HasName(dto.Name)); + customer.Id.ShouldBe(1); + customer.HasName().ShouldBe("Customer 1"); } [TestMethod] @@ -167,10 +166,10 @@ private static void SetUpMappingNonPublicProperties() public class CustomerWithPrivateField { - private readonly int _id; + private int _id; public string Name { get; private set; } - private CustomerWithPrivateField() { } + public CustomerWithPrivateField() { } public CustomerWithPrivateField(int id, string name) { @@ -178,28 +177,28 @@ public CustomerWithPrivateField(int id, string name) Name = name; } - public bool HasId(int id) + public int HasId() { - return _id == id; + return _id; } } - public class CustomerWithPrivateProperty + public class CustomerWithPrivateProperty { public int Id { get; private set; } private string Name { get; set; } - private CustomerWithPrivateProperty() { } + public CustomerWithPrivateProperty() { } - public CustomerWithPrivateProperty(int id, string name) + public CustomerWithPrivateProperty(int id, string name) { Id = id; Name = name; } - public bool HasName(string name) + public string HasName() { - return Name == name; + return Name; } } @@ -228,6 +227,12 @@ public class CustomerDTO public string Name { get; set; } } + public class CustomerDTOWithPrivateGet + { + public int Id { private get; set; } + public string Name { private get; set; } + } + public class Pet { public string Name { get; set; } diff --git a/src/Mapster.Tests/WhenMappingRecordRegression.cs b/src/Mapster.Tests/WhenMappingRecordRegression.cs index f5ef6add..f68cd6f9 100644 --- a/src/Mapster.Tests/WhenMappingRecordRegression.cs +++ b/src/Mapster.Tests/WhenMappingRecordRegression.cs @@ -41,7 +41,7 @@ public void AdaptRecordStructToRecordStruct() var _structResult = _sourceStruct.Adapt(_destinationStruct); _structResult.X.ShouldBe(1000); - object.ReferenceEquals(_destinationStruct, _structResult).ShouldBeFalse(); + _destinationStruct.X.Equals(_structResult.X).ShouldBeFalse(); } [TestMethod] @@ -194,26 +194,6 @@ public void UpdateNullable() } - /// - /// 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 /// @@ -247,6 +227,56 @@ public void DetectFakeRecord() _destination.X.ShouldBe(200); object.ReferenceEquals(_destination, _result).ShouldBeTrue(); } + + [TestMethod] + public void OnlyInlineRecordWorked() + { + var _sourcePoco = new InlinePoco501() { MyInt = 1 , MyString = "Hello" }; + var _sourceOnlyInitRecord = new OnlyInitRecord501 { MyInt = 2, MyString = "Hello World" }; + + var _resultOnlyinitRecord = _sourcePoco.Adapt(); + var _updateResult = _sourceOnlyInitRecord.Adapt(_resultOnlyinitRecord); + + _resultOnlyinitRecord.MyInt.ShouldBe(1); + _resultOnlyinitRecord.MyString.ShouldBe("Hello"); + _updateResult.MyInt.ShouldBe(2); + _updateResult.MyString.ShouldBe("Hello World"); + } + + [TestMethod] + public void MultyCtorRecordWorked() + { + var _sourcePoco = new InlinePoco501() { MyInt = 1, MyString = "Hello" }; + var _sourceMultyCtorRecord = new MultiCtorRecord (2, "Hello World"); + + var _resultMultyCtorRecord = _sourcePoco.Adapt(); + var _updateResult = _sourceMultyCtorRecord.Adapt(_resultMultyCtorRecord); + + _resultMultyCtorRecord.MyInt.ShouldBe(1); + _resultMultyCtorRecord.MyString.ShouldBe("Hello"); + _updateResult.MyInt.ShouldBe(2); + _updateResult.MyString.ShouldBe("Hello World"); + } + + [TestMethod] + public void MultiCtorAndInlineRecordWorked() + { + var _sourcePoco = new MultiCtorAndInlinePoco() { MyInt = 1, MyString = "Hello", MyEmail = "123@gmail.com", InitData="Test"}; + var _sourceMultiCtorAndInline = new MultiCtorAndInlineRecord(2, "Hello World") { InitData = "Worked", MyEmail = "243@gmail.com" }; + + var _resultMultiCtorAndInline = _sourcePoco.Adapt(); + var _updateResult = _sourceMultiCtorAndInline.Adapt(_resultMultiCtorAndInline); + + _resultMultiCtorAndInline.MyInt.ShouldBe(1); + _resultMultiCtorAndInline.MyString.ShouldBe("Hello"); + _resultMultiCtorAndInline.MyEmail.ShouldBe("123@gmail.com"); + _resultMultiCtorAndInline.InitData.ShouldBe("Test"); + _updateResult.MyInt.ShouldBe(2); + _updateResult.MyString.ShouldBe("Hello World"); + _updateResult.MyEmail.ShouldBe("243@gmail.com"); + _updateResult.InitData.ShouldBe("Worked"); + } + #region NowNotWorking @@ -268,35 +298,67 @@ public void CollectionUpdate() 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); + #endregion NowNotWorking + + } + - _result.X.ShouldBe(123); + #region TestClasses + + class MultiCtorAndInlinePoco + { + public int MyInt { get; set; } + public string MyString { get; set; } + public string MyEmail { get; set; } + public string InitData { get; set; } + } + + record MultiCtorAndInlineRecord + { + public MultiCtorAndInlineRecord(int myInt) + { + MyInt = myInt; } - TestClassPublicCtr Somemap(object source) + public MultiCtorAndInlineRecord(int myInt, string myString) : this(myInt) { - 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 + MyString = myString; + } + + + public int MyInt { get; private set; } + public string MyString { get; private set; } + public string MyEmail { get; set; } + public string InitData { get; init; } + } - return dest; + record MultiCtorRecord + { + public MultiCtorRecord(int myInt) + { + MyInt = myInt; } - #endregion NowNotWorking + public MultiCtorRecord(int myInt, string myString) : this(myInt) + { + MyString = myString; + } + public int MyInt { get; private set; } + public string MyString { get; private set; } } + class InlinePoco501 + { + public int MyInt { get; set; } + public string MyString { get; set; } + } - #region TestClasses + record OnlyInitRecord501 + { + public int MyInt { get; init; } + public string MyString { get; init; } + } class PocoWithGuid { diff --git a/src/Mapster/Adapters/ClassAdapter.cs b/src/Mapster/Adapters/ClassAdapter.cs index 49027a49..c190a7a4 100644 --- a/src/Mapster/Adapters/ClassAdapter.cs +++ b/src/Mapster/Adapters/ClassAdapter.cs @@ -53,29 +53,8 @@ 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) - - /// - 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)) + + if (arg.GetConstructUsing() != null || arg.Settings.MapToConstructor == null) return base.CreateInstantiationExpression(source, destination, arg); ClassMapping? classConverter; diff --git a/src/Mapster/Adapters/ReadOnlyInterfaceAdapter.cs b/src/Mapster/Adapters/ReadOnlyInterfaceAdapter.cs new file mode 100644 index 00000000..df4a0f66 --- /dev/null +++ b/src/Mapster/Adapters/ReadOnlyInterfaceAdapter.cs @@ -0,0 +1,49 @@ +using Mapster.Utils; +using System.Linq; +using System.Linq.Expressions; + +namespace Mapster.Adapters +{ + internal class ReadOnlyInterfaceAdapter : ClassAdapter + { + protected override int Score => -148; + + protected override bool CanMap(PreCompileArgument arg) + { + return arg.DestinationType.IsInterface; + } + + protected override bool CanInline(Expression source, Expression? destination, CompileArgument arg) + { + if (base.CanInline(source, destination, arg)) + return true; + else + return false; + + } + + protected override Expression CreateInstantiationExpression(Expression source, Expression? destination, CompileArgument arg) + { + var destintionType = arg.DestinationType; + var props = destintionType.GetFieldsAndProperties().ToList(); + + //interface with readonly props + if (props.Any(p => p.SetterModifier != AccessModifier.Public)) + { + if (arg.GetConstructUsing() != null) + return base.CreateInstantiationExpression(source, destination, arg); + + var destType = DynamicTypeGenerator.GetTypeForInterface(arg.DestinationType, arg.Settings.Includes.Count > 0); + if (destType == null) + return base.CreateInstantiationExpression(source, destination, arg); + var ctor = destType.GetConstructors()[0]; + var classModel = GetConstructorModel(ctor, false); + var classConverter = CreateClassConverter(source, classModel, arg); + return CreateInstantiationExpression(source, classConverter, arg); + } + else + return base.CreateInstantiationExpression(source,destination, arg); + } + + } +} diff --git a/src/Mapster/Adapters/RecordTypeAdapter.cs b/src/Mapster/Adapters/RecordTypeAdapter.cs index a0034829..009af932 100644 --- a/src/Mapster/Adapters/RecordTypeAdapter.cs +++ b/src/Mapster/Adapters/RecordTypeAdapter.cs @@ -1,4 +1,6 @@ -using System.Linq.Expressions; +using System.Collections.Generic; +using System.Linq; +using System.Linq.Expressions; using System.Reflection; using Mapster.Utils; @@ -26,21 +28,73 @@ protected override Expression CreateInstantiationExpression(Expression source, E : arg.DestinationType; if (destType == null) return base.CreateInstantiationExpression(source, destination, arg); - var ctor = destType.GetConstructors()[0]; + var ctor = destType.GetConstructors() + .OrderByDescending(it => it.GetParameters().Length).ToArray().FirstOrDefault(); // Will be used public constructor with the maximum number of parameters var classModel = GetConstructorModel(ctor, false); var classConverter = CreateClassConverter(source, classModel, arg); - return CreateInstantiationExpression(source, classConverter, arg); + var installExpr = CreateInstantiationExpression(source, classConverter, arg); + return RecordInlineExpression(source, arg, installExpr); // Activator field when not include in public ctor } protected override Expression CreateBlockExpression(Expression source, Expression destination, CompileArgument arg) { - return base.CreateBlockExpression(source, destination, arg); + return Expression.Empty(); } protected override Expression CreateInlineExpression(Expression source, CompileArgument arg) { return base.CreateInstantiationExpression(source, arg); } + + private Expression? RecordInlineExpression(Expression source, CompileArgument arg, Expression installExpr) + { + //new TDestination { + // Prop1 = convert(src.Prop1), + // Prop2 = convert(src.Prop2), + //} + + var exp = installExpr; + var memberInit = exp as MemberInitExpression; + var newInstance = memberInit?.NewExpression ?? (NewExpression)exp; + var contructorMembers = newInstance.Arguments.OfType().Select(me => me.Member).ToArray(); + var classModel = GetSetterModel(arg); + var classConverter = CreateClassConverter(source, classModel, arg); + var members = classConverter.Members; + + var lines = new List(); + if (memberInit != null) + lines.AddRange(memberInit.Bindings); + foreach (var member in members) + { + if (member.UseDestinationValue) + return null; + + if (!arg.Settings.Resolvers.Any(r => r.DestinationMemberName == member.DestinationMember.Name) + && member.Getter is MemberExpression memberExp && contructorMembers.Contains(memberExp.Member)) + continue; + + if (member.DestinationMember.SetterModifier == AccessModifier.None) + continue; + + 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 + //except collection type & complex type which cannot be null + if (arg.MapType == MapType.Projection + && member.Getter.Type != member.DestinationMember.Type + && !member.Getter.Type.IsCollection() + && !member.DestinationMember.Type.IsCollection() + && member.Getter.Type.GetTypeInfo().GetCustomAttributesData().All(attr => attr.GetAttributeType().Name != "ComplexTypeAttribute")) + { + value = member.Getter.NotNullReturn(value); + } + var bind = Expression.Bind((MemberInfo)member.DestinationMember.Info!, value); + lines.Add(bind); + } + + return Expression.MemberInit(newInstance, lines); + } } } diff --git a/src/Mapster/TypeAdapterConfig.cs b/src/Mapster/TypeAdapterConfig.cs index f5ebbb3a..cb8ac881 100644 --- a/src/Mapster/TypeAdapterConfig.cs +++ b/src/Mapster/TypeAdapterConfig.cs @@ -23,6 +23,7 @@ private static List CreateRuleTemplate() new PrimitiveAdapter().CreateRule(), //-200 new ClassAdapter().CreateRule(), //-150 new RecordTypeAdapter().CreateRule(), //-149 + new ReadOnlyInterfaceAdapter().CreateRule(), // -148 new CollectionAdapter().CreateRule(), //-125 new DictionaryAdapter().CreateRule(), //-124 new ArrayAdapter().CreateRule(), //-123 diff --git a/src/Mapster/TypeAdapterSetter.cs b/src/Mapster/TypeAdapterSetter.cs index 9215f375..192df8aa 100644 --- a/src/Mapster/TypeAdapterSetter.cs +++ b/src/Mapster/TypeAdapterSetter.cs @@ -8,6 +8,7 @@ namespace Mapster { + [AdaptWith(AdaptDirectives.DestinationAsRecord)] public class TypeAdapterSetter { protected const string SourceParameterName = "source"; diff --git a/src/Mapster/TypeAdapterSettings.cs b/src/Mapster/TypeAdapterSettings.cs index 7af0f52b..e2ecde08 100644 --- a/src/Mapster/TypeAdapterSettings.cs +++ b/src/Mapster/TypeAdapterSettings.cs @@ -6,6 +6,7 @@ namespace Mapster { + [AdaptWith(AdaptDirectives.DestinationAsRecord)] public class TypeAdapterSettings : SettingStore { public IgnoreDictionary Ignore diff --git a/src/Mapster/Utils/ReflectionUtils.cs b/src/Mapster/Utils/ReflectionUtils.cs index fe44e790..d84f3dfa 100644 --- a/src/Mapster/Utils/ReflectionUtils.cs +++ b/src/Mapster/Utils/ReflectionUtils.cs @@ -1,11 +1,11 @@ -using System; +using Mapster.Models; +using Mapster.Utils; +using System; using System.Collections; using System.Collections.Generic; using System.Linq; using System.Linq.Expressions; using System.Reflection; -using Mapster.Models; -using Mapster.Utils; // ReSharper disable once CheckNamespace namespace Mapster @@ -168,47 +168,19 @@ public static bool IsRecordType(this Type type) if (type.IsConvertible()) return false; - 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)) + if(RecordTypeIdentityHelper.IsDirectiveTagret(type)) // added Support work from custom Attribute return true; + + #region SupportingСurrentBehavior for Config Clone and Fork - 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)) + if (type.IsGenericType && type.GetGenericTypeDefinition() == typeof(KeyValuePair<,>)) return true; #endregion SupportingСurrentBehavior for Config Clone and Fork - - //interface with readonly props - if (type.GetTypeInfo().IsInterface && - props.Any(p => p.SetterModifier != AccessModifier.Public)) - return true; - if(RecordTypeIdentityHelper.IsRecordType(type)) return true; - return false; }