Skip to content

Commit d49d54f

Browse files
authored
Merge pull request #100 from AutoMapper/SupportIncludeMembers
Supporting IncludeMembers.
2 parents db88e0d + 442eeb5 commit d49d54f

File tree

8 files changed

+572
-26
lines changed

8 files changed

+572
-26
lines changed

Pack_Push.ps1

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ $NUGET_PACKAGE_PATH = ".\artifacts\$($Env:PROJECT_NAME).*.nupkg"
99
Write-Host "Project Path ${PROJECT_PATH}"
1010
Write-Host "Package Path ${NUGET_PACKAGE_PATH}"
1111

12-
if ($Env:REPO_OWNER -ne "AutoMapper") {
12+
if ([string]::IsNullOrEmpty($Env:DEPLOY_PACKAGE_API_KEY)) {
1313
Write-Host "${scriptName}: Only creates packages on AutoMapper repositories."
1414
} else {
1515
dotnet pack $PROJECT_PATH -c Release -o .\artifacts --no-build

src/AutoMapper.Extensions.ExpressionMapping/AutoMapper.Extensions.ExpressionMapping.csproj

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818
</PropertyGroup>
1919

2020
<ItemGroup>
21-
<PackageReference Include="AutoMapper" Version="[10.0.0,11.0.0)" />
21+
<PackageReference Include="AutoMapper" Version="[10.1.1,11.0.0)" />
2222
<PackageReference Include="MinVer" Version="2.3.0">
2323
<PrivateAssets>all</PrivateAssets>
2424
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>

src/AutoMapper.Extensions.ExpressionMapping/Extensions/VisitorExtensions.cs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,9 @@ public static Expression ConvertTypeIfNecessary(this Expression expression, Type
9595
public static MemberExpression GetMemberExpression(this LambdaExpression expr)
9696
=> expr.Body.GetUnconvertedMemberExpression() as MemberExpression;
9797

98+
public static MemberExpression GetMemberExpression(this Expression expr)
99+
=> expr.GetUnconvertedMemberExpression() as MemberExpression;
100+
98101
/// <summary>
99102
/// Returns the ParameterExpression for the LINQ parameter.
100103
/// </summary>
@@ -177,7 +180,8 @@ public static string GetMemberFullName(this LambdaExpression expr)
177180
{
178181
case ExpressionType.Convert:
179182
case ExpressionType.ConvertChecked:
180-
me = (expr.Body as UnaryExpression)?.Operand as MemberExpression;
183+
case ExpressionType.TypeAs:
184+
me = expr.Body.GetUnconvertedMemberExpression() as MemberExpression;
181185
break;
182186
default:
183187
me = expr.Body as MemberExpression;
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
using System;
2+
using System.Reflection;
3+
4+
namespace AutoMapper.Extensions.ExpressionMapping.Structures
5+
{
6+
internal class DeclaringMemberKey : IEquatable<DeclaringMemberKey>
7+
{
8+
public DeclaringMemberKey(MemberInfo declaringMemberInfo, string declaringMemberFullName)
9+
{
10+
DeclaringMemberInfo = declaringMemberInfo;
11+
DeclaringMemberFullName = declaringMemberFullName;
12+
}
13+
14+
public MemberInfo DeclaringMemberInfo { get; set; }
15+
public string DeclaringMemberFullName { get; set; }
16+
17+
public override bool Equals(object obj)
18+
{
19+
if (ReferenceEquals(null, obj)) return false;
20+
if (ReferenceEquals(this, obj)) return true;
21+
22+
DeclaringMemberKey key = obj as DeclaringMemberKey;
23+
if (key == null) return false;
24+
25+
return Equals(key);
26+
}
27+
28+
public bool Equals(DeclaringMemberKey other)
29+
{
30+
if (ReferenceEquals(null, other)) return false;
31+
if (ReferenceEquals(this, other)) return true;
32+
33+
return this.DeclaringMemberInfo.Equals(other.DeclaringMemberInfo)
34+
&& this.DeclaringMemberFullName == other.DeclaringMemberFullName;
35+
}
36+
37+
public override int GetHashCode() => this.DeclaringMemberInfo.GetHashCode();
38+
39+
public override string ToString() => this.DeclaringMemberFullName;
40+
}
41+
}
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.Linq.Expressions;
4+
using System.Text;
5+
6+
namespace AutoMapper.Extensions.ExpressionMapping.Structures
7+
{
8+
internal class MemberAssignmentInfo
9+
{
10+
public MemberAssignmentInfo(PropertyMap propertyMap, MemberAssignment memberAssignment)
11+
{
12+
PropertyMap = propertyMap;
13+
MemberAssignment = memberAssignment;
14+
}
15+
16+
/// <summary>
17+
/// Used to get the source member to be bound with the mapped binding expression.
18+
/// </summary>
19+
public PropertyMap PropertyMap { get; set; }
20+
21+
/// <summary>
22+
/// Initial member assignment who's binding expression will be mapped and assigned to the source menber of the new type
23+
/// </summary>
24+
public MemberAssignment MemberAssignment { get; set; }
25+
}
26+
}
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
using System;
2+
using System.Collections.Generic;
3+
4+
namespace AutoMapper.Extensions.ExpressionMapping.Structures
5+
{
6+
/// <summary>
7+
/// Defines the type to be initialized and a list of source bindings.
8+
/// The new bound members will be matched using MemberAssignmentInfos.PropertyMap and
9+
/// assigned to the mapped expression (mapped from MemberAssignmentInfos.MemberAssignment.Expression).
10+
/// </summary>
11+
internal class MemberBindingGroup
12+
{
13+
public MemberBindingGroup(DeclaringMemberKey declaringMemberKey, bool isRootMemberAssignment, Type newType, List<MemberAssignmentInfo> memberAssignmentInfos)
14+
{
15+
DeclaringMemberKey = declaringMemberKey;
16+
IsRootMemberAssignment = isRootMemberAssignment;
17+
NewType = newType;
18+
MemberAssignmentInfos = memberAssignmentInfos;
19+
}
20+
21+
/// <summary>
22+
/// DeclaringMemberKey will be null when the member assignment is a member binding of OldType on the initial (root) TypeMap (OldType -> NewType)
23+
/// </summary>
24+
public DeclaringMemberKey DeclaringMemberKey { get; set; }
25+
26+
/// <summary>
27+
/// MemberAssignment is true if it is a member binding of OldType on the initial (root) TypeMap (OldType -> NewType)
28+
/// </summary>
29+
public bool IsRootMemberAssignment { get; set; }
30+
31+
/// <summary>
32+
/// Destination type of the member assignment. If IsRootMemberAssignment == true then this is the destination type of initial (root) TypeMap (OldType -> NewType)
33+
/// Otherwise it is the PropertyType/FieldType of DeclaringMemberInfo
34+
/// </summary>
35+
public Type NewType { get; set; }
36+
37+
/// <summary>
38+
/// List of members to be mapped and bound to the new type
39+
/// </summary>
40+
public List<MemberAssignmentInfo> MemberAssignmentInfos { get; set; }
41+
}
42+
}

src/AutoMapper.Extensions.ExpressionMapping/XpressionMapperVisitor.cs

Lines changed: 146 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,14 @@
1-
using System;
1+
using AutoMapper.Extensions.ExpressionMapping.Extensions;
2+
using AutoMapper.Extensions.ExpressionMapping.Structures;
3+
using AutoMapper.Internal;
4+
using AutoMapper.QueryableExtensions.Impl;
5+
using System;
26
using System.Collections.Generic;
37
using System.Globalization;
48
using System.Linq;
59
using System.Linq.Expressions;
610
using System.Reflection;
711
using System.Text;
8-
using AutoMapper.Extensions.ExpressionMapping.ArgumentMappers;
9-
using AutoMapper.Extensions.ExpressionMapping.Extensions;
10-
using AutoMapper.Extensions.ExpressionMapping.Structures;
11-
using AutoMapper.Internal;
12-
using AutoMapper.QueryableExtensions.Impl;
1312

1413
namespace AutoMapper.Extensions.ExpressionMapping
1514
{
@@ -175,34 +174,119 @@ protected override Expression VisitMemberInit(MemberInitExpression node)
175174
//The destination becomes the source because to map a source expression to a destination expression,
176175
//we need the expressions used to create the source from the destination
177176

178-
IEnumerable<MemberBinding> bindings = node.Bindings.Aggregate(new List<MemberBinding>(), (list, binding) =>
179-
{
180-
var propertyMap = typeMap.PropertyMaps.SingleOrDefault(item => item.DestinationName == binding.Member.Name);
181-
if (propertyMap == null)
182-
return list;
177+
return GetMemberInit
178+
(
179+
new MemberBindingGroup
180+
(
181+
declaringMemberKey: null,
182+
isRootMemberAssignment: true,
183+
newType: newType,
184+
memberAssignmentInfos: node.Bindings.OfType<MemberAssignment>().Aggregate(new List<MemberAssignmentInfo>(), (list, binding) =>
185+
{
186+
var propertyMap = typeMap.PropertyMaps.SingleOrDefault(item => item.DestinationName == binding.Member.Name);
187+
if (propertyMap == null)
188+
return list;
189+
190+
list.Add(new MemberAssignmentInfo(propertyMap, binding));
191+
return list;
192+
})
193+
)
194+
);
195+
}
196+
197+
return base.VisitMemberInit(node);
198+
199+
}
183200

184-
var sourceMember = GetSourceMember(propertyMap);
185-
if (sourceMember == null)
186-
return list;
201+
private MemberInitExpression GetMemberInit(MemberBindingGroup memberBindingGroup)
202+
{
203+
Dictionary<DeclaringMemberKey, List<MemberAssignmentInfo>> includedMembers = new Dictionary<DeclaringMemberKey, List<MemberAssignmentInfo>>();
204+
205+
List<MemberBinding> bindings = memberBindingGroup.MemberAssignmentInfos.Aggregate(new List<MemberBinding>(), (list, next) =>
206+
{
207+
var propertyMap = next.PropertyMap;
208+
var binding = next.MemberAssignment;
209+
210+
var sourceMember = GetSourceMember(propertyMap);//does the corresponding member mapping exist
211+
if (sourceMember == null)
212+
return list;
213+
214+
DeclaringMemberKey declaringMemberKey = new DeclaringMemberKey
215+
(
216+
GetParentMember(propertyMap),
217+
BuildParentFullName(propertyMap)
218+
);
187219

188-
Expression bindingExpression = ((MemberAssignment)binding).Expression;
189-
list.Add
220+
if (ShouldBindPropertyMap(next))
221+
{
222+
list.Add//adding bindings for property maps
190223
(
191224
DoBind
192225
(
193226
sourceMember,
194-
bindingExpression,
195-
this.Visit(bindingExpression)
227+
binding.Expression,
228+
this.Visit(binding.Expression)
196229
)
197230
);
231+
}
232+
else
233+
{
234+
if (declaringMemberKey.DeclaringMemberInfo == null)
235+
throw new ArgumentNullException(nameof(declaringMemberKey.DeclaringMemberInfo));
198236

199-
return list;
200-
});
237+
if (!includedMembers.TryGetValue(declaringMemberKey, out List<MemberAssignmentInfo> assignments))
238+
{
239+
includedMembers.Add
240+
(
241+
declaringMemberKey,
242+
new List<MemberAssignmentInfo>
243+
{
244+
new MemberAssignmentInfo
245+
(
246+
propertyMap,
247+
binding
248+
)
249+
}
250+
);
251+
}
252+
else
253+
{
254+
assignments.Add(new MemberAssignmentInfo(propertyMap, binding));
255+
}
256+
}
201257

202-
return Expression.MemberInit(Expression.New(newType), bindings);
203-
}
258+
return list;
204259

205-
return base.VisitMemberInit(node);
260+
bool ShouldBindPropertyMap(MemberAssignmentInfo memberAssignmentInfo)
261+
=> (memberBindingGroup.IsRootMemberAssignment && sourceMember.ReflectedType == memberBindingGroup.NewType)
262+
|| (!memberBindingGroup.IsRootMemberAssignment && declaringMemberKey.Equals(memberBindingGroup.DeclaringMemberKey));
263+
});
264+
265+
includedMembers.Select
266+
(
267+
kvp => new MemberBindingGroup
268+
(
269+
declaringMemberKey: kvp.Key,
270+
isRootMemberAssignment: false,
271+
newType: kvp.Key.DeclaringMemberInfo.GetMemberType(),
272+
memberAssignmentInfos: includedMembers.Values.SelectMany(m => m).ToList()
273+
)
274+
)
275+
.ToList()
276+
.ForEach(group =>
277+
{
278+
if (ShouldBindChildReference(group))
279+
bindings.Add(Expression.Bind(group.DeclaringMemberKey.DeclaringMemberInfo, GetMemberInit(group)));
280+
});
281+
282+
bool ShouldBindChildReference(MemberBindingGroup group)
283+
=> (memberBindingGroup.IsRootMemberAssignment
284+
&& group.DeclaringMemberKey.DeclaringMemberInfo.ReflectedType == memberBindingGroup.NewType)
285+
|| (!memberBindingGroup.IsRootMemberAssignment
286+
&& group.DeclaringMemberKey.DeclaringMemberInfo.ReflectedType == memberBindingGroup.NewType
287+
&& group.DeclaringMemberKey.DeclaringMemberFullName.StartsWith(memberBindingGroup.DeclaringMemberKey.DeclaringMemberFullName));
288+
289+
return Expression.MemberInit(Expression.New(memberBindingGroup.NewType), bindings);
206290
}
207291

208292
private MemberBinding DoBind(MemberInfo sourceMember, Expression initial, Expression mapped)
@@ -217,6 +301,39 @@ private MemberInfo GetSourceMember(PropertyMap propertyMap)
217301
? propertyMap.CustomMapExpression.GetMemberExpression()?.Member
218302
: propertyMap.SourceMember;
219303

304+
private MemberInfo GetParentMember(PropertyMap propertyMap)
305+
=> propertyMap.IncludedMember != null
306+
? propertyMap.ProjectToCustomSource.GetMemberExpression().Member
307+
: GetSourceParentMember(propertyMap);
308+
309+
private MemberInfo GetSourceParentMember(PropertyMap propertyMap)
310+
{
311+
if (propertyMap.CustomMapExpression != null)
312+
return propertyMap.CustomMapExpression.GetMemberExpression()?.Expression.GetMemberExpression()?.Member;
313+
314+
if (propertyMap.SourceMembers.Count > 1)
315+
return new List<MemberInfo>(propertyMap.SourceMembers)[propertyMap.SourceMembers.Count - 2];
316+
317+
return null;
318+
}
319+
320+
private string BuildParentFullName(PropertyMap propertyMap)
321+
{
322+
List<PropertyMapInfo> propertyMapInfos = new List<PropertyMapInfo>();
323+
if (propertyMap.IncludedMember != null)
324+
propertyMapInfos.Add(new PropertyMapInfo(propertyMap.ProjectToCustomSource, new List<MemberInfo>()));
325+
326+
propertyMapInfos.Add(new PropertyMapInfo(propertyMap.CustomMapExpression, propertyMap.SourceMembers.ToList()));
327+
328+
List<string> fullNameArray = BuildFullName(propertyMapInfos)
329+
.Split(new char[] { '.' })
330+
.ToList();
331+
332+
fullNameArray.Remove(fullNameArray.Last());
333+
334+
return string.Join(".", fullNameArray);
335+
}
336+
220337
protected override Expression VisitBinary(BinaryExpression node)
221338
{
222339
return DoVisitBinary(this.Visit(node.Left), this.Visit(node.Right), this.Visit(node.Conversion));
@@ -509,6 +626,9 @@ void CompareSourceAndDestLiterals(Type mappedPropertyType, string mappedProperty
509626
throw new InvalidOperationException(string.Format(CultureInfo.CurrentCulture, Resource.expressionMapValueTypeMustMatchFormat, mappedPropertyType.Name, mappedPropertyDescription, sourceMemberType.Name, propertyMap.DestinationMember.Name));
510627
}
511628

629+
if (propertyMap.ProjectToCustomSource != null)
630+
propertyMapInfoList.Add(new PropertyMapInfo(propertyMap.ProjectToCustomSource, new List<MemberInfo>()));
631+
512632
propertyMapInfoList.Add(new PropertyMapInfo(propertyMap.CustomMapExpression, propertyMap.SourceMembers.ToList()));
513633
}
514634
else
@@ -520,6 +640,9 @@ void CompareSourceAndDestLiterals(Type mappedPropertyType, string mappedProperty
520640
if (propertyMap.CustomMapExpression == null && !propertyMap.SourceMembers.Any())//If sourceFullName has a period then the SourceMember cannot be null. The SourceMember is required to find the ProertyMap of its child object.
521641
throw new InvalidOperationException(string.Format(CultureInfo.CurrentCulture, Resource.srcMemberCannotBeNullFormat, typeSource.Name, typeDestination.Name, propertyName));
522642

643+
if (propertyMap.ProjectToCustomSource != null)
644+
propertyMapInfoList.Add(new PropertyMapInfo(propertyMap.ProjectToCustomSource, new List<MemberInfo>()));
645+
523646
propertyMapInfoList.Add(new PropertyMapInfo(propertyMap.CustomMapExpression, propertyMap.SourceMembers.ToList()));
524647
var childFullName = sourceFullName.Substring(sourceFullName.IndexOf(period, StringComparison.OrdinalIgnoreCase) + 1);
525648

0 commit comments

Comments
 (0)