diff --git a/src/Mapster.Tests/WhenMappingMemberNameContainingPeriod.cs b/src/Mapster.Tests/WhenMappingMemberNameContainingPeriod.cs new file mode 100644 index 00000000..5ca642fc --- /dev/null +++ b/src/Mapster.Tests/WhenMappingMemberNameContainingPeriod.cs @@ -0,0 +1,298 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Linq.Expressions; +using System.Reflection; +using System.Reflection.Emit; +using System.Runtime.CompilerServices; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Shouldly; + +namespace Mapster.Tests; + +[TestClass] +public class WhenMappingMemberNameContainingPeriod +{ + private const string MemberName = "Some.Property.With.Periods"; + + [TestMethod] + public void Property_Name_Containing_Periods_Is_Supported() + { + // Create a target type with a property that contains periods + Type targetType = new TestTypeBuilder() + .AddProperty(MemberName) + .CreateType(); + + // Call the local function defined below, the actual test method + CallStaticLocalTestMethod( + nameof(Test), + new Type[] { targetType }); + + // The actual test method adapting Source to the target type and back to the source to verify mapping the property with periods + static void Test() + { + // Get expression for mapping the property with periods + Expression> getPropertyExpression = BuildGetPropertyExpression(MemberName); + + // Create the config + TypeAdapterConfig + .NewConfig() + .TwoWays() + .Map(getPropertyExpression, src => src.Value); + + // Execute the mapping both ways + Source source = new() { Value = 551 }; + TTarget target = source.Adapt(); + Source adaptedSource = target.Adapt(); + + Assert.AreEqual(source.Value, adaptedSource.Value); + } + } + + [TestMethod] + public void Constructor_Parameter_Name_Containing_Periods_Is_Supported() + { + // Create a target type with a property that contains periods + Type targetTypeWithProperty = new TestTypeBuilder() + .AddProperty(MemberName) + .CreateType(); + + // Create a target type with a constructor parameter that contains periods + Type targetTypeWithConstructor = new TestTypeBuilder() + .AddConstructorWithReadOnlyProperty(MemberName) + .CreateType(); + + // Call the local function defined below, the actual test method + CallStaticLocalTestMethod( + nameof(Test), + new Type[] { targetTypeWithProperty, targetTypeWithConstructor }); + + // The actual test method + static void Test() + where TWithProperty : new() + { + // Create the config + TypeAdapterConfig + .NewConfig() + .TwoWays() + .MapToConstructor(true); + + // Create delegate for setting the property value on TWithProperty + Expression> setPropertyExpression = BuildSetPropertyExpression(MemberName); + Action setProperty = setPropertyExpression.Compile(); + + // Create the source object + int value = 551; + TWithProperty source = new(); + setProperty.Invoke(source, value); + + // Map + TWithConstructor target = source.Adapt(); + TWithProperty adaptedSource = target.Adapt(); + + // Create delegate for getting the property from TWithProperty + Expression> getPropertyExpression = BuildGetPropertyExpression(MemberName); + Func getProperty = getPropertyExpression.Compile(); + + // Verify + Assert.AreEqual(value, getProperty.Invoke(adaptedSource)); + } + } + + [TestMethod] + public void Using_Property_Path_String_Is_Supported() + { + // Create a target type with a property that contains periods + Type targetType = new TestTypeBuilder() + .AddProperty(MemberName) + .CreateType(); + + // Create the config, both ways + TypeAdapterConfig + .GlobalSettings + .NewConfig(typeof(Source), targetType) + .Map(MemberName, nameof(Source.Value)); + TypeAdapterConfig + .GlobalSettings + .NewConfig(targetType, typeof(Source)) + .Map(nameof(Source.Value), MemberName); + + // Execute the mapping both ways + Source source = new() { Value = 551 }; + object target = source.Adapt(typeof(Source), targetType); + Source adaptedSource = target.Adapt(); + + Assert.AreEqual(source.Value, adaptedSource.Value); + } + + [TestMethod] + public void Object_To_Dictionary_Map() + { + var poco = new SimplePoco + { + Id = Guid.NewGuid(), + Name = "test", + }; + + var config = new TypeAdapterConfig(); + config.NewConfig>() + .Map(MemberName, c => c.Id); + var dict = poco.Adapt>(config); + + dict.Count.ShouldBe(2); + dict[MemberName].ShouldBe(poco.Id); + dict["Name"].ShouldBe(poco.Name); + } + + private static void CallStaticLocalTestMethod(string methodName, Type[] genericArguments, [CallerMemberName] string caller = "Unknown") + { + MethodInfo genericMethodInfo = typeof(WhenMappingMemberNameContainingPeriod) + .GetMethods(BindingFlags.NonPublic | BindingFlags.Static) + .Single(x => x.Name.Contains($"<{caller}>") && x.Name.Contains(methodName)); + + MethodInfo method = genericMethodInfo.MakeGenericMethod(genericArguments); + + method.Invoke(null, null); + } + + private static Expression> BuildGetPropertyExpression(string propertyName) + { + ParameterExpression param = Expression.Parameter(typeof(T), "x"); + MemberExpression property = Expression.Property(param, propertyName); + return Expression.Lambda>(property, param); + } + + private static Expression> BuildSetPropertyExpression(string propertyName) + { + ParameterExpression param = Expression.Parameter(typeof(T), "x"); + ParameterExpression value = Expression.Parameter(typeof(TProperty), "value"); + MemberExpression property = Expression.Property(param, propertyName); + BinaryExpression assign = Expression.Assign(property, value); + return Expression.Lambda>(assign, param, value); + } + + private class Source + { + public int Value { get; set; } + } + + private class TestTypeBuilder + { + private readonly TypeBuilder _typeBuilder; + + public TestTypeBuilder() + { + AssemblyBuilder assemblyBuilder = AssemblyBuilder.DefineDynamicAssembly( + new AssemblyName("Types"), + AssemblyBuilderAccess.Run); + ModuleBuilder moduleBuilder = assemblyBuilder.DefineDynamicModule(""); + _typeBuilder = moduleBuilder.DefineType( + "Types.Target", + TypeAttributes.Public | + TypeAttributes.Class | + TypeAttributes.Sealed | + TypeAttributes.AutoClass | + TypeAttributes.AnsiClass | + TypeAttributes.BeforeFieldInit | + TypeAttributes.AutoLayout, + null); + } + + public TestTypeBuilder AddConstructorWithReadOnlyProperty(string parameterName) + { + // Add read-only property + FieldBuilder fieldBuilder = AddProperty(parameterName, false); + + // Build the constructor with the parameter for the property + ConstructorBuilder constructorBuilder = _typeBuilder.DefineConstructor( + MethodAttributes.Public, + CallingConventions.Standard, + new Type[] { typeof(TParameter) }); + + // Define the parameter name + constructorBuilder.DefineParameter(1, ParameterAttributes.None, MemberName); + + ILGenerator constructorIL = constructorBuilder.GetILGenerator(); + + // Call the base class constructor + constructorIL.Emit(OpCodes.Ldarg_0); + constructorIL.Emit(OpCodes.Call, typeof(object).GetConstructor(Type.EmptyTypes)); + + // Set the property value + constructorIL.Emit(OpCodes.Ldarg_0); + constructorIL.Emit(OpCodes.Ldarg_1); + constructorIL.Emit(OpCodes.Stfld, fieldBuilder); + + constructorIL.Emit(OpCodes.Ret); + + return this; + } + + public TestTypeBuilder AddProperty(string propertyName) + { + AddProperty(propertyName, true); + return this; + } + + private FieldBuilder AddProperty(string propertyName, bool addSetter) + { + Type propertyType = typeof(T); + FieldBuilder fieldBuilder = _typeBuilder.DefineField($"_{propertyName}", propertyType, FieldAttributes.Private); + PropertyBuilder propertyBuilder = _typeBuilder.DefineProperty(propertyName, PropertyAttributes.None, propertyType, null); + + AddGetMethod(_typeBuilder, propertyBuilder, fieldBuilder, propertyName, propertyType); + if (addSetter) + { + AddSetMethod(_typeBuilder, propertyBuilder, fieldBuilder, propertyName, propertyType); + } + + return fieldBuilder; + } + + public Type CreateType() => _typeBuilder.CreateType(); + + private static PropertyBuilder AddGetMethod(TypeBuilder typeBuilder, PropertyBuilder propertyBuilder, FieldBuilder fieldBuilder, string propertyName, Type propertyType) + { + MethodBuilder getMethodBuilder = typeBuilder.DefineMethod( + "get_" + propertyName, + MethodAttributes.Public | MethodAttributes.SpecialName | MethodAttributes.HideBySig, + propertyType, + Type.EmptyTypes); + ILGenerator getMethodGenerator = getMethodBuilder.GetILGenerator(); + + getMethodGenerator.Emit(OpCodes.Ldarg_0); + getMethodGenerator.Emit(OpCodes.Ldfld, fieldBuilder); + getMethodGenerator.Emit(OpCodes.Ret); + + propertyBuilder.SetGetMethod(getMethodBuilder); + + return propertyBuilder; + } + + private static PropertyBuilder AddSetMethod(TypeBuilder typeBuilder, PropertyBuilder propertyBuilder, FieldBuilder fieldBuilder, string propertyName, Type propertyType) + { + MethodBuilder setMethodBuilder = typeBuilder.DefineMethod( + $"set_{propertyName}", + MethodAttributes.Public | MethodAttributes.SpecialName | MethodAttributes.HideBySig, + null, + new Type[] { propertyType }); + + ILGenerator setMethodGenerator = setMethodBuilder.GetILGenerator(); + Label modifyProperty = setMethodGenerator.DefineLabel(); + Label exitSet = setMethodGenerator.DefineLabel(); + + setMethodGenerator.MarkLabel(modifyProperty); + setMethodGenerator.Emit(OpCodes.Ldarg_0); + setMethodGenerator.Emit(OpCodes.Ldarg_1); + setMethodGenerator.Emit(OpCodes.Stfld, fieldBuilder); + + setMethodGenerator.Emit(OpCodes.Nop); + setMethodGenerator.MarkLabel(exitSet); + setMethodGenerator.Emit(OpCodes.Ret); + + propertyBuilder.SetSetMethod(setMethodBuilder); + + return propertyBuilder; + } + } +} diff --git a/src/Mapster.Tool/Program.cs b/src/Mapster.Tool/Program.cs index 1347cb13..2e8fe0b2 100644 --- a/src/Mapster.Tool/Program.cs +++ b/src/Mapster.Tool/Program.cs @@ -76,7 +76,7 @@ private static void GenerateMappers(MapperOptions opt) // This way when we compare attribute types (such as MapperAttribute) between our running assembly // and the scanned assembly the two types with the same FullName can be considered equal because // they both were resolved from AssemblyLoadContext.Default. - + // This isolated Assembly Load Context will be able to resolve the Mapster assembly, but // the resolved Assembly will be the same one that is in AssemblyLoadContext.Default // (the runtime assembly load context that our code refers to by default when referencing @@ -461,8 +461,8 @@ Dictionary settings setter.Settings.Resolvers.Add( new InvokerModel { - DestinationMemberName = setting.TargetPropertyName ?? name, - SourceMemberName = name, + DestinationMemberPath = (setting.TargetPropertyName ?? name).Split('.'), + SourceMemberPath = name.Split('.'), Invoker = setting.MapFunc, } ); diff --git a/src/Mapster/Adapters/BaseClassAdapter.cs b/src/Mapster/Adapters/BaseClassAdapter.cs index faa490ec..f23355e6 100644 --- a/src/Mapster/Adapters/BaseClassAdapter.cs +++ b/src/Mapster/Adapters/BaseClassAdapter.cs @@ -26,7 +26,7 @@ protected ClassMapping CreateClassConverter(Expression source, ClassModel classM arg.Settings.ExtraSources.Select(src => src is LambdaExpression lambda ? lambda.Apply(arg.MapType, source) - : ExpressionEx.PropertyOrFieldPath(source, (string)src))); + : ExpressionEx.PropertyOrFieldPath(source, (string[])src))); foreach (var destinationMember in destinationMembers) { if (ProcessIgnores(arg, destinationMember, out var ignore)) @@ -124,7 +124,7 @@ protected static bool ProcessIgnores( if (!destinationMember.ShouldMapMember(arg, MemberSide.Destination)) return true; - return arg.Settings.Ignore.TryGetValue(destinationMember.Name, out ignore) + return arg.Settings.Ignore.TryGetValue(new[] { destinationMember.Name }, out ignore) && ignore.Condition == null; } diff --git a/src/Mapster/Adapters/ClassAdapter.cs b/src/Mapster/Adapters/ClassAdapter.cs index 407c5a5b..0272a464 100644 --- a/src/Mapster/Adapters/ClassAdapter.cs +++ b/src/Mapster/Adapters/ClassAdapter.cs @@ -214,7 +214,9 @@ private static Expression SetValueByReflection(MemberMapping member, MemberExpre if (member.UseDestinationValue) return null; - if (!arg.Settings.Resolvers.Any(r => r.DestinationMemberName == member.DestinationMember.Name) + if (!arg.Settings.Resolvers.Any(r + => r.DestinationMemberPath.Length == 1 + && r.DestinationMemberPath[0] == member.DestinationMember.Name) && member.Getter is MemberExpression memberExp && contructorMembers.Contains(memberExp.Member)) continue; diff --git a/src/Mapster/Adapters/DictionaryAdapter.cs b/src/Mapster/Adapters/DictionaryAdapter.cs index 25af8e3b..edaf8d00 100644 --- a/src/Mapster/Adapters/DictionaryAdapter.cs +++ b/src/Mapster/Adapters/DictionaryAdapter.cs @@ -105,8 +105,9 @@ protected override Expression CreateBlockExpression(Expression source, Expressio //ignore mapped var ignores = arg.Settings.Resolvers - .Select(r => r.SourceMemberName) - .Where(name => name != null) + .Select(r => r.SourceMemberPath) + .Where(path => path != null) + .Select(path => path![0]) .ToHashSet(); //ignore @@ -114,7 +115,7 @@ protected override Expression CreateBlockExpression(Expression source, Expressio foreach (var ignore in arg.Settings.Ignore) { if (ignore.Value.Condition == null) - ignores.Add(ignore.Key); + ignores.Add(ignore.Key[0]); else { var body = ignore.Value.IsChildPath @@ -123,7 +124,7 @@ protected override Expression CreateBlockExpression(Expression source, Expressio var setWithCondition = Expression.IfThen( ExpressionEx.Not(body), set); - ignoreIfs.Add(ignore.Key, setWithCondition); + ignoreIfs.Add(ignore.Key[0], setWithCondition); } } diff --git a/src/Mapster/Adapters/RecordTypeAdapter.cs b/src/Mapster/Adapters/RecordTypeAdapter.cs index 009af932..21eac3c0 100644 --- a/src/Mapster/Adapters/RecordTypeAdapter.cs +++ b/src/Mapster/Adapters/RecordTypeAdapter.cs @@ -69,7 +69,9 @@ protected override Expression CreateInlineExpression(Expression source, CompileA if (member.UseDestinationValue) return null; - if (!arg.Settings.Resolvers.Any(r => r.DestinationMemberName == member.DestinationMember.Name) + if (!arg.Settings.Resolvers.Any(r + => r.DestinationMemberPath.Length == 1 + && r.DestinationMemberPath[0] == member.DestinationMember.Name) && member.Getter is MemberExpression memberExp && contructorMembers.Contains(memberExp.Member)) continue; diff --git a/src/Mapster/Compile/CompileArgument.cs b/src/Mapster/Compile/CompileArgument.cs index 2e15637a..09469725 100644 --- a/src/Mapster/Compile/CompileArgument.cs +++ b/src/Mapster/Compile/CompileArgument.cs @@ -2,7 +2,6 @@ using System.Collections.Generic; using System.Linq; using System.Linq.Expressions; -using Mapster.Utils; namespace Mapster { @@ -20,16 +19,18 @@ public class CompileArgument internal HashSet GetSourceNames() { return _srcNames ??= (from it in Settings.Resolvers - where it.SourceMemberName != null - select it.SourceMemberName!.Split('.').First()).ToHashSet(); + where it.SourceMemberPath != null + && it.SourceMemberPath.Length > 0 + select it.SourceMemberPath![0]).ToHashSet(); } private HashSet? _destNames; internal HashSet GetDestinationNames() { return _destNames ??= (from it in Settings.Resolvers - where it.DestinationMemberName != null - select it.DestinationMemberName.Split('.').First()).ToHashSet(); + where it.DestinationMemberPath != null + && it.DestinationMemberPath.Length > 0 + select it.DestinationMemberPath[0]).ToHashSet(); } private bool _fetchConstructUsing; diff --git a/src/Mapster/Models/InvokerModel.cs b/src/Mapster/Models/InvokerModel.cs index b5a4546d..66e6f52a 100644 --- a/src/Mapster/Models/InvokerModel.cs +++ b/src/Mapster/Models/InvokerModel.cs @@ -5,27 +5,28 @@ namespace Mapster.Models { public class InvokerModel { - public string DestinationMemberName { get; set; } + public string[] DestinationMemberPath { get; set; } public LambdaExpression? Invoker { get; set; } - public string? SourceMemberName { get; set; } + public string[]? SourceMemberPath { get; set; } public LambdaExpression? Condition { get; set; } public bool IsChildPath { get; set; } public InvokerModel? Next(ParameterExpression source, string destMemberName) { - if (!DestinationMemberName.StartsWith(destMemberName + ".")) + if (DestinationMemberPath.Length == 0 + || DestinationMemberPath[0] != destMemberName) return null; return new InvokerModel { - DestinationMemberName = DestinationMemberName.Substring(destMemberName.Length + 1), + DestinationMemberPath = DestinationMemberPath[1..], Condition = IsChildPath || Condition == null ? Condition : Expression.Lambda(Condition.Apply(source), source), Invoker = IsChildPath ? Invoker : Expression.Lambda(GetInvokingExpression(source), source), - SourceMemberName = SourceMemberName, + SourceMemberPath = SourceMemberPath, IsChildPath = true, }; } @@ -34,8 +35,8 @@ public Expression GetInvokingExpression(Expression exp, MapType mapType = MapTyp { if (IsChildPath) return Invoker!.Body; - return SourceMemberName != null - ? ExpressionEx.PropertyOrFieldPath(exp, SourceMemberName) + return SourceMemberPath != null + ? ExpressionEx.PropertyOrFieldPath(exp, SourceMemberPath) : Invoker!.Apply(mapType, exp); } diff --git a/src/Mapster/Settings/IgnoreDictionary.cs b/src/Mapster/Settings/IgnoreDictionary.cs index aaa530f1..64c2c749 100644 --- a/src/Mapster/Settings/IgnoreDictionary.cs +++ b/src/Mapster/Settings/IgnoreDictionary.cs @@ -1,11 +1,11 @@ -using Mapster.Utils; -using System.Collections.Concurrent; +using System.Collections.Concurrent; using System.Linq; using System.Linq.Expressions; +using Mapster.Utils; namespace Mapster { - public class IgnoreDictionary : ConcurrentDictionary, IApplyable + public class IgnoreDictionary : ConcurrentDictionary, IApplyable { public readonly struct IgnoreItem { @@ -19,11 +19,14 @@ public IgnoreItem(LambdaExpression? condition, bool isChildPath) public bool IsChildPath { get; } } + public IgnoreDictionary() : base(new StringArrayEqualityComparer()) { } + public void Apply(object other) { if (other is IgnoreDictionary collection) Apply(collection); } + public void Apply(IgnoreDictionary other) { foreach (var member in other) @@ -32,9 +35,9 @@ public void Apply(IgnoreDictionary other) } } - internal void Merge(string name, in IgnoreItem src) + internal void Merge(string[] path, in IgnoreItem src) { - if (src.Condition != null && TryGetValue(name, out var item)) + if (src.Condition != null && TryGetValue(path, out var item)) { if (item.Condition == null) return; @@ -43,10 +46,10 @@ internal void Merge(string name, in IgnoreItem src) var body = item.IsChildPath ? item.Condition.Body : item.Condition.Apply(param[0], param[1]); var condition = Expression.Lambda(Expression.OrElse(src.Condition.Body, body), param); - TryUpdate(name, new IgnoreItem(condition, src.IsChildPath), item); + TryUpdate(path, new IgnoreItem(condition, src.IsChildPath), item); } else - TryAdd(name, src); + TryAdd(path, src); } @@ -55,7 +58,7 @@ internal IgnoreDictionary Next(ParameterExpression source, ParameterExpression? var result = new IgnoreDictionary(); foreach (var member in this) { - if (!member.Key.StartsWith(destMemberName + ".")) + if (member.Key.Length <= 1 || member.Key[0] != destMemberName) continue; var condition = member.Value.IsChildPath || member.Value.Condition == null @@ -63,7 +66,7 @@ internal IgnoreDictionary Next(ParameterExpression source, ParameterExpression? : Expression.Lambda(member.Value.Condition.Apply(source, destination), source, destination); var next = new IgnoreItem(condition, true); - result.Merge(member.Key.Substring(destMemberName.Length + 1), next); + result.Merge(member.Key[1..], next); } return result; diff --git a/src/Mapster/Settings/ValueAccessingStrategy.cs b/src/Mapster/Settings/ValueAccessingStrategy.cs index ea47d9ad..6d81f571 100644 --- a/src/Mapster/Settings/ValueAccessingStrategy.cs +++ b/src/Mapster/Settings/ValueAccessingStrategy.cs @@ -36,7 +36,8 @@ public static class ValueAccessingStrategy Expression? getter = null; foreach (var resolver in resolvers) { - if (!destinationMember.Name.Equals(resolver.DestinationMemberName, StringComparison.InvariantCultureIgnoreCase)) + if (resolver.DestinationMemberPath.Length != 1 + || !destinationMember.Name.Equals(resolver.DestinationMemberPath[0], StringComparison.InvariantCultureIgnoreCase)) continue; var invoke = resolver.GetInvokingExpression(source, arg.MapType); @@ -146,8 +147,8 @@ internal static IEnumerable FindUnflatteningPairs(Expression sourc { yield return new InvokerModel { - SourceMemberName = member.Name, - DestinationMemberName = destinationMember.Name + "." + prop, + SourceMemberPath = new[] { member.Name }, + DestinationMemberPath = new[] { destinationMember.Name }.Concat(prop.Split('.')).ToArray(), }; } } @@ -222,11 +223,12 @@ private static IEnumerable GetDeepUnflattening(IMemberModel destinationM Expression? lastCondition = null; foreach (var resolver in resolvers) { - if (!destinationMember.Name.Equals(resolver.DestinationMemberName)) + if (resolver.DestinationMemberPath.Length != 1 + || !destinationMember.Name.Equals(resolver.DestinationMemberPath[0])) continue; Expression invoke = resolver.Invoker == null - ? Expression.Call(method, source.To(dictType), Expression.Constant(resolver.SourceMemberName)) + ? Expression.Call(method, source.To(dictType), Expression.Constant(resolver.SourceMemberPath![0])) : resolver.GetInvokingExpression(source, arg.MapType); getter = lastCondition != null ? Expression.Condition(lastCondition, getter!, invoke) diff --git a/src/Mapster/TypeAdapter.cs b/src/Mapster/TypeAdapter.cs index 0ac37924..fc13d7ce 100644 --- a/src/Mapster/TypeAdapter.cs +++ b/src/Mapster/TypeAdapter.cs @@ -3,6 +3,7 @@ using System.Linq; using System.Reflection; using Mapster.Models; +using Mapster.Utils; namespace Mapster { @@ -249,7 +250,8 @@ public static TDestination ValidateAndAdapt(this TSource { if (sourceProperties.Contains(selectorProperty)) continue; // Check whether the adapter config has a config for the property - if (rule != null && rule.Settings.Resolvers.Any(r => r.DestinationMemberName.Equals(selectorProperty))) continue; + StringArrayEqualityComparer equalityComparer = new(); + if (rule != null && rule.Settings.Resolvers.Any(r => equalityComparer.Equals(r.DestinationMemberPath, selectorProperty.Split('.')))) continue; throw new Exception($"Property {selectorProperty} does not exist in {sourceType.Name} and is not configured in Mapster"); } return source.Adapt(config); diff --git a/src/Mapster/TypeAdapterSetter.cs b/src/Mapster/TypeAdapterSetter.cs index a0581845..6a9d690f 100644 --- a/src/Mapster/TypeAdapterSetter.cs +++ b/src/Mapster/TypeAdapterSetter.cs @@ -57,7 +57,7 @@ public static TSetter Ignore(this TSetter setter, params string[] names foreach (var name in names) { - setter.Settings.Ignore[name] = new IgnoreDictionary.IgnoreItem(); + setter.Settings.Ignore[new[] { name }] = new IgnoreDictionary.IgnoreItem(); } return setter; } @@ -149,7 +149,7 @@ public static TSetter Map( var invoker = Expression.Lambda(source.Body, Expression.Parameter(typeof(object))); setter.Settings.Resolvers.Add(new InvokerModel { - DestinationMemberName = memberName, + DestinationMemberPath = memberName.Split('.'), Invoker = invoker, Condition = null }); @@ -165,8 +165,8 @@ public static TSetter Map( setter.Settings.Resolvers.Add(new InvokerModel { - DestinationMemberName = memberName, - SourceMemberName = source.GetMemberPath(noError: true), + DestinationMemberPath = memberName.Split('.'), + SourceMemberPath = source.GetMemberPath(noError: true), Invoker = source, Condition = null }); @@ -181,8 +181,8 @@ public static TSetter Map( setter.Settings.Resolvers.Add(new InvokerModel { - DestinationMemberName = destinationMemberName, - SourceMemberName = sourceMemberName, + DestinationMemberPath = destinationMemberName.Split('.'), + SourceMemberPath = sourceMemberName.Split('.'), Condition = null }); @@ -332,7 +332,8 @@ public TypeAdapterSetter Ignore(params Expression Map( Settings.Resolvers.Add(new InvokerModel { - DestinationMemberName = member.GetMemberPath()!, + DestinationMemberPath = member.GetMemberPath()!, Invoker = invoker, Condition = null }); @@ -373,8 +374,8 @@ public TypeAdapterSetter Map( Settings.Resolvers.Add(new InvokerModel { - DestinationMemberName = destinationMember.GetMemberPath()!, - SourceMemberName = sourceMemberName, + DestinationMemberPath = destinationMember.GetMemberPath()!, + SourceMemberPath = new[] { sourceMemberName }, Condition = null }); @@ -531,8 +532,8 @@ public TypeAdapterSetter IgnoreIf( foreach (var member in members) { - var name = member.GetMemberPath()!; - Settings.Ignore.Merge(name, new IgnoreDictionary.IgnoreItem(condition, false)); + var path = member.GetMemberPath()!; + Settings.Ignore.Merge(path, new IgnoreDictionary.IgnoreItem(condition, false)); } return this; } @@ -545,7 +546,7 @@ public TypeAdapterSetter IgnoreIf( foreach (var member in members) { - Settings.Ignore.Merge(member, new IgnoreDictionary.IgnoreItem(condition, false)); + Settings.Ignore.Merge(member.Split('.'), new IgnoreDictionary.IgnoreItem(condition, false)); } return this; } @@ -565,8 +566,8 @@ public TypeAdapterSetter Map Map( Settings.Resolvers.Add(new InvokerModel { - DestinationMemberName = memberName, - SourceMemberName = source.GetMemberPath(noError: true), + DestinationMemberPath = memberName.Split('.'), + SourceMemberPath = source.GetMemberPath(noError: true), Invoker = source, Condition = shouldMap }); diff --git a/src/Mapster/Utils/ExpressionEx.cs b/src/Mapster/Utils/ExpressionEx.cs index a1ab8550..d79c8bb3 100644 --- a/src/Mapster/Utils/ExpressionEx.cs +++ b/src/Mapster/Utils/ExpressionEx.cs @@ -3,6 +3,7 @@ using System.Collections; using System.Collections.Generic; using System.Linq; +using System.Linq.Expressions; using System.Reflection; namespace Mapster.Utils @@ -17,10 +18,9 @@ public static Expression Assign(Expression left, Expression right) return Expression.Assign(left, middle); } - public static Expression PropertyOrFieldPath(Expression expr, string path) + public static Expression PropertyOrFieldPath(Expression expr, string[] path) { - var props = path.Split('.'); - return props.Aggregate(expr, PropertyOrField); + return path.Aggregate(expr, PropertyOrField); } private static Expression PropertyOrField(Expression expr, string prop) @@ -352,7 +352,7 @@ public static Expression ApplyNullPropagation(this Expression getter) return getter; } - public static string? GetMemberPath(this LambdaExpression lambda, bool firstLevelOnly = false, bool noError = false) + public static string[]? GetMemberPath(this LambdaExpression lambda, bool firstLevelOnly = false, bool noError = false) { var props = new List(); var expr = lambda.Body.TrimConversion(true); @@ -376,7 +376,7 @@ public static Expression ApplyNullPropagation(this Expression getter) throw new ArgumentException("Allow only member access (eg. obj => obj.Child.Name)", nameof(lambda)); } props.Reverse(); - return string.Join(".", props); + return props.ToArray(); } public static bool IsIdentity(this LambdaExpression lambda) diff --git a/src/Mapster/Utils/ReflectionUtils.cs b/src/Mapster/Utils/ReflectionUtils.cs index dae413d8..71d7ec45 100644 --- a/src/Mapster/Utils/ReflectionUtils.cs +++ b/src/Mapster/Utils/ReflectionUtils.cs @@ -1,11 +1,11 @@ -using Mapster.Models; -using Mapster.Utils; -using System; +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 @@ -69,7 +69,7 @@ public static IEnumerable GetFieldsAndProperties(this Type type, var bindingFlags = BindingFlags.Instance | BindingFlags.Public; if (includeNonPublic) bindingFlags |= BindingFlags.NonPublic; - + if (type.GetTypeInfo().IsInterface) { var allInterfaces = GetAllInterfaces(type); @@ -173,9 +173,9 @@ public static bool IsRecordType(this Type type) if (type.IsConvertible()) return false; - if(RecordTypeIdentityHelper.IsDirectiveTagret(type)) // added Support work from custom Attribute + if (RecordTypeIdentityHelper.IsDirectiveTagret(type)) // added Support work from custom Attribute return true; - + #region SupportingСurrentBehavior for Config Clone and Fork if (type.IsGenericType && type.GetGenericTypeDefinition() == typeof(KeyValuePair<,>)) @@ -183,7 +183,7 @@ public static bool IsRecordType(this Type type) #endregion SupportingСurrentBehavior for Config Clone and Fork - if(RecordTypeIdentityHelper.IsRecordType(type)) + if (RecordTypeIdentityHelper.IsRecordType(type)) return true; return false; @@ -222,7 +222,7 @@ public static bool IsAssignableFromSet(this Type type) var setType = typeof(HashSet<>).MakeGenericType(elementType); return type.GetTypeInfo().IsAssignableFrom(setType.GetTypeInfo()); } - + public static bool IsAssignableFromCollection(this Type type) { return type.IsAssignableFromList() || type.IsAssignableFromSet(); @@ -257,9 +257,9 @@ public static bool IsCollectionCompatible(this Type type) return destinationType; } #endif - return destinationType.GetInterface(type => - type.GetTypeInfo().IsGenericType && - type.GetGenericTypeDefinition() == typeof(IDictionary<,>) ); + return destinationType.GetInterface(type => + type.GetTypeInfo().IsGenericType && + type.GetGenericTypeDefinition() == typeof(IDictionary<,>)); } public static AccessModifier GetAccessModifier(this FieldInfo memberInfo) @@ -352,7 +352,7 @@ public static bool HasDefaultConstructor(this Type type) public static IEnumerable Next(this IEnumerable resolvers, IgnoreDictionary ignore, ParameterExpression source, string destName) { - return resolvers.Where(it => !ignore.TryGetValue(it.DestinationMemberName, out var item) || item.Condition != null) + return resolvers.Where(it => !ignore.TryGetValue(it.DestinationMemberPath, out var item) || item.Condition != null) .Select(it => it.Next(source, destName)) .Where(it => it != null)!; } @@ -370,7 +370,7 @@ public static IEnumerable GetAllTypes(this Type type) type = type.GetTypeInfo().BaseType; } while (type != null && type != typeof(object)); } - + public static bool IsInitOnly(this PropertyInfo propertyInfo) { var setMethod = propertyInfo.SetMethod; diff --git a/src/Mapster/Utils/StringArrayEqualityComparer.cs b/src/Mapster/Utils/StringArrayEqualityComparer.cs new file mode 100644 index 00000000..f07bacf5 --- /dev/null +++ b/src/Mapster/Utils/StringArrayEqualityComparer.cs @@ -0,0 +1,21 @@ +using System.Collections.Generic; +using System.Linq; + +namespace Mapster.Utils; + +internal class StringArrayEqualityComparer : IEqualityComparer +{ + public bool Equals(string[]? x, string[]? y) + { + if (x == null || y == null) + { + return x == y; + } + return x.SequenceEqual(y); + } + + public int GetHashCode(string[] obj) + { + return obj.Aggregate(0, (hash, s) => hash ^ s.GetHashCode()); + } +}