Skip to content

Commit b45ef61

Browse files
committed
Add tests for mapping members containing periods
1 parent 155a723 commit b45ef61

File tree

1 file changed

+277
-0
lines changed

1 file changed

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

0 commit comments

Comments
 (0)