Skip to content

Commit de1d3b7

Browse files
committed
Automatically detecting, ordering and selecting complex types linked via a linking type in many-to-many relationships
1 parent d5a0466 commit de1d3b7

File tree

8 files changed

+166
-36
lines changed

8 files changed

+166
-36
lines changed

AgileMapper.UnitTests.Orms/WhenProjectingToEnumerableMembers.cs

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -157,9 +157,7 @@ public Task ShouldProjectViaLinkingType()
157157
var accountDto = context
158158
.Accounts
159159
.Project()
160-
.To<AccountDto>(cfg => cfg
161-
.Map(a => a.DeliveryAddresses.OrderBy(da => da.AddressId).Select(da => da.Address))
162-
.To(dto => dto.DeliveryAddresses))
160+
.To<AccountDto>()
163161
.ShouldHaveSingleItem();
164162

165163
accountDto.Id.ShouldBe(account.Id);

AgileMapper.UnitTests/Members/WhenDeterminingATypeIdentifier.cs

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
namespace AgileObjects.AgileMapper.UnitTests.Members
22
{
3-
using AgileMapper.Members;
43
using TestClasses;
54
using Xunit;
65

@@ -11,7 +10,7 @@ public void ShouldUseAnIdProperty()
1110
{
1211
DefaultMapperContext
1312
.Naming
14-
.GetIdentifierOrNull(TypeKey.ForTypeId(new { Id = "blahblahblah" }.GetType()))
13+
.GetIdentifierOrNull(new { Id = "blahblahblah" }.GetType())
1514
.ShouldNotBeNull();
1615
}
1716

@@ -20,7 +19,7 @@ public void ShouldUseAnIdentifierProperty()
2019
{
2120
DefaultMapperContext
2221
.Naming
23-
.GetIdentifierOrNull(TypeKey.ForTypeId(new { Identifier = "lalalala" }.GetType()))
22+
.GetIdentifierOrNull(new { Identifier = "lalalala" }.GetType())
2423
.ShouldNotBeNull();
2524
}
2625

@@ -29,7 +28,7 @@ public void ShouldUseATypeIdProperty()
2928
{
3029
DefaultMapperContext
3130
.Naming
32-
.GetIdentifierOrNull(TypeKey.ForTypeId(typeof(Person)))
31+
.GetIdentifierOrNull(typeof(Person))
3332
.ShouldNotBeNull();
3433
}
3534

@@ -38,7 +37,7 @@ public void ShouldReturnNullIfNoIdentifier()
3837
{
3938
DefaultMapperContext
4039
.Naming
41-
.GetIdentifierOrNull(TypeKey.ForTypeId(new { NoIdHere = true }.GetType()))
40+
.GetIdentifierOrNull(new { NoIdHere = true }.GetType())
4241
.ShouldBeNull();
4342
}
4443
}

AgileMapper/DataSources/EnumerableMappingDataSource.cs

