Skip to content

Commit d4a2f79

Browse files
feat(literal-lists): including literal types for non-navigation/simple members - adding tests
AutoMapper#196
1 parent c7ae880 commit d4a2f79

File tree

6 files changed

+238
-24
lines changed

6 files changed

+238
-24
lines changed

AutoMapper.AspNetCore.OData.EFCore/QueryableExtensions.cs

+184-7
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,12 @@
55
using Microsoft.EntityFrameworkCore.Query;
66
using System;
77
using System.Collections.Generic;
8+
using System.Globalization;
89
using System.Linq;
910
using System.Linq.Expressions;
1011
using System.Threading;
1112
using System.Threading.Tasks;
13+
using LogicBuilder.Expressions.Utils;
1214

1315
namespace AutoMapper.AspNet.OData
1416
{
@@ -133,19 +135,24 @@ private static IQueryable<TModel> GetQueryable<TModel, TData>(this IQueryable<TD
133135

134136
var expansions = options.SelectExpand.GetExpansions(typeof(TModel));
135137

136-
var selects = options.SelectExpand.GetSelects();
137-
var literalLists = typeof(TModel).GetLiteralLists();
138-
var includes = selects.Union(literalLists).ToList();
138+
var includeProperties = expansions
139+
.Select(list => new List<Expansion>(list))
140+
.BuildIncludes<TModel>(options.SelectExpand.GetSelects());
139141

142+
var includeLiteralLists = expansions
143+
.Select(list => new List<Expansion>(list))
144+
.BuildWithLiteralLists<TModel>(options.SelectExpand.GetSelects());
145+
146+
var includes = includeProperties
147+
.UnionBy(includeLiteralLists, e => e.Body.ToString())
148+
.ToList();
149+
140150
return query.GetQuery
141151
(
142152
mapper,
143153
filter,
144154
options.GetQueryableExpression(querySettings?.ODataSettings),
145-
expansions
146-
.Select(list => new List<Expansion>(list))
147-
.BuildIncludes<TModel>(includes)
148-
.ToList(),
155+
includes,
149156
querySettings?.ProjectionSettings
150157
).UpdateQueryableExpression(expansions, options.Context);
151158
}
@@ -205,5 +212,175 @@ private static long QueryLongCount<TModel, TData>(this IQueryable<TData> query,
205212
(
206213
mapper.MapExpression<Expression<Func<TData, bool>>>(modelFilter)
207214
);
215+
216+
private static ICollection<Expression<Func<TSource, object>>> BuildWithLiteralLists<TSource>(this IEnumerable<List<Expansion>> includes, List<string> selects)
217+
where TSource : class
218+
{
219+
return GetAllExpansions(new List<LambdaExpression>());
220+
221+
List<Expression<Func<TSource, object>>> GetAllExpansions(List<LambdaExpression> valueMemberSelectors)
222+
{
223+
string parameterName = "i";
224+
ParameterExpression param = Expression.Parameter(typeof(TSource), parameterName);
225+
226+
valueMemberSelectors.AddSelectors(selects, param, param);
227+
228+
return includes
229+
.Select(include => BuildSelectorExpression<TSource>(include, valueMemberSelectors, parameterName))
230+
.Concat(valueMemberSelectors.Select(selector => (Expression<Func<TSource, object>>)selector))
231+
.ToList();
232+
}
233+
}
234+
235+
private static Expression<Func<TSource, object>> BuildSelectorExpression<TSource>(List<Expansion> fullName, List<LambdaExpression> valueMemberSelectors, string parameterName = "i")
236+
{
237+
ParameterExpression param = Expression.Parameter(typeof(TSource), parameterName);
238+
239+
return (Expression<Func<TSource, object>>)Expression.Lambda
240+
(
241+
typeof(Func<,>).MakeGenericType(new[] { param.Type, typeof(object) }),
242+
BuildSelectorExpression(param, fullName, valueMemberSelectors, parameterName),
243+
param
244+
);
245+
}
246+
247+
// e.g. /opstenant?$top=5&$expand=Buildings($expand=Builder($expand=City))
248+
private static Expression BuildSelectorExpression(Expression sourceExpression, List<Expansion> parts, List<LambdaExpression> valueMemberSelectors, string parameterName = "i")
249+
{
250+
Expression parent = sourceExpression;
251+
252+
//Arguments to create a nested expression when the parent expansion is a collection
253+
//See AddChildSeelctors() below
254+
List<LambdaExpression> childValueMemberSelectors = new List<LambdaExpression>();
255+
256+
for (int i = 0; i < parts.Count; i++)
257+
{
258+
if (parent.Type.IsList())
259+
{
260+
Expression selectExpression = GetSelectExpression
261+
(
262+
parts.Skip(i),
263+
parent,
264+
childValueMemberSelectors,
265+
parameterName
266+
);
267+
268+
AddChildSeelctors();
269+
270+
return selectExpression;
271+
}
272+
else
273+
{
274+
parent = Expression.MakeMemberAccess(parent, parent.Type.GetMemberInfo(parts[i].MemberName));
275+
276+
if (parent.Type.IsList())
277+
{
278+
ParameterExpression childParam = Expression.Parameter(parent.GetUnderlyingElementType(), parameterName.ChildParameterName());
279+
//selectors from an underlying list element must be added here.
280+
childValueMemberSelectors.AddSelectors
281+
(
282+
parts[i].Selects,
283+
childParam,
284+
childParam
285+
);
286+
}
287+
else
288+
{
289+
valueMemberSelectors.AddSelectors(parts[i].Selects, Expression.Parameter(sourceExpression.Type, parameterName), parent);
290+
}
291+
}
292+
}
293+
294+
AddChildSeelctors();
295+
296+
return parent;
297+
298+
//Adding childValueMemberSelectors created above and in a the recursive call:
299+
//i0 => i0.Builder.Name becomes
300+
//i => i.Buildings.Select(i0 => i0.Builder.Name)
301+
void AddChildSeelctors()
302+
{
303+
childValueMemberSelectors.ForEach(selector =>
304+
{
305+
valueMemberSelectors.Add(Expression.Lambda
306+
(
307+
typeof(Func<,>).MakeGenericType(new[] { sourceExpression.Type, typeof(object) }),
308+
Expression.Call
309+
(
310+
typeof(Enumerable),
311+
"Select",
312+
new Type[] { parent.GetUnderlyingElementType(), typeof(object) },
313+
parent,
314+
selector
315+
),
316+
Expression.Parameter(sourceExpression.Type, parameterName)
317+
));
318+
});
319+
}
320+
}
321+
322+
private static Expression GetSelectExpression(IEnumerable<Expansion> expansions, Expression parent, List<LambdaExpression> valueMemberSelectors, string parameterName)
323+
{
324+
ParameterExpression parameter = Expression.Parameter(parent.GetUnderlyingElementType(), parameterName.ChildParameterName());
325+
Expression selectorBody = BuildSelectorExpression(parameter, expansions.ToList(), valueMemberSelectors, parameter.Name);
326+
return Expression.Call
327+
(
328+
typeof(Enumerable),
329+
"Select",
330+
new Type[] { parameter.Type, selectorBody.Type },
331+
parent,
332+
Expression.Lambda
333+
(
334+
typeof(Func<,>).MakeGenericType(new[] { parameter.Type, selectorBody.Type }),
335+
selectorBody,
336+
parameter
337+
)
338+
);
339+
}
340+
341+
private static string ChildParameterName(this string currentParameterName)
342+
{
343+
string lastChar = currentParameterName.Substring(currentParameterName.Length - 1);
344+
if (short.TryParse(lastChar, out short lastCharShort))
345+
{
346+
return string.Concat
347+
(
348+
currentParameterName.Substring(0, currentParameterName.Length - 1),
349+
(lastCharShort++).ToString(CultureInfo.CurrentCulture)
350+
);
351+
}
352+
else
353+
{
354+
return currentParameterName += "0";
355+
}
356+
}
357+
358+
private static void AddSelectors(this List<LambdaExpression> valueMemberSelectors, List<string> selects, ParameterExpression param, Expression parentBody)
359+
{
360+
if (parentBody.Type.IsList() || parentBody.Type.IsLiteralType())
361+
return;
362+
363+
valueMemberSelectors.AddRange
364+
(
365+
parentBody.Type
366+
.GetSelectedMembers(selects)
367+
.Select(member => Expression.MakeMemberAccess(parentBody, member))
368+
.Select
369+
(
370+
selector => selector.Type.IsValueType
371+
? (Expression)Expression.Convert(selector, typeof(object))
372+
: selector
373+
)
374+
.Select
375+
(
376+
selector => Expression.Lambda
377+
(
378+
typeof(Func<,>).MakeGenericType(new[] { param.Type, typeof(object) }),
379+
selector,
380+
param
381+
)
382+
)
383+
);
384+
}
208385
}
209386
}

