|
1 | 1 | using System;
|
| 2 | +using System.Collections; |
2 | 3 | using System.Collections.Generic;
|
3 | 4 | using System.Linq;
|
4 | 5 | using System.Linq.Expressions;
|
5 | 6 | using System.Reflection;
|
6 | 7 | using JsonApiDotNetCore.Internal;
|
7 | 8 | using JsonApiDotNetCore.Internal.Query;
|
| 9 | +using JsonApiDotNetCore.Models; |
8 | 10 | using JsonApiDotNetCore.Services;
|
9 | 11 |
|
10 | 12 | namespace JsonApiDotNetCore.Extensions
|
@@ -297,29 +299,111 @@ private static IQueryable<TSource> CallGenericWhereMethod<TSource>(IQueryable<TS
|
297 | 299 | }
|
298 | 300 |
|
299 | 301 | public static IQueryable<TSource> Select<TSource>(this IQueryable<TSource> source, List<string> columns)
|
300 |
| - { |
301 |
| - if (columns == null || columns.Count == 0) |
302 |
| - return source; |
| 302 | + => CallGenericSelectMethod(source, columns); |
303 | 303 |
|
304 |
| - var sourceType = source.ElementType; |
| 304 | + private static IQueryable<TSource> CallGenericSelectMethod<TSource>(IQueryable<TSource> source, List<string> columns) |
| 305 | + { |
| 306 | + var sourceBindings = new List<MemberAssignment>(); |
| 307 | + var sourceType = typeof(TSource); |
| 308 | + var parameter = Expression.Parameter(source.ElementType, "x"); |
| 309 | + var sourceProperties = new List<string>() { }; |
| 310 | + |
| 311 | + // Store all property names to it's own related property (name as key) |
| 312 | + var nestedTypesAndProperties = new Dictionary<string, List<string>>(); |
| 313 | + foreach (var column in columns) |
| 314 | + { |
| 315 | + var props = column.Split('.'); |
| 316 | + if (props.Length > 1) // Nested property |
| 317 | + { |
| 318 | + if (nestedTypesAndProperties.TryGetValue(props[0], out var properties) == false) |
| 319 | + nestedTypesAndProperties.Add(props[0], new List<string>() { nameof(Identifiable.Id), props[1] }); |
| 320 | + else |
| 321 | + properties.Add(props[1]); |
| 322 | + } |
| 323 | + else |
| 324 | + sourceProperties.Add(props[0]); |
| 325 | + } |
305 | 326 |
|
306 |
| - var resultType = typeof(TSource); |
| 327 | + // Bind attributes on TSource |
| 328 | + sourceBindings = sourceProperties.Select(prop => Expression.Bind(sourceType.GetProperty(prop), Expression.PropertyOrField(parameter, prop))).ToList(); |
307 | 329 |
|
308 |
| - // {model} |
309 |
| - var parameter = Expression.Parameter(sourceType, "model"); |
310 |
| - |
311 |
| - var bindings = columns.Select(column => Expression.Bind( |
312 |
| - resultType.GetProperty(column), Expression.PropertyOrField(parameter, column))); |
| 330 | + // Bind attributes on nested types |
| 331 | + var nestedBindings = new List<MemberAssignment>(); |
| 332 | + Expression bindExpression; |
| 333 | + foreach (var item in nestedTypesAndProperties) |
| 334 | + { |
| 335 | + var nestedProperty = sourceType.GetProperty(item.Key); |
| 336 | + var nestedPropertyType = nestedProperty.PropertyType; |
| 337 | + // [HasMany] attribute |
| 338 | + if (nestedPropertyType != typeof(string) && typeof(IEnumerable).IsAssignableFrom(nestedPropertyType)) |
| 339 | + { |
| 340 | + // Concrete type of Collection |
| 341 | + var singleType = nestedPropertyType.GetGenericArguments().Single(); |
| 342 | + // {y} |
| 343 | + var nestedParameter = Expression.Parameter(singleType, "y"); |
| 344 | + nestedBindings = item.Value.Select(prop => Expression.Bind( |
| 345 | + singleType.GetProperty(prop), Expression.PropertyOrField(nestedParameter, prop))).ToList(); |
| 346 | + |
| 347 | + // { new Item() } |
| 348 | + var newNestedExp = Expression.New(singleType); |
| 349 | + var initNestedExp = Expression.MemberInit(newNestedExp, nestedBindings); |
| 350 | + // { y => new Item() {Id = y.Id, Name = y.Name}} |
| 351 | + var body = Expression.Lambda(initNestedExp, nestedParameter); |
| 352 | + // { x.Items } |
| 353 | + Expression propertyExpression = Expression.Property(parameter, nestedProperty.Name); |
| 354 | + // { x.Items.Select(y => new Item() {Id = y.Id, Name = y.Name}) } |
| 355 | + Expression selectMethod = Expression.Call( |
| 356 | + typeof(Enumerable), |
| 357 | + "Select", |
| 358 | + new Type[] { singleType, singleType }, |
| 359 | + propertyExpression, body); |
| 360 | + |
| 361 | + // { x.Items.Select(y => new Item() {Id = y.Id, Name = y.Name}).ToList() } |
| 362 | + bindExpression = Expression.Call( |
| 363 | + typeof(Enumerable), |
| 364 | + "ToList", |
| 365 | + new Type[] { singleType }, |
| 366 | + selectMethod); |
| 367 | + } |
| 368 | + // [HasOne] attribute |
| 369 | + else |
| 370 | + { |
| 371 | + // {x.Owner} |
| 372 | + var srcBody = Expression.PropertyOrField(parameter, item.Key); |
| 373 | + foreach (var nested in item.Value) |
| 374 | + { |
| 375 | + // {x.Owner.Name} |
| 376 | + var nestedBody = Expression.PropertyOrField(srcBody, nested); |
| 377 | + var propInfo = nestedPropertyType.GetProperty(nested); |
| 378 | + nestedBindings.Add(Expression.Bind(propInfo, nestedBody)); |
| 379 | + } |
| 380 | + // { new Owner() } |
| 381 | + var newExp = Expression.New(nestedPropertyType); |
| 382 | + // { new Owner() { Id = x.Owner.Id, Name = x.Owner.Name }} |
| 383 | + var newInit = Expression.MemberInit(newExp, nestedBindings); |
| 384 | + |
| 385 | + // Handle nullable relationships |
| 386 | + // { Owner = x.Owner == null ? null : new Owner() {...} } |
| 387 | + bindExpression = Expression.Condition( |
| 388 | + Expression.Equal(srcBody, Expression.Constant(null)), |
| 389 | + Expression.Convert(Expression.Constant(null), nestedPropertyType), |
| 390 | + newInit |
| 391 | + ); |
| 392 | + } |
313 | 393 |
|
314 |
| - // { new Model () { Property = model.Property } } |
315 |
| - var body = Expression.MemberInit(Expression.New(resultType), bindings); |
| 394 | + sourceBindings.Add(Expression.Bind(nestedProperty, bindExpression)); |
| 395 | + nestedBindings.Clear(); |
| 396 | + } |
316 | 397 |
|
317 |
| - // { model => new TodoItem() { Property = model.Property } } |
318 |
| - var selector = Expression.Lambda(body, parameter); |
| 398 | + var sourceInit = Expression.MemberInit(Expression.New(sourceType), sourceBindings); |
| 399 | + var finalBody = Expression.Lambda(sourceInit, parameter); |
319 | 400 |
|
320 |
| - return source.Provider.CreateQuery<TSource>( |
321 |
| - Expression.Call(typeof(Queryable), "Select", new[] { sourceType, resultType }, |
322 |
| - source.Expression, Expression.Quote(selector))); |
| 401 | + return source.Provider.CreateQuery<TSource>(Expression.Call( |
| 402 | + typeof(Queryable), |
| 403 | + "Select", |
| 404 | + new[] { source.ElementType, typeof(TSource) }, |
| 405 | + source.Expression, |
| 406 | + Expression.Quote(finalBody))); |
323 | 407 | }
|
324 | 408 |
|
325 | 409 | public static IQueryable<T> PageForward<T>(this IQueryable<T> source, int pageSize, int pageNumber)
|
|
0 commit comments