Lines changed: 129 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,13 @@
11
namespace AgileObjects.AgileMapper.DataSources
22
{
3+
using System;
4+
using System.Linq;
35
using System.Linq.Expressions;
6+
using Extensions.Internal;
47
using Members;
8+
using NetStandardPolyfills;
59
using ObjectPopulation;
10+
using ObjectPopulation.Enumerables;
611

712
internal class EnumerableMappingDataSource : DataSourceBase
813
{
@@ -21,13 +26,135 @@ private static Expression GetMapping(
2126
int dataSourceIndex,
2227
IChildMemberMappingData enumerableMappingData)
2328
{
29+
var sourceEnumerable = GetSourceEnumerable(sourceEnumerableDataSource, enumerableMappingData);
30+
var sourceMember = sourceEnumerableDataSource.SourceMember.WithType(sourceEnumerable.Type);
31+
2432
var mapping = MappingFactory.GetChildMapping(
25-
sourceEnumerableDataSource.SourceMember,
26-
sourceEnumerableDataSource.Value,
33+
sourceMember,
34+
sourceEnumerable,
2735
dataSourceIndex,
2836
enumerableMappingData);
2937

3038
return mapping;
3139
}
40+
41+
private static Expression GetSourceEnumerable(
42+
IDataSource sourceEnumerableDataSource,
43+
IChildMemberMappingData enumerableMappingData)
44+
{
45+
var mapperData = enumerableMappingData.MapperData;
46+
var sourceElementType = sourceEnumerableDataSource.Value.Type.GetEnumerableElementType();
47+
48+
if (IsNotMappingFromLinkingType(sourceElementType, enumerableMappingData, out var forwardLink))
49+
{
50+
return sourceEnumerableDataSource.Value;
51+
}
52+
53+
var linkParameter = Parameters.Create(sourceElementType);
54+
55+
var orderedLinks = GetLinkOrdering(
56+
sourceEnumerableDataSource.Value,
57+
linkParameter,
58+
forwardLink,
59+
mapperData);
60+
61+
var sourceEnumerable = GetForwardLinkSelection(
62+
orderedLinks,
63+
linkParameter,
64+
forwardLink);
65+
66+
return sourceEnumerable;
67+
}
68+
69+
private static bool IsNotMappingFromLinkingType(
70+
Type sourceElementType,
71+
IChildMemberMappingData enumerableMappingData,
72+
out Member forwardLink)
73+
{
74+
var mapperData = enumerableMappingData.MapperData;
75+
76+
if ((sourceElementType == mapperData.TargetMember.ElementType) ||
77+
!sourceElementType.IsComplex() ||
78+
(mapperData.MapperContext.Naming.GetIdentifierOrNull(sourceElementType) != null))
79+
{
80+
forwardLink = null;
81+
return true;
82+
}
83+
84+
var sourceElementMembers = GlobalContext.Instance
85+
.MemberCache
86+
.GetSourceMembers(sourceElementType)
87+
.ToArray();
88+
89+
var backLinkMember = sourceElementMembers
90+
.FirstOrDefault(m => m.IsComplex && m.Type == mapperData.SourceType);
91+
92+
if (backLinkMember == null)
93+
{
94+
forwardLink = null;
95+
return true;
96+
}
97+
98+
var otherComplexTypeMembers = sourceElementMembers
99+
.Where(m => m.IsComplex && m.Type != mapperData.SourceType)
100+
.ToArray();
101+
102+
if (otherComplexTypeMembers.Length != 1)
103+
{
104+
forwardLink = null;
105+
return true;
106+
}
107+
108+
forwardLink = otherComplexTypeMembers[0];
109+
return false;
110+
}
111+
112+
private static Expression GetForwardLinkSelection(
113+
Expression sourceEnumerable,
114+
ParameterExpression linkParameter,
115+
Member forwardLink)
116+
{
117+
var funcTypes = new[] { linkParameter.Type, forwardLink.Type };
118+
var forwardLinkAccess = forwardLink.GetAccess(linkParameter);
119+
120+
var forwardLinkLambda = Expression.Lambda(
121+
Expression.GetFuncType(funcTypes),
122+
forwardLinkAccess,
123+
linkParameter);
124+
125+
return Expression.Call(
126+
EnumerablePopulationBuilder
127+
.EnumerableSelectWithoutIndexMethod
128+
.MakeGenericMethod(funcTypes),
129+
sourceEnumerable,
130+
forwardLinkLambda);
131+
}
132+
133+
private static Expression GetLinkOrdering(
134+
Expression sourceEnumerable,
135+
ParameterExpression linkParameter,
136+
Member forwardLink,
137+
IMemberMapperData mapperData)
138+
{
139+
var orderMember =
140+
mapperData.MapperContext.Naming.GetIdentifierOrNull(forwardLink.Type)?.MemberInfo ??
141+
linkParameter.Type.GetPublicInstanceMember("Order");
142+
143+
if (orderMember == null)
144+
{
145+
return sourceEnumerable;
146+
}
147+
148+
var orderMemberAccess = Expression.MakeMemberAccess(
149+
(orderMember.DeclaringType != linkParameter.Type)
150+
? forwardLink.GetAccess(linkParameter)
151+
: linkParameter,
152+
orderMember);
153+
154+
return sourceEnumerable.WithOrderingLinqCall(
155+
nameof(Enumerable.OrderBy),
156+
linkParameter,
157+
orderMemberAccess);
158+
}
32159
}
33160
}

AgileMapper/DataSources/Finders/MetaMemberDataSourceFinder.cs

Lines changed: 6 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@
44
using System.Collections.Generic;
55
using System.Linq;
66
using System.Linq.Expressions;
7-
using System.Reflection;
87
using Extensions.Internal;
98
using Members;
109
using NetStandardPolyfills;
@@ -493,36 +492,21 @@ private Expression GetOrderedEnumerableAccess(Expression enumerableAccess, Enume
493492
var orderMember =
494493
elementType.GetPublicInstanceMember("Order") ??
495494
elementType.GetPublicInstanceMember("DateCreated") ??
496-
MapperData.MapperContext.Naming.GetIdentifierOrNull(TypeKey.ForTypeId(elementType))?.MemberInfo;
495+
MapperData.MapperContext.Naming.GetIdentifierOrNull(elementType)?.MemberInfo;
497496

498497
if (orderMember == null)
499498
{
500499
return GetLinqMethodCall(LinqSelectionMethodName, enumerableAccess, helper);
501500
}
502501

503-
enumerableAccess = GetOrderingCall(enumerableAccess, orderMember);
504-
505-
return GetLinqMethodCall(nameof(Enumerable.FirstOrDefault), enumerableAccess, helper);
506-
}
507-
508-
private Expression GetOrderingCall(Expression enumerableAccess, MemberInfo orderMember)
509-
{
510502
var element = Parameters.Create(_sourceMember.Type);
511-
var orderAccess = Expression.MakeMemberAccess(element, orderMember);
512-
513-
var orderingMethod = typeof(Enumerable)
514-
.GetPublicStaticMethod(LinqOrderingMethodName, parameterCount: 2)
515-
.MakeGenericMethod(element.Type, orderAccess.Type);
516503

517-
var orderLambda = Expression.Lambda(
518-
Expression.GetFuncType(element.Type, orderAccess.Type),
519-
orderAccess,
520-
element);
504+
enumerableAccess = enumerableAccess.WithOrderingLinqCall(
505+
LinqOrderingMethodName,
506+
element,
507+
Expression.MakeMemberAccess(element, orderMember));
521508

522-
return Expression.Call(
523-
orderingMethod,
524-
enumerableAccess,
525-
orderLambda);
509+
return GetLinqMethodCall(nameof(Enumerable.FirstOrDefault), enumerableAccess, helper);
526510
}
527511

528512
protected abstract Expression GetIndex(Expression enumerableAccess);

AgileMapper/Extensions/Internal/ExpressionExtensions.cs

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -175,7 +175,7 @@ public static Expression GetCount(
175175
if (collectionAccess.Type.IsAssignableTo(collectionType))
176176
{
177177
return Expression.Property(
178-
collectionAccess,
178+
collectionAccess,
179179
collectionType.GetPublicInstanceProperty("Count"));
180180
}
181181

@@ -230,6 +230,26 @@ public static bool IsLinqToArrayOrToListCall(this MethodCallExpression call)
230230
ReferenceEquals(call.Method, _linqToArrayMethod));
231231
}
232232

233+
public static Expression WithOrderingLinqCall(
234+
this Expression enumerable,
235+
string orderingMethodName,
236+
ParameterExpression element,
237+
Expression orderMemberAccess)
238+
{
239+
var funcTypes = new[] { element.Type, orderMemberAccess.Type };
240+
241+
var orderingMethod = typeof(Enumerable)
242+
.GetPublicStaticMethod(orderingMethodName, parameterCount: 2)
243+
.MakeGenericMethod(funcTypes);
244+
245+
var orderLambda = Expression.Lambda(
246+
Expression.GetFuncType(funcTypes),
247+
orderMemberAccess,
248+
element);
249+
250+
return Expression.Call(orderingMethod, enumerable, orderLambda);
251+
}
252+
233253
public static Expression WithToArrayLinqCall(this Expression enumerable, Type elementType)
234254
=> GetToEnumerableCall(enumerable, _linqToArrayMethod, elementType);
235255

AgileMapper/Members/MemberIdentifierSet.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ private void ThrowIfIdentifierIsRedundant(Type type, LambdaExpression idMember)
3939
return;
4040
}
4141

42-
var defaultIdentifier = _mapperContext.Naming.GetIdentifierOrNull(TypeKey.ForTypeId(type));
42+
var defaultIdentifier = _mapperContext.Naming.GetIdentifierOrNull(type);
4343

4444
if (defaultIdentifier.Name != idMember.Body.GetMemberName())
4545
{

AgileMapper/Members/MemberMapperDataExtensions.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ public static bool IsEntity(this IMemberMapperData mapperData, Type type, out Me
3232
idMember = mapperData
3333
.MapperContext
3434
.Naming
35-
.GetIdentifierOrNull(TypeKey.ForTypeId(type));
35+
.GetIdentifierOrNull(type);
3636

3737
return idMember?.IsEntityId() == true;
3838
}

AgileMapper/Members/NamingSettings.cs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -140,7 +140,9 @@ private static Exception CreateConfigurationException(string pattern)
140140
}
141141

142142
private bool IsTypeIdentifier(Member member)
143-
=> GetIdentifierOrNull(TypeKey.ForTypeId(member.DeclaringType))?.Equals(member) == true;
143+
=> GetIdentifierOrNull(member.DeclaringType)?.Equals(member) == true;
144+
145+
public Member GetIdentifierOrNull(Type type) => GetIdentifierOrNull(TypeKey.ForTypeId(type));
144146

145147
public Member GetIdentifierOrNull(TypeKey typeIdKey)
146148
{

0 commit comments

Comments
 (0)