AutoMapper.AspNetCore.OData.EFCore/TypeExtensions.cs

+1-12
Original file line numberDiff line numberDiff line change
@@ -18,17 +18,6 @@ public static MemberInfo[] GetSelectedMembers(this Type parentType, List<string>
1818

1919
return selects.Select(select => parentType.GetMemberInfo(select)).ToArray();
2020
}
21-
22-
public static IEnumerable<string> GetLiteralLists(this Type type)
23-
{
24-
foreach (var member in type.GetMemberInfos())
25-
{
26-
if (member.MemberType is not (MemberTypes.Field or MemberTypes.Property)) continue;
27-
28-
if (member.GetMemberType().IsListLiteral())
29-
yield return member.Name;
30-
}
31-
}
3221

3322
private static MemberInfo[] GetValueTypeMembers(this Type parentType)
3423
{
@@ -38,7 +27,7 @@ private static MemberInfo[] GetValueTypeMembers(this Type parentType)
3827
return parentType.GetMemberInfos().Where
3928
(
4029
info => (info.MemberType == MemberTypes.Field || info.MemberType == MemberTypes.Property)
41-
&& info.GetMemberType().IsLiteralType()
30+
&& (info.GetMemberType().IsLiteralType() || info.GetMemberType() == typeof(byte[]) || info.GetMemberType().IsListLiteral())
4231
).ToArray();
4332
}
4433

