Skip to content

Commit b436031

Browse files
authored
Merge pull request #1 from JMolenkamp/development
Development-4
2 parents 155a723 + 0b9b05f commit b436031

15 files changed

+407
-73
lines changed
Lines changed: 298 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,298 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.Linq;
4+
using System.Linq.Expressions;
5+
using System.Reflection;
6+
using System.Reflection.Emit;
7+
using System.Runtime.CompilerServices;
8+
using Microsoft.VisualStudio.TestTools.UnitTesting;
9+
using Shouldly;
10+
11+
namespace Mapster.Tests;
12+
13+
[TestClass]
14+
public class WhenMappingMemberNameContainingPeriod
15+
{
16+
private const string MemberName = "Some.Property.With.Periods";
17+
18+
[TestMethod]
19+
public void Property_Name_Containing_Periods_Is_Supported()
20+
{
21+
// Create a target type with a property that contains periods
22+
Type targetType = new TestTypeBuilder()
23+
.AddProperty<int>(MemberName)
24+
.CreateType();
25+
26+
// Call the local function defined below, the actual test method
27+
CallStaticLocalTestMethod(
28+
nameof(Test),
29+
new Type[] { targetType });
30+
31+
// The actual test method adapting Source to the target type and back to the source to verify mapping the property with periods
32+
static void Test<TTarget>()
33+
{
34+
// Get expression for mapping the property with periods
35+
Expression<Func<TTarget, int>> getPropertyExpression = BuildGetPropertyExpression<TTarget, int>(MemberName);
36+
37+
// Create the config
38+
TypeAdapterConfig<Source, TTarget>
39+
.NewConfig()
40+
.TwoWays()
41+
.Map(getPropertyExpression, src => src.Value);
42+
43+
// Execute the mapping both ways
44+
Source source = new() { Value = 551 };
45+
TTarget target = source.Adapt<TTarget>();
46+
Source adaptedSource = target.Adapt<Source>();
47+
48+
Assert.AreEqual(source.Value, adaptedSource.Value);
49+
}
50+
}
51+
52+
[TestMethod]
53+
public void Constructor_Parameter_Name_Containing_Periods_Is_Supported()
54+
{
55+
// Create a target type with a property that contains periods
56+
Type targetTypeWithProperty = new TestTypeBuilder()
57+
.AddProperty<int>(MemberName)
58+
.CreateType();
59+
60+
// Create a target type with a constructor parameter that contains periods
61+
Type targetTypeWithConstructor = new TestTypeBuilder()
62+
.AddConstructorWithReadOnlyProperty<int>(MemberName)
63+
.CreateType();
64+
65+
// Call the local function defined below, the actual test method
66+
CallStaticLocalTestMethod(
67+
nameof(Test),
68+
new Type[] { targetTypeWithProperty, targetTypeWithConstructor });
69+
70+
// The actual test method
71+
static void Test<TWithProperty, TWithConstructor>()
72+
where TWithProperty : new()
73+
{
74+
// Create the config
75+
TypeAdapterConfig<TWithProperty, TWithConstructor>
76+
.NewConfig()
77+
.TwoWays()
78+
.MapToConstructor(true);
79+
80+
// Create delegate for setting the property value on TWithProperty
81+
Expression<Action<TWithProperty, int>> setPropertyExpression = BuildSetPropertyExpression<TWithProperty, int>(MemberName);
82+
Action<TWithProperty, int> setProperty = setPropertyExpression.Compile();
83+
84+
// Create the source object
85+
int value = 551;
86+
TWithProperty source = new();
87+
setProperty.Invoke(source, value);
88+
89+
// Map
90+
TWithConstructor target = source.Adapt<TWithConstructor>();
91+
TWithProperty adaptedSource = target.Adapt<TWithProperty>();
92+
93+
// Create delegate for getting the property from TWithProperty
94+
Expression<Func<TWithProperty, int>> getPropertyExpression = BuildGetPropertyExpression<TWithProperty, int>(MemberName);
95+
Func<TWithProperty, int> getProperty = getPropertyExpression.Compile();
96+
97+
// Verify
98+
Assert.AreEqual(value, getProperty.Invoke(adaptedSource));
99+
}
100+
}
101+
102+
[TestMethod]
103+
public void Using_Property_Path_String_Is_Supported()
104+
{
105+
// Create a target type with a property that contains periods
106+
Type targetType = new TestTypeBuilder()
107+
.AddProperty<int>(MemberName)
108+
.CreateType();
109+
110+
// Create the config, both ways
111+
TypeAdapterConfig
112+
.GlobalSettings
113+
.NewConfig(typeof(Source), targetType)
114+
.Map(MemberName, nameof(Source.Value));
115+
TypeAdapterConfig
116+
.GlobalSettings
117+
.NewConfig(targetType, typeof(Source))
118+
.Map(nameof(Source.Value), MemberName);
119+
120+
// Execute the mapping both ways
121+
Source source = new() { Value = 551 };
122+
object target = source.Adapt(typeof(Source), targetType);
123+
Source adaptedSource = target.Adapt<Source>();
124+
125+
Assert.AreEqual(source.Value, adaptedSource.Value);
126+
}
127+
128+
[TestMethod]
129+
public void Object_To_Dictionary_Map()
130+
{
131+
var poco = new SimplePoco
132+
{
133+
Id = Guid.NewGuid(),
134+
Name = "test",
135+
};
136+
137+
var config = new TypeAdapterConfig();
138+
config.NewConfig<SimplePoco, Dictionary<string, object>>()
139+
.Map(MemberName, c => c.Id);
140+
var dict = poco.Adapt<Dictionary<string, object>>(config);
141+
142+
dict.Count.ShouldBe(2);
143+
dict[MemberName].ShouldBe(poco.Id);
144+
dict["Name"].ShouldBe(poco.Name);
145+
}
146+
147+
private static void CallStaticLocalTestMethod(string methodName, Type[] genericArguments, [CallerMemberName] string caller = "Unknown")
148+
{
149+
MethodInfo genericMethodInfo = typeof(WhenMappingMemberNameContainingPeriod)
150+
.GetMethods(BindingFlags.NonPublic | BindingFlags.Static)
151+
.Single(x => x.Name.Contains($"<{caller}>") && x.Name.Contains(methodName));
152+
153+
MethodInfo method = genericMethodInfo.MakeGenericMethod(genericArguments);
154+
155+
method.Invoke(null, null);
156+
}
157+
158+
private static Expression<Func<T, TProperty>> BuildGetPropertyExpression<T, TProperty>(string propertyName)
159+
{
160+
ParameterExpression param = Expression.Parameter(typeof(T), "x");
161+
MemberExpression property = Expression.Property(param, propertyName);
162+
return Expression.Lambda<Func<T, TProperty>>(property, param);
163+
}
164+
165+
private static Expression<Action<T, TProperty>> BuildSetPropertyExpression<T, TProperty>(string propertyName)
166+
{
167+
ParameterExpression param = Expression.Parameter(typeof(T), "x");
168+
ParameterExpression value = Expression.Parameter(typeof(TProperty), "value");
169+
MemberExpression property = Expression.Property(param, propertyName);
170+
BinaryExpression assign = Expression.Assign(property, value);
171+
return Expression.Lambda<Action<T, TProperty>>(assign, param, value);
172+
}
173+
174+
private class Source
175+
{
176+
public int Value { get; set; }
177+
}
178+
179+
private class TestTypeBuilder
180+
{
181+
private readonly TypeBuilder _typeBuilder;
182+
183+
public TestTypeBuilder()
184+
{
185+
AssemblyBuilder assemblyBuilder = AssemblyBuilder.DefineDynamicAssembly(
186+
new AssemblyName("Types"),
187+
AssemblyBuilderAccess.Run);
188+
ModuleBuilder moduleBuilder = assemblyBuilder.DefineDynamicModule("<Module>");
189+
_typeBuilder = moduleBuilder.DefineType(
190+
"Types.Target",
191+
TypeAttributes.Public |
192+
TypeAttributes.Class |
193+
TypeAttributes.Sealed |
194+
TypeAttributes.AutoClass |
195+
TypeAttributes.AnsiClass |
196+
TypeAttributes.BeforeFieldInit |
197+
TypeAttributes.AutoLayout,
198+
null);
199+
}
200+
201+
public TestTypeBuilder AddConstructorWithReadOnlyProperty<TParameter>(string parameterName)
202+
{
203+
// Add read-only property
204+
FieldBuilder fieldBuilder = AddProperty<TParameter>(parameterName, false);
205+
206+
// Build the constructor with the parameter for the property
207+
ConstructorBuilder constructorBuilder = _typeBuilder.DefineConstructor(
208+
MethodAttributes.Public,
209+
CallingConventions.Standard,
210+
new Type[] { typeof(TParameter) });
211+
212+
// Define the parameter name
213+
constructorBuilder.DefineParameter(1, ParameterAttributes.None, MemberName);
214+
215+
ILGenerator constructorIL = constructorBuilder.GetILGenerator();
216+
217+
// Call the base class constructor
218+
constructorIL.Emit(OpCodes.Ldarg_0);
219+
constructorIL.Emit(OpCodes.Call, typeof(object).GetConstructor(Type.EmptyTypes));
220+
221+
// Set the property value
222+
constructorIL.Emit(OpCodes.Ldarg_0);
223+
constructorIL.Emit(OpCodes.Ldarg_1);
224+
constructorIL.Emit(OpCodes.Stfld, fieldBuilder);
225+
226+
constructorIL.Emit(OpCodes.Ret);
227+
228+
return this;
229+
}
230+
231+
public TestTypeBuilder AddProperty<T>(string propertyName)
232+
{
233+
AddProperty<T>(propertyName, true);
234+
return this;
235+
}
236+
237+
private FieldBuilder AddProperty<T>(string propertyName, bool addSetter)
238+
{
239+
Type propertyType = typeof(T);
240+
FieldBuilder fieldBuilder = _typeBuilder.DefineField($"_{propertyName}", propertyType, FieldAttributes.Private);
241+
PropertyBuilder propertyBuilder = _typeBuilder.DefineProperty(propertyName, PropertyAttributes.None, propertyType, null);
242+
243+
AddGetMethod(_typeBuilder, propertyBuilder, fieldBuilder, propertyName, propertyType);
244+
if (addSetter)
245+
{
246+
AddSetMethod(_typeBuilder, propertyBuilder, fieldBuilder, propertyName, propertyType);
247+
}
248+
249+
return fieldBuilder;
250+
}
251+
252+
public Type CreateType() => _typeBuilder.CreateType();
253+
254+
private static PropertyBuilder AddGetMethod(TypeBuilder typeBuilder, PropertyBuilder propertyBuilder, FieldBuilder fieldBuilder, string propertyName, Type propertyType)
255+
{
256+
MethodBuilder getMethodBuilder = typeBuilder.DefineMethod(
257+
"get_" + propertyName,
258+
MethodAttributes.Public | MethodAttributes.SpecialName | MethodAttributes.HideBySig,
259+
propertyType,
260+
Type.EmptyTypes);
261+
ILGenerator getMethodGenerator = getMethodBuilder.GetILGenerator();
262+
263+
getMethodGenerator.Emit(OpCodes.Ldarg_0);
264+
getMethodGenerator.Emit(OpCodes.Ldfld, fieldBuilder);
265+
getMethodGenerator.Emit(OpCodes.Ret);
266+
267+
propertyBuilder.SetGetMethod(getMethodBuilder);
268+
269+
return propertyBuilder;
270+
}
271+
272+
private static PropertyBuilder AddSetMethod(TypeBuilder typeBuilder, PropertyBuilder propertyBuilder, FieldBuilder fieldBuilder, string propertyName, Type propertyType)
273+
{
274+
MethodBuilder setMethodBuilder = typeBuilder.DefineMethod(
275+
$"set_{propertyName}",
276+
MethodAttributes.Public | MethodAttributes.SpecialName | MethodAttributes.HideBySig,
277+
null,
278+
new Type[] { propertyType });
279+
280+
ILGenerator setMethodGenerator = setMethodBuilder.GetILGenerator();
281+
Label modifyProperty = setMethodGenerator.DefineLabel();
282+
Label exitSet = setMethodGenerator.DefineLabel();
283+
284+
setMethodGenerator.MarkLabel(modifyProperty);
285+
setMethodGenerator.Emit(OpCodes.Ldarg_0);
286+
setMethodGenerator.Emit(OpCodes.Ldarg_1);
287+
setMethodGenerator.Emit(OpCodes.Stfld, fieldBuilder);
288+
289+
setMethodGenerator.Emit(OpCodes.Nop);
290+
setMethodGenerator.MarkLabel(exitSet);
291+
setMethodGenerator.Emit(OpCodes.Ret);
292+
293+
propertyBuilder.SetSetMethod(setMethodBuilder);
294+
295+
return propertyBuilder;
296+
}
297+
}
298+
}

