From b45ef61ad93ae4f56bc3cea5f11797916fc2fe4f Mon Sep 17 00:00:00 2001 From: Joost Molenkamp Date: Thu, 27 Feb 2025 17:05:02 +0100 Subject: [PATCH 1/2] Add tests for mapping members containing periods --- .../WhenMappingMemberNameContainingPeriod.cs | 277 ++++++++++++++++++ 1 file changed, 277 insertions(+) create mode 100644 src/Mapster.Tests/WhenMappingMemberNameContainingPeriod.cs diff --git a/src/Mapster.Tests/WhenMappingMemberNameContainingPeriod.cs b/src/Mapster.Tests/WhenMappingMemberNameContainingPeriod.cs new file mode 100644 index 0000000..d86ee07 --- /dev/null +++ b/src/Mapster.Tests/WhenMappingMemberNameContainingPeriod.cs @@ -0,0 +1,277 @@ +using System; +using System.Linq; +using System.Linq.Expressions; +using System.Reflection; +using System.Reflection.Emit; +using System.Runtime.CompilerServices; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +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); + } + + 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; + } + } +} From d8f7693ea02f5e30179e59512072bdd454f533e2 Mon Sep 17 00:00:00 2001 From: Joost Molenkamp Date: Tue, 4 Mar 2025 21:03:15 +0100 Subject: [PATCH 2/2] Allow properties or fields to contain periods --- src/Mapster/Utils/ExpressionEx.cs | 94 +++++++++++++++++++++++++++++-- 1 file changed, 88 insertions(+), 6 deletions(-) diff --git a/src/Mapster/Utils/ExpressionEx.cs b/src/Mapster/Utils/ExpressionEx.cs index a1ab855..f8011b5 100644 --- a/src/Mapster/Utils/ExpressionEx.cs +++ b/src/Mapster/Utils/ExpressionEx.cs @@ -2,7 +2,9 @@ using System; using System.Collections; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.Linq; +using System.Linq.Expressions; using System.Reflection; namespace Mapster.Utils @@ -19,32 +21,112 @@ public static Expression Assign(Expression left, Expression right) public static Expression PropertyOrFieldPath(Expression expr, string path) { - var props = path.Split('.'); - return props.Aggregate(expr, PropertyOrField); + Expression current = expr; + string[] props = path.Split('.'); + + for (int i = 0; i < props.Length; i++) + { + if (IsDictionaryKey(current, props[i], out Expression? next)) + { + current = next; + continue; + } + + if (IsPropertyOrField(current, props[i], out next)) + { + current = next; + continue; + } + + // For dynamically built types, it is possible to have periods in the property name. + // Rejoin an incrementing number of parts with periods to try and find a property match. + if (IsPropertyOrFieldPathWithPeriods(current, props[i..], out next, out int combinationLength)) + { + current = next; + i += combinationLength - 1; + continue; + } + + throw new ArgumentException($"'{props[i]}' is not a member of type '{current.Type.FullName}'", nameof(path)); + } + + return current; } - private static Expression PropertyOrField(Expression expr, string prop) + private static bool IsPropertyOrFieldPathWithPeriods(Expression expr, string[] path, [NotNullWhen(true)] out Expression? propExpr, out int combinationLength) + { + if (path.Length < 2) + { + propExpr = null; + combinationLength = 0; + return false; + } + + for (int count = 2; count <= path.Length; count++) + { + string prop = string.Join('.', path[..count]); + if (IsPropertyOrField(expr, prop, out propExpr)) + { + combinationLength = count; + return true; + } + } + + propExpr = null; + combinationLength = 0; + return false; + } + + private static bool IsDictionaryKey(Expression expr, string prop, [NotNullWhen(true)] out Expression? propExpr) { var type = expr.Type; var dictType = type.GetDictionaryType(); - if (dictType?.GetGenericArguments()[0] == typeof(string)) + + if (dictType?.GetGenericArguments()[0] != typeof(string)) { + propExpr = null; + return false; + } + var method = typeof(MapsterHelper).GetMethods() .First(m => m.Name == nameof(MapsterHelper.GetValueOrDefault) && m.GetParameters()[0].ParameterType.Name == dictType.Name) .MakeGenericMethod(dictType.GetGenericArguments()); - return Expression.Call(method, expr.To(type), Expression.Constant(prop)); + propExpr = Expression.Call(method, expr.To(type), Expression.Constant(prop)); + return true; } + private static bool IsPropertyOrField(Expression expr, string prop, [NotNullWhen(true)] out Expression? propExpr) + { + Type type = expr.Type; + if (type.GetTypeInfo().IsInterface) { var allTypes = type.GetAllInterfaces(); var flags = BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance; var interfaceType = allTypes.FirstOrDefault(it => it.GetProperty(prop, flags) != null || it.GetField(prop, flags) != null); if (interfaceType != null) + { expr = Expression.Convert(expr, interfaceType); + type = expr.Type; + } } - return Expression.PropertyOrField(expr, prop); + + MemberInfo? propertyOrField = type + .GetMember( + prop, + MemberTypes.Field | MemberTypes.Property, + BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.FlattenHierarchy) + .FirstOrDefault(); + + propExpr = propertyOrField?.MemberType switch + { + MemberTypes.Property => Expression.Property(expr, (PropertyInfo)propertyOrField), + MemberTypes.Field => Expression.Field(expr, (FieldInfo)propertyOrField), + _ => null + }; + + return propExpr != null; } private static bool IsReferenceAssignableFrom(this Type destType, Type srcType)