AutoMapper.OData.EFCore.Tests/Data/DatabaseInitializer.cs

+5-5
Original file line numberDiff line numberDiff line change
@@ -28,8 +28,8 @@ public static void SeedDatabase(MyDbContext context)
2828
CreatedDate = new DateTime(2012, 12, 12),
2929
Buildings = new List<TBuilding>
3030
{
31-
new TBuilding { Identity = Guid.NewGuid(), LongName = "One L1", BuilderId = builders.First(b => b.Name == "Sam").Id },
32-
new TBuilding { Identity = Guid.NewGuid(), LongName = "One L2", BuilderId = builders.First(b => b.Name == "Sam").Id }
31+
new TBuilding { Identity = Guid.NewGuid(), LongName = "One L1", BuilderId = builders.First(b => b.Name == "Sam").Id, Parameters = new []{"Param A", "Param B"}},
32+
new TBuilding { Identity = Guid.NewGuid(), LongName = "One L2", BuilderId = builders.First(b => b.Name == "Sam").Id, Parameters = new []{"Param A", "Param B"}}
3333
}
3434
});
3535
context.MandatorSet.Add(new TMandator
@@ -39,9 +39,9 @@ public static void SeedDatabase(MyDbContext context)
3939
CreatedDate = new DateTime(2012, 12, 12),
4040
Buildings = new List<TBuilding>
4141
{
42-
new TBuilding { Identity = Guid.NewGuid(), LongName = "Two L1", BuilderId = builders.First(b => b.Name == "John").Id },
43-
new TBuilding { Identity = Guid.NewGuid(), LongName = "Two L2", BuilderId = builders.First(b => b.Name == "Mark").Id },
44-
new TBuilding { Identity = Guid.NewGuid(), LongName = "Two L3", BuilderId = builders.First(b => b.Name == "Mark").Id }
42+
new TBuilding { Identity = Guid.NewGuid(), LongName = "Two L1", BuilderId = builders.First(b => b.Name == "John").Id, Parameters = new []{"Param A", "Param B"} },
43+
new TBuilding { Identity = Guid.NewGuid(), LongName = "Two L2", BuilderId = builders.First(b => b.Name == "Mark").Id, Parameters = new []{"Param A", "Param B"} },
44+
new TBuilding { Identity = Guid.NewGuid(), LongName = "Two L3", BuilderId = builders.First(b => b.Name == "Mark").Id, Parameters = new []{"Param A", "Param B"} }
4545
}
4646
});
4747
context.SaveChanges();

AutoMapper.OData.EFCore.Tests/GetQueryTests.cs

+45
Original file line numberDiff line numberDiff line change
@@ -450,6 +450,23 @@ void Test(ICollection<CoreBuilding> collection)
450450
Assert.Equal("Two L3", collection.First().Name);
451451
}
452452
}
453+
454+
[Fact]
455+
public async void GetBuildings_ShouldReturnParametersLiteralList()
456+
{
457+
const string query = "/corebuilding";
458+
Test(Get<CoreBuilding, TBuilding>(query));
459+
Test(await GetAsync<CoreBuilding, TBuilding>(query));
460+
Test(await GetUsingCustomNameSpace<CoreBuilding, TBuilding>(query));
461+
462+
return;
463+
464+
void Test(ICollection<CoreBuilding> collection)
465+
{
466+
Assert.Equal(5, collection.Count);
467+
Assert.True(collection.All(coreBuilding => coreBuilding.Parameters.Length is 2));
468+
}
469+
}
453470

454471
[Fact]
455472
public async void BuildingExpandBuilderTenantExpandCityFilterOnNestedNestedPropertyWithCount()
@@ -588,6 +605,34 @@ void Test(ICollection<CoreBuilding> collection)
588605
Assert.Equal(5, collection.Count);
589606
}
590607
}
608+
609+
[Fact]
610+
public async void BuildingSelectName_OtherPropertiesShouldNotBeUnset()
611+
{
612+
const string query = "/corebuilding?$select=Name";
613+
Test(Get<CoreBuilding, TBuilding>(query));
614+
Test(await GetAsync<CoreBuilding, TBuilding>(query));
615+
Test(await GetUsingCustomNameSpace<CoreBuilding, TBuilding>(query));
616+
617+
return;
618+
619+
void Test(ICollection<CoreBuilding> collection)
620+
{
621+
Assert.Multiple(() =>
622+
{
623+
Assert.Equal(5, collection.Count);
624+
foreach (var coreBuilding in collection)
625+
{
626+
Assert.NotNull(coreBuilding.Name);
627+
Assert.Null(coreBuilding.Parameters);
628+
Assert.Null(coreBuilding.Parameter);
629+
Assert.Null(coreBuilding.Tenant);
630+
Assert.Null(coreBuilding.Builder);
631+
Assert.Equivalent(Guid.Empty, coreBuilding.Identity);
632+
}
633+
});
634+
}
635+
}
591636

592637
[Fact]
593638
public async void BuildingWithoutTopAndPageSize()

DAL.EFCore/TBuilding.cs

+2
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,8 @@ public class TBuilding
2525
public TBuilder Builder { get; set; }
2626

2727
public TMandator Mandator { get; set; }
28+
29+
public string[] Parameters { get; set; }
2830

2931
[ForeignKey("Mandator")]
3032
[Column("fkMandatorID")]

Domain.OData/CoreBuilding.cs

+1
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ public class CoreBuilding : BaseCoreBuilding
1010
public OpsBuilder Builder { get; set; }
1111
public OpsTenant Tenant { get; set; }
1212
public string Parameter { get; set; }
13+
public string[] Parameters { get; set; }
1314
}
1415

1516
public abstract class BaseCoreBuilding

0 commit comments

Comments
 (0)