src/Mapster.Tool/Program.cs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,7 @@ private static void GenerateMappers(MapperOptions opt)
7676
// This way when we compare attribute types (such as MapperAttribute) between our running assembly
7777
// and the scanned assembly the two types with the same FullName can be considered equal because
7878
// they both were resolved from AssemblyLoadContext.Default.
79-
79+
8080
// This isolated Assembly Load Context will be able to resolve the Mapster assembly, but
8181
// the resolved Assembly will be the same one that is in AssemblyLoadContext.Default
8282
// (the runtime assembly load context that our code refers to by default when referencing
@@ -461,8 +461,8 @@ Dictionary<string, PropertySetting> settings
461461
setter.Settings.Resolvers.Add(
462462
new InvokerModel
463463
{
464-
DestinationMemberName = setting.TargetPropertyName ?? name,
465-
SourceMemberName = name,
464+
DestinationMemberPath = (setting.TargetPropertyName ?? name).Split('.'),
465+
SourceMemberPath = name.Split('.'),
466466
Invoker = setting.MapFunc,
467467
}
468468
);

src/Mapster/Adapters/BaseClassAdapter.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ protected ClassMapping CreateClassConverter(Expression source, ClassModel classM
2626
arg.Settings.ExtraSources.Select(src =>
2727
src is LambdaExpression lambda
2828
? lambda.Apply(arg.MapType, source)
29-
: ExpressionEx.PropertyOrFieldPath(source, (string)src)));
29+
: ExpressionEx.PropertyOrFieldPath(source, (string[])src)));
3030
foreach (var destinationMember in destinationMembers)
3131
{
3232
if (ProcessIgnores(arg, destinationMember, out var ignore))
@@ -124,7 +124,7 @@ protected static bool ProcessIgnores(
124124
if (!destinationMember.ShouldMapMember(arg, MemberSide.Destination))
125125
return true;
126126

127-
return arg.Settings.Ignore.TryGetValue(destinationMember.Name, out ignore)
127+
return arg.Settings.Ignore.TryGetValue(new[] { destinationMember.Name }, out ignore)
128128
&& ignore.Condition == null;
129129
}
130130

src/Mapster/Adapters/ClassAdapter.cs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -214,7 +214,9 @@ private static Expression SetValueByReflection(MemberMapping member, MemberExpre
214214
if (member.UseDestinationValue)
215215
return null;
216216

217-
if (!arg.Settings.Resolvers.Any(r => r.DestinationMemberName == member.DestinationMember.Name)
217+
if (!arg.Settings.Resolvers.Any(r
218+
=> r.DestinationMemberPath.Length == 1
219+
&& r.DestinationMemberPath[0] == member.DestinationMember.Name)
218220
&& member.Getter is MemberExpression memberExp && contructorMembers.Contains(memberExp.Member))
219221
continue;
220222

src/Mapster/Adapters/DictionaryAdapter.cs

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -105,16 +105,17 @@ protected override Expression CreateBlockExpression(Expression source, Expressio
105105

106106
//ignore mapped
107107
var ignores = arg.Settings.Resolvers
108-
.Select(r => r.SourceMemberName)
109-
.Where(name => name != null)
108+
.Select(r => r.SourceMemberPath)
109+
.Where(path => path != null)
110+
.Select(path => path![0])
110111
.ToHashSet();
111112

112113
//ignore
113114
var ignoreIfs = new Dictionary<string, Expression>();
114115
foreach (var ignore in arg.Settings.Ignore)
115116
{
116117
if (ignore.Value.Condition == null)
117-
ignores.Add(ignore.Key);
118+
ignores.Add(ignore.Key[0]);
118119
else
119120
{
120121
var body = ignore.Value.IsChildPath
@@ -123,7 +124,7 @@ protected override Expression CreateBlockExpression(Expression source, Expressio
123124
var setWithCondition = Expression.IfThen(
124125
ExpressionEx.Not(body),
125126
set);
126-
ignoreIfs.Add(ignore.Key, setWithCondition);
127+
ignoreIfs.Add(ignore.Key[0], setWithCondition);
127128
}
128129
}
129130

src/Mapster/Adapters/RecordTypeAdapter.cs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,9 @@ protected override Expression CreateInlineExpression(Expression source, CompileA
6969
if (member.UseDestinationValue)
7070
return null;
7171

72-
if (!arg.Settings.Resolvers.Any(r => r.DestinationMemberName == member.DestinationMember.Name)
72+
if (!arg.Settings.Resolvers.Any(r
73+
=> r.DestinationMemberPath.Length == 1
74+
&& r.DestinationMemberPath[0] == member.DestinationMember.Name)
7375
&& member.Getter is MemberExpression memberExp && contructorMembers.Contains(memberExp.Member))
7476
continue;
7577

0 commit comments

Comments
 (0)