Skip to content

Commit 7168113

Browse files
Merge pull request #651 from johelvisguzman/GH-565
(GH-565) Add support for fetching nested properties for enumerables
2 parents 3eec608 + 09c838e commit 7168113

File tree

9 files changed

+668
-190
lines changed

9 files changed

+668
-190
lines changed

src/DotNetToolkit.Repository.InMemory/README.md

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,7 @@
11
# Disclaimer
22
This is intended for testing purposes only and should not be used in production code.
33

4-
# Issues
5-
There is currently an issue with join nested properties that has not yet been resolved, and because of this, it is not recommended to use this context in production code. For more info, see related issue [#556](https://github.com/johelvisguzman/DotNetToolkit.Repository/issues/565)
6-
74
# How to Use?
8-
95
```csharp
106
var options = new RepositoryOptionsBuilder()
117
.UseInMemoryDatabase(...) // for an in-memory database (for testing purposes only)
Lines changed: 85 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
namespace DotNetToolkit.Repository.Configuration.Conventions.Internal
22
{
3-
using Extensions;
43
using Extensions.Internal;
54
using JetBrains.Annotations;
65
using Properties;
@@ -14,63 +13,96 @@
1413

1514
internal class ForeignKeyConventionHelper
1615
{
17-
private static readonly ConcurrentDictionary<PropertyInfo, PropertyInfo[]> _foreignKeyCache = new ConcurrentDictionary<PropertyInfo, PropertyInfo[]>();
16+
private static readonly ConcurrentDictionary<PropertyInfo, Result> _foreignKeyCache = new ConcurrentDictionary<PropertyInfo, Result>();
1817

19-
public static PropertyInfo[] GetForeignKeyPropertyInfos([NotNull] PropertyInfo pi)
18+
public static Result GetForeignKeyPropertyInfos([NotNull] PropertyInfo pi)
2019
{
2120
Guard.NotNull(pi, nameof(pi));
22-
23-
if (!_foreignKeyCache.TryGetValue(pi, out PropertyInfo[] result))
21+
22+
if (!_foreignKeyCache.TryGetValue(pi, out var result))
2423
{
2524
result = GetForeignKeyPropertyInfosCore(pi);
2625
_foreignKeyCache.TryAdd(pi, result);
2726
}
28-
27+
2928
return result;
3029
}
3130

32-
private static PropertyInfo[] GetForeignKeyPropertyInfosCore(PropertyInfo pi)
31+
private static Result GetForeignKeyPropertyInfosCore(PropertyInfo pi)
3332
{
3433
var foreignType = pi.PropertyType.GetGenericTypeOrDefault();
3534
var declaringType = pi.DeclaringType;
3635

3736
if (foreignType.IsEnumerable() || declaringType.IsEnumerable())
38-
return new PropertyInfo[0];
37+
return null;
3938

40-
PropertyInfo[] propertyInfos = new PropertyInfo[0];
39+
bool foundInSource;
4140

42-
if (TryGetForeignKeyPropertyInfos(foreignType, declaringType, out var foreignKeyPropertyInfosFromTarget, out _))
41+
if (TryGetForeignKeyPropertyInfos(foreignType, declaringType,
42+
out var foreignKeyPropertyInfos,
43+
out var foreignNavPropertyInfo,
44+
out var adjacentNavPropertyInfo))
45+
{
46+
foundInSource = false;
47+
adjacentNavPropertyInfo = pi;
48+
}
49+
else if (TryGetForeignKeyPropertyInfos(declaringType, foreignType,
50+
out foreignKeyPropertyInfos,
51+
out foreignNavPropertyInfo,
52+
out adjacentNavPropertyInfo))
4353
{
44-
propertyInfos = foreignKeyPropertyInfosFromTarget;
54+
foundInSource = true;
4555
}
46-
else if (TryGetForeignKeyPropertyInfos(declaringType, foreignType, out _, out var primaryKeyPropertyInfosFromSource))
56+
else
4757
{
48-
propertyInfos = primaryKeyPropertyInfosFromSource;
58+
return null;
4959
}
5060

51-
return propertyInfos;
61+
var rightNavPi = pi;
62+
var rightPiType = rightNavPi.PropertyType.GetGenericTypeOrDefault();
63+
var rightKeysToJoinOn = foundInSource
64+
? PrimaryKeyConventionHelper.GetPrimaryKeyPropertyInfos(rightPiType)
65+
: foreignKeyPropertyInfos;
66+
67+
var leftNavPi = foreignNavPropertyInfo;
68+
var leftPiType = leftNavPi.PropertyType.GetGenericTypeOrDefault();
69+
var leftKeysToJoinOn = foundInSource
70+
? foreignKeyPropertyInfos
71+
: PrimaryKeyConventionHelper.GetPrimaryKeyPropertyInfos(leftPiType);
72+
73+
var newLeftNavPi = foundInSource ? adjacentNavPropertyInfo : leftNavPi;
74+
75+
return new Result(newLeftNavPi, leftKeysToJoinOn, rightNavPi, rightKeysToJoinOn);
5276
}
5377

54-
private static bool TryGetForeignKeyPropertyInfos(Type source, Type target, out PropertyInfo[] foreignKeyPropertyInfosFromTarget, out PropertyInfo[] primaryKeyPropertyInfosFromSource)
78+
private static bool TryGetForeignKeyPropertyInfos(Type source, Type target,
79+
out PropertyInfo[] foreignKeyPropertyInfos,
80+
out PropertyInfo foreignNavPropertyInfo,
81+
out PropertyInfo adjacentNavPropertyInfo)
5582
{
56-
var properties = source.GetRuntimeProperties().Where(ModelConventionHelper.IsColumnMapped).ToList();
57-
var foreignNavigationPropertyInfo = properties.SingleOrDefault(x => x.PropertyType == target);
58-
var propertyInfos = new List<PropertyInfo>();
83+
var propsFromSource = source.GetRuntimeProperties().Where(ModelConventionHelper.IsColumnMapped).ToList();
84+
var propsFromTarget = target.GetRuntimeProperties().Where(ModelConventionHelper.IsColumnMapped).ToList();
85+
86+
var foreignNavPi = propsFromSource.FirstOrDefault(x => x.PropertyType == target);
87+
var adjacentNavPi = propsFromTarget.FirstOrDefault(x => x.PropertyType == source);
5988

60-
foreignKeyPropertyInfosFromTarget = null;
61-
primaryKeyPropertyInfosFromSource = null;
89+
var foreignKeyPiList = new List<PropertyInfo>();
6290

63-
if (foreignNavigationPropertyInfo != null)
91+
adjacentNavPropertyInfo = null;
92+
foreignKeyPropertyInfos = null;
93+
foreignNavPropertyInfo = null;
94+
95+
if (foreignNavPi != null)
6496
{
65-
// Gets by checking the annotations
66-
var propertyInfosWithForeignKeys = properties.Where(x => x.GetCustomAttribute<ForeignKeyAttribute>() != null).ToList();
97+
// Gets by checking the annotations in source
98+
var propertyInfosWithForeignKeys = propsFromSource.Where(x => x.GetCustomAttribute<ForeignKeyAttribute>() != null).ToList();
6799
if (propertyInfosWithForeignKeys.Any())
68100
{
69101
// Ensure that the foreign key names are valid
70102
foreach (var propertyInfosWithForeignKey in propertyInfosWithForeignKeys)
71103
{
72104
var foreignKeyAttributeName = propertyInfosWithForeignKey.GetCustomAttribute<ForeignKeyAttribute>().Name;
73-
if (!properties.Any(x => foreignKeyAttributeName.Equals(ModelConventionHelper.GetColumnName(x))))
105+
if (!propsFromSource.Any(x => foreignKeyAttributeName.Equals(ModelConventionHelper.GetColumnName(x))))
74106
{
75107
throw new InvalidOperationException(
76108
string.Format(
@@ -82,49 +114,66 @@ private static bool TryGetForeignKeyPropertyInfos(Type source, Type target, out
82114
}
83115

84116
// Try to find by checking on the foreign key property
85-
propertyInfos = propertyInfosWithForeignKeys
117+
foreignKeyPiList = propertyInfosWithForeignKeys
86118
.Where(DotNetToolkit.Repository.Extensions.Internal.PropertyInfoExtensions.IsPrimitive)
87-
.Where(x => x.GetCustomAttribute<ForeignKeyAttribute>().Name.Equals(foreignNavigationPropertyInfo.Name))
119+
.Where(x => x.GetCustomAttribute<ForeignKeyAttribute>().Name.Equals(foreignNavPi.Name))
88120
.ToList();
89121

90122
// Try to find by checking on the navigation property
91-
if (!propertyInfos.Any())
123+
if (!foreignKeyPiList.Any())
92124
{
93-
propertyInfos = properties
125+
foreignKeyPiList = propsFromSource
94126
.Where(DotNetToolkit.Repository.Extensions.Internal.PropertyInfoExtensions.IsPrimitive)
95-
.Where(x => foreignNavigationPropertyInfo.GetCustomAttribute<ForeignKeyAttribute>().Name.Equals(ModelConventionHelper.GetColumnName(x)))
127+
.Where(x => foreignNavPi.GetCustomAttribute<ForeignKeyAttribute>().Name.Equals(ModelConventionHelper.GetColumnName(x)))
96128
.ToList();
97129
}
98130
}
99131

100132
// Try to find by naming convention
101133
var primaryKeyPropertyInfos = PrimaryKeyConventionHelper.GetPrimaryKeyPropertyInfos(target);
102134

103-
if (!propertyInfos.Any() && primaryKeyPropertyInfos.Any())
135+
if (!foreignKeyPiList.Any() && primaryKeyPropertyInfos.Any())
104136
{
105137
foreach (var primaryKeyPropertyInfo in primaryKeyPropertyInfos)
106138
{
107139
var foreignPrimaryKeyName = ModelConventionHelper.GetColumnName(primaryKeyPropertyInfo);
108-
var propertyName = $"{foreignNavigationPropertyInfo.Name}{foreignPrimaryKeyName}";
109-
var propertyInfo = properties.FirstOrDefault(x => x.Name == propertyName);
140+
var propertyName = $"{foreignNavPi.Name}{foreignPrimaryKeyName}";
141+
var propertyInfo = propsFromSource.FirstOrDefault(x => x.Name == propertyName);
110142

111143
if (propertyInfo != null)
112144
{
113-
propertyInfos.Add(propertyInfo);
145+
foreignKeyPiList.Add(propertyInfo);
114146
}
115147
}
116148
}
117149

118-
if (propertyInfos.Any())
150+
if (foreignKeyPiList.Any())
119151
{
120-
foreignKeyPropertyInfosFromTarget = propertyInfos.ToArray();
121-
primaryKeyPropertyInfosFromSource = primaryKeyPropertyInfos;
152+
foreignKeyPropertyInfos = foreignKeyPiList.ToArray();
153+
foreignNavPropertyInfo = foreignNavPi;
154+
adjacentNavPropertyInfo = adjacentNavPi;
122155

123156
return true;
124157
}
125158
}
126159

127160
return false;
128161
}
162+
163+
public class Result
164+
{
165+
public PropertyInfo LeftNavPi { get; }
166+
public PropertyInfo[] LeftKeysToJoinOn { get; }
167+
public PropertyInfo RightNavPi { get; }
168+
public PropertyInfo[] RightKeysToJoinOn { get; }
169+
170+
public Result(PropertyInfo leftNavPi, PropertyInfo[] leftKeysToJoinOn, PropertyInfo rightNavPi, PropertyInfo[] rightKeysToJoinOn)
171+
{
172+
LeftNavPi = leftNavPi;
173+
LeftKeysToJoinOn = leftKeysToJoinOn;
174+
RightNavPi = rightNavPi;
175+
RightKeysToJoinOn = rightKeysToJoinOn;
176+
}
177+
}
129178
}
130179
}
Lines changed: 3 additions & 128 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,10 @@
11
namespace DotNetToolkit.Repository.Extensions
22
{
33
using Configuration.Conventions;
4-
using Configuration.Conventions.Internal;
5-
using Extensions.Internal;
64
using JetBrains.Annotations;
75
using Query;
86
using Query.Strategies;
97
using System;
10-
using System.Collections;
118
using System.Collections.Generic;
129
using System.Linq;
1310
using System.Reflection;
@@ -18,9 +15,6 @@
1815
/// </summary>
1916
public static class EnumerableExtensions
2017
{
21-
private static readonly MethodInfo _castMethod = typeof(Enumerable).GetMethod(nameof(Enumerable.Cast));
22-
private static readonly MethodInfo _toListMethod = typeof(Enumerable).GetMethod(nameof(Enumerable.ToList));
23-
2418
/// <summary>
2519
/// Apply a specification strategy options to the specified entity's query.
2620
/// </summary>
@@ -107,120 +101,15 @@ public static IEnumerable<T> ApplyFetchingOptions<T>([NotNull] this IEnumerable<
107101
Guard.NotNull(query, nameof(query));
108102
Guard.NotNull(innerQueryCallback, nameof(innerQueryCallback));
109103

110-
var mainTableType = typeof(T);
111-
var mainTablePropertiesMap = mainTableType.GetRuntimeProperties().ToDictionary(x => x.Name);
112104
var fetchingPaths = fetchStrategy.DefaultIfFetchStrategyEmpty().PropertyPaths.ToList();
105+
var fetchHelper = new FetchHelper<T>(innerQueryCallback);
113106

114-
foreach (var path in fetchingPaths)
115-
{
116-
// Only do a join when the primary table has a foreign key property for the join table
117-
var joinTablePropertyInfo = mainTablePropertiesMap[path];
118-
var joinTableForeignKeyPropertyInfos = ForeignKeyConventionHelper.GetForeignKeyPropertyInfos(joinTablePropertyInfo);
119-
120-
if (joinTableForeignKeyPropertyInfos != null && joinTableForeignKeyPropertyInfos.Length > 0)
121-
{
122-
var joinTableType = joinTablePropertyInfo.PropertyType.TryGetGenericTypeOrDefault(out bool isJoinPropertyCollection);
123-
var innerQuery = innerQueryCallback(joinTableType);
124-
125-
var mainTablePrimaryKeyPropertyInfos = PrimaryKeyConventionHelper.GetPrimaryKeyPropertyInfos(mainTableType);
126-
var mainTablePropertyInfo = joinTableType.GetRuntimeProperties().FirstOrDefault(x => x.PropertyType == mainTableType);
127-
128-
var comparer = new ObjectArrayComparer();
129-
130-
// key selector functions
131-
object[] outerKeySelectorFunc(T outer)
132-
{
133-
return mainTablePrimaryKeyPropertyInfos.Select(pi => pi.GetValue(outer)).ToArray();
134-
}
135-
136-
object[] innerKeySelectorFunc(object inner)
137-
{
138-
return joinTableForeignKeyPropertyInfos.Select(pi => pi.GetValue(inner)).ToArray();
139-
}
140-
141-
if (isJoinPropertyCollection)
142-
{
143-
query = query.GroupJoin(
144-
innerQuery,
145-
outerKeySelectorFunc,
146-
innerKeySelectorFunc,
147-
(outer, inner) =>
148-
{
149-
if (inner != null)
150-
{
151-
var items = inner.Select(item =>
152-
{
153-
if (mainTablePropertyInfo != null)
154-
{
155-
// Sets the main table property in the join table
156-
mainTablePropertyInfo.SetValue(item, outer);
157-
}
158-
159-
return item;
160-
}).ToList(joinTableType);
161-
162-
// Sets the join table property in the main table
163-
joinTablePropertyInfo.SetValue(outer, items);
164-
}
165-
166-
return outer;
167-
}, comparer);
168-
}
169-
else
170-
{
171-
query = query.LeftJoin(
172-
innerQuery,
173-
outerKeySelectorFunc,
174-
innerKeySelectorFunc,
175-
(outer, inner) =>
176-
{
177-
if (inner != null)
178-
{
179-
if (mainTablePropertyInfo != null)
180-
{
181-
// Sets the main table property in the join table
182-
mainTablePropertyInfo.SetValue(inner, outer);
183-
}
184-
185-
// Sets the join table property in the main table
186-
joinTablePropertyInfo.SetValue(outer, inner);
187-
}
188-
189-
return outer;
190-
}, comparer);
191-
}
192-
}
193-
}
107+
if (fetchingPaths.Any())
108+
query = fetchingPaths.Aggregate(query, (current, path) => fetchHelper.Include(current, path));
194109

195110
return query;
196111
}
197112

198-
private static ICollection ToList(this IEnumerable items, Type type)
199-
{
200-
var castItems = _castMethod
201-
.MakeGenericMethod(new Type[] { type })
202-
.Invoke(null, new object[] { items });
203-
204-
var list = _toListMethod
205-
.MakeGenericMethod(new Type[] { type })
206-
.Invoke(null, new object[] { castItems });
207-
208-
return (ICollection)list;
209-
}
210-
211-
private static IEnumerable<TResult> LeftJoin<TOuter, TInner, TKey, TResult>(this IEnumerable<TOuter> outer, IEnumerable<TInner> inner, Func<TOuter, TKey> outerKeySelector, Func<TInner, TKey> innerKeySelector, Func<TOuter, TInner, TResult> resultSelector, IEqualityComparer<TKey> comparer)
212-
{
213-
return outer.GroupJoin(
214-
inner,
215-
outerKeySelector,
216-
innerKeySelector,
217-
(o, i) => new { outer = o, innerCollection = i },
218-
comparer)
219-
.SelectMany(
220-
a => a.innerCollection.DefaultIfEmpty(),
221-
(a, i) => resultSelector(a.outer, i));
222-
}
223-
224113
private static IOrderedEnumerable<T> ApplyOrder<T>(this IEnumerable<T> source, string propertyName, string methodName)
225114
{
226115
Guard.NotNull(source, nameof(source));
@@ -262,19 +151,5 @@ private static IOrderedEnumerable<T> ThenByDescending<T>(this IOrderedEnumerable
262151
{
263152
return ApplyOrder<T>(source, propertyName, nameof(Enumerable.ThenByDescending));
264153
}
265-
266-
private class ObjectArrayComparer : IEqualityComparer<object[]>
267-
{
268-
public bool Equals(object[] x, object[] y)
269-
{
270-
return x.Length == y.Length && Enumerable.SequenceEqual(x, y);
271-
}
272-
273-
public int GetHashCode(object[] o)
274-
{
275-
var result = o.Aggregate((a, b) => a.GetHashCode() ^ b.GetHashCode());
276-
return result.GetHashCode();
277-
}
278-
}
279154
}
280155
}

0 commit comments

Comments
 (0)