Skip to content

Commit 6d7842d

Browse files
authored
Merge pull request #56 from IgniteUI/pmoleri/improve-qb-projection
Make sure to return only the projected fields #55
2 parents d247434 + b14b088 commit 6d7842d

File tree

10 files changed

+144
-62
lines changed

10 files changed

+144
-62
lines changed

Diff for: NorthwindCRUD/Controllers/OrdersController.cs

+2-2
Original file line numberDiff line numberDiff line change
@@ -222,9 +222,9 @@ public ActionResult<CustomerDto> GetShipperByOrderId(int id)
222222
try
223223
{
224224
var order = this.orderService.GetById(id);
225-
if (order != null)
225+
if (order?.ShipperId != null)
226226
{
227-
var shipper = this.shipperService.GetById(order.ShipVia);
227+
var shipper = this.shipperService.GetById(order.ShipperId.Value);
228228

229229
if (shipper != null)
230230
{

Diff for: NorthwindCRUD/Controllers/QueryBuilderController.cs

+23-17
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,17 @@
11
namespace QueryBuilder;
22

3+
using System.Diagnostics.CodeAnalysis;
4+
using System.Globalization;
35
using AutoMapper;
46
using Microsoft.AspNetCore.Mvc;
57
using Newtonsoft.Json;
68
using NorthwindCRUD;
9+
using NorthwindCRUD.Models.DbModels;
710
using NorthwindCRUD.Models.Dtos;
8-
using System.Diagnostics.CodeAnalysis;
9-
using System.Globalization;
1011

1112
[SuppressMessage("StyleCop.CSharp.LayoutRules", "SA1516:Elements should be separated by blank line", Justification = "...")]
1213
[SuppressMessage("StyleCop.CSharp.SpacingRules", "SA1025:Code should not contain multiple whitespace in a row", Justification = "...")]
1314
[SuppressMessage("StyleCop.CSharp.ReadabilityRules", "SA1134:Attributes should not share line", Justification = "...")]
14-
[SuppressMessage("StyleCop.CSharp.SpacingRules", "SA1011:Closing square brackets should be spaced correctly", Justification = "...")]
1515
public class QueryBuilderResult
1616
{
1717
[JsonProperty(NullValueHandling = NullValueHandling.Ignore)] public AddressDto[]? Addresses { get; set; }
@@ -29,7 +29,6 @@ public class QueryBuilderResult
2929

3030
[ApiController]
3131
[Route("[controller]")]
32-
[SuppressMessage("StyleCop.CSharp.SpacingRules", "SA1025:Code should not contain multiple whitespace in a row", Justification = "...")]
3332
public class QueryBuilderController : ControllerBase
3433
{
3534
private readonly DataContext dataContext;
@@ -51,19 +50,26 @@ public ActionResult<QueryBuilderResult> ExecuteQuery(Query query)
5150
var sanitizedEntity = query.Entity.Replace("\r", string.Empty).Replace("\n", string.Empty);
5251
logger.LogInformation("Executing query for entity: {Entity}", sanitizedEntity);
5352
var t = query.Entity.ToLower(CultureInfo.InvariantCulture);
54-
return Ok(new QueryBuilderResult
53+
return Ok(new Dictionary<string, object[]?>
5554
{
56-
Addresses = t == "addresses" ? mapper.Map<AddressDto[]>(dataContext.Addresses.Run(query)) : null,
57-
Categories = t == "categories" ? mapper.Map<CategoryDto[]>(dataContext.Categories.Run(query)) : null,
58-
Products = t == "products" ? mapper.Map<ProductDto[]>(dataContext.Products.Run(query)) : null,
59-
Regions = t == "regions" ? mapper.Map<RegionDto[]>(dataContext.Regions.Run(query)) : null,
60-
Territories = t == "territories" ? mapper.Map<TerritoryDto[]>(dataContext.Territories.Run(query)) : null,
61-
Employees = t == "employees" ? mapper.Map<EmployeeDto[]>(dataContext.Employees.Run(query)) : null,
62-
Customers = t == "customers" ? mapper.Map<CustomerDto[]>(dataContext.Customers.Run(query)) : null,
63-
Orders = t == "orders" ? mapper.Map<OrderDto[]>(dataContext.Orders.Run(query)) : null,
64-
OrderDetails = t == "orderdetails" ? mapper.Map<OrderDetailDto[]>(dataContext.OrderDetails.Run(query)) : null,
65-
Shippers = t == "shippers" ? mapper.Map<ShipperDto[]>(dataContext.Shippers.Run(query)) : null,
66-
Suppliers = t == "suppliers" ? mapper.Map<SupplierDto[]>(dataContext.Suppliers.Run(query)) : null,
55+
{
56+
t,
57+
t switch
58+
{
59+
"addresses" => dataContext.Addresses.Run<AddressDb, AddressDto>(query, mapper),
60+
"categories" => dataContext.Categories.Run<CategoryDb, CategoryDto>(query, mapper),
61+
"products" => dataContext.Products.Run<ProductDb, ProductDto>(query, mapper),
62+
"regions" => dataContext.Regions.Run<RegionDb, RegionDto>(query, mapper),
63+
"territories" => dataContext.Territories.Run<TerritoryDb, TerritoryDto>(query, mapper),
64+
"employees" => dataContext.Employees.Run<EmployeeDb, EmployeeDto>(query, mapper),
65+
"customers" => dataContext.Customers.Run<CustomerDb, CustomerDto>(query, mapper),
66+
"orders" => dataContext.Orders.Run<OrderDb, OrderDto>(query, mapper),
67+
"orderdetails" => dataContext.OrderDetails.Run<OrderDetailDb, OrderDetailDto>(query, mapper),
68+
"shippers" => dataContext.Shippers.Run<ShipperDb, ShipperDto>(query, mapper),
69+
"suppliers" => dataContext.Suppliers.Run<SupplierDb, SupplierDto>(query, mapper),
70+
_ => throw new InvalidOperationException($"Unknown entity {t}"),
71+
}
72+
},
6773
});
6874
}
69-
}
75+
}

Diff for: NorthwindCRUD/Helpers/Enums.cs

-22
This file was deleted.

Diff for: NorthwindCRUD/Models/Contracts/IOrder.cs

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
using NorthwindCRUD.Models.Dtos;
2-
using static NorthwindCRUD.Helpers.Enums;
2+
using NorthwindCRUD.Models.Enums;
33

44
namespace NorthwindCRUD.Models.Contracts
55
{

Diff for: NorthwindCRUD/Models/DbModels/OrderDb.cs

+2-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
using System.ComponentModel.DataAnnotations;
22
using System.ComponentModel.DataAnnotations.Schema;
3+
using NorthwindCRUD.Models.Enums;
34

45
namespace NorthwindCRUD.Models.DbModels
56
{
@@ -29,7 +30,7 @@ public OrderDb()
2930

3031
public string RequiredDate { get; set; }
3132

32-
public int ShipVia { get; set; }
33+
public Shipping ShipVia { get; set; }
3334

3435
public double Freight { get; set; }
3536

Diff for: NorthwindCRUD/Models/Dtos/OrderDto.cs

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
using System.ComponentModel.DataAnnotations;
22
using NorthwindCRUD.Models.Contracts;
3-
using static NorthwindCRUD.Helpers.Enums;
3+
using NorthwindCRUD.Models.Enums;
44

55
namespace NorthwindCRUD.Models.Dtos
66
{

Diff for: NorthwindCRUD/Models/Enums/Enums.cs

+19
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
using System.Runtime.Serialization;
2+
3+
namespace NorthwindCRUD.Models.Enums
4+
{
5+
public enum Shipping
6+
{
7+
[EnumMember(Value = "SeaFreight")]
8+
SeaFreight,
9+
10+
[EnumMember(Value = "GroundTransport")]
11+
GroundTransport,
12+
13+
[EnumMember(Value = "AirCargo")]
14+
AirCargo,
15+
16+
[EnumMember(Value = "Mail")]
17+
Mail,
18+
}
19+
}

Diff for: NorthwindCRUD/NorthwindCRUD.csproj

+1-1
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@
3939
</PackageReference>
4040
<PackageReference Include="Microsoft.VisualStudio.Web.CodeGeneration.Design" Version="6.0.10" />
4141
<PackageReference Include="Newtonsoft.Json" Version="13.0.1" />
42-
<PackageReference Include="StyleCop.Analyzers" Version="1.1.118">
42+
<PackageReference Include="StyleCop.Analyzers" Version="1.2.0-beta.556">
4343
<PrivateAssets>all</PrivateAssets>
4444
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
4545
</PackageReference>

Diff for: NorthwindCRUD/QueryBuilder/QueryExecutor.cs

+94-16
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,10 @@
44
using System.Globalization;
55
using System.Linq.Expressions;
66
using System.Reflection;
7+
using System.Reflection.Emit;
8+
using AutoMapper;
79
using AutoMapper.Internal;
10+
using AutoMapper.QueryableExtensions;
811
using Microsoft.EntityFrameworkCore;
912
using Microsoft.EntityFrameworkCore.Infrastructure;
1013
using NorthwindCRUD;
@@ -16,16 +19,21 @@
1619
[SuppressMessage("StyleCop.CSharp.SpacingRules", "SA1025:Code should not contain multiple whitespace in a row", Justification = "...")]
1720
public static class QueryExecutor
1821
{
19-
public static TEntity[] Run<TEntity>(this IQueryable<TEntity> source, Query? query)
22+
public static object[] Run<TEntity>(this IQueryable<TEntity> source, Query? query)
23+
{
24+
return source.Run<TEntity, TEntity>(query);
25+
}
26+
27+
public static object[] Run<TSource, TTarget>(this IQueryable<TSource> source, Query? query, IMapper? mapper = null)
2028
{
2129
var infrastructure = source as IInfrastructure<IServiceProvider>;
2230
var serviceProvider = infrastructure!.Instance;
2331
var currentDbContext = serviceProvider.GetService(typeof(ICurrentDbContext)) as ICurrentDbContext;
2432
var db = currentDbContext!.Context as DataContext;
25-
return db is not null ? BuildQuery(db, source, query).ToArray() : Array.Empty<TEntity>();
33+
return db is not null ? BuildQuery<TSource, TTarget>(db, source, query, mapper).ToArray() : Array.Empty<object>();
2634
}
2735

28-
private static IQueryable<TEntity> BuildQuery<TEntity>(DataContext db, IQueryable<TEntity> source, Query? query)
36+
private static IQueryable<object> BuildQuery<TSource, TTarget>(DataContext db, IQueryable<TSource> source, Query? query, IMapper? mapper = null)
2937
{
3038
if (query is null)
3139
{
@@ -34,14 +42,26 @@ private static IQueryable<TEntity> BuildQuery<TEntity>(DataContext db, IQueryabl
3442

3543
var filterExpression = BuildExpression(db, source, query.FilteringOperands, query.Operator);
3644
var filteredQuery = source.Where(filterExpression);
37-
if (query.ReturnFields != null && query.ReturnFields.Any())
45+
if (query.ReturnFields != null && query.ReturnFields.Any() && !query.ReturnFields.Contains("*"))
46+
{
47+
if (mapper is not null)
48+
{
49+
var projectionExpression = BuildProjectionExpression<TTarget, TTarget>(query.ReturnFields);
50+
return filteredQuery.ProjectTo<TTarget>(mapper.ConfigurationProvider).Select(projectionExpression);
51+
}
52+
else
53+
{
54+
var projectionExpression = BuildProjectionExpression<TSource, TTarget>(query.ReturnFields);
55+
return filteredQuery.Select(projectionExpression);
56+
}
57+
}
58+
else if (mapper is not null)
3859
{
39-
var projectionExpression = BuildProjectionExpression<TEntity>(query.ReturnFields);
40-
return filteredQuery.Select(projectionExpression).Cast<TEntity>();
60+
return (IQueryable<object>)filteredQuery.ProjectTo<TTarget>(mapper.ConfigurationProvider);
4161
}
4262
else
4363
{
44-
return filteredQuery;
64+
return filteredQuery.Cast<object>();
4565
}
4666
}
4767

@@ -71,7 +91,7 @@ private static Expression<Func<TEntity, bool>> BuildExpression<TEntity>(DataCont
7191

7292
private static Expression BuildConditionExpression<TEntity>(DataContext db, IQueryable<TEntity> source, QueryFilter filter, ParameterExpression parameter)
7393
{
74-
if (filter.FieldName is not null && filter.IgnoreCase is not null && filter.Condition is not null)
94+
if (filter.FieldName is not null && filter.Condition is not null)
7595
{
7696
var property = source.ElementType.GetProperty(filter.FieldName, BindingFlags.IgnoreCase | BindingFlags.Public | BindingFlags.Instance)
7797
?? throw new InvalidOperationException($"Property '{filter.FieldName}' not found on type '{source.ElementType}'");
@@ -117,7 +137,7 @@ private static Expression BuildConditionExpression<TEntity>(DataContext db, IQue
117137
"false" => Expression.Equal(field, Expression.Constant(false)),
118138
_ => throw new NotImplementedException("Not implemented"),
119139
};
120-
if (filter.IgnoreCase.Value && field.Type == typeof(string))
140+
if (filter.IgnoreCase == true && field.Type == typeof(string))
121141
{
122142
// TODO: Implement case-insensitive comparison
123143
}
@@ -238,6 +258,12 @@ private static Expression GetSearchValue(dynamic? value, Type targetType)
238258
}
239259

240260
var nonNullableType = Nullable.GetUnderlyingType(targetType) ?? targetType;
261+
262+
if (nonNullableType.IsEnum && value is string)
263+
{
264+
return Expression.Constant(Enum.Parse(nonNullableType, value));
265+
}
266+
241267
var convertedValue = Convert.ChangeType(value, nonNullableType, CultureInfo.InvariantCulture);
242268
return Expression.Constant(convertedValue, targetType);
243269
}
@@ -247,17 +273,69 @@ private static Expression GetEmptyValue(Type targetType)
247273
return Expression.Constant(targetType == typeof(string) ? string.Empty : targetType.GetDefaultValue());
248274
}
249275

250-
private static Expression<Func<TEntity, dynamic>> BuildProjectionExpression<TEntity>(string[] returnFields)
276+
private static Expression<Func<TSource, object>> BuildProjectionExpression<TSource, TTarget>(string[] returnFields)
251277
{
252-
var parameter = Expression.Parameter(typeof(TEntity), "entity");
253-
var bindings = returnFields.Select(field =>
278+
var tagetEntityType = typeof(TTarget);
279+
var dbEntityType = typeof(TSource);
280+
281+
// Create the anonymous projection type
282+
var projectionType = CreateProjectionType(tagetEntityType, returnFields);
283+
284+
var parameter = Expression.Parameter(dbEntityType, "entity");
285+
286+
var bindings = returnFields.Select(fieldName =>
254287
{
255-
var property = typeof(TEntity).GetProperty(field, BindingFlags.IgnoreCase | BindingFlags.Public | BindingFlags.Instance) ?? throw new InvalidOperationException($"Property '{field}' not found on type '{typeof(TEntity)}'");
288+
var property = dbEntityType.GetProperty(fieldName, BindingFlags.IgnoreCase | BindingFlags.Public | BindingFlags.Instance) ?? throw new InvalidOperationException($"Property '{fieldName}' not found on type '{dbEntityType}");
289+
var field = projectionType.GetField(fieldName, BindingFlags.IgnoreCase | BindingFlags.Public | BindingFlags.Instance) ?? throw new InvalidOperationException($"Property '{fieldName}' not found on type '{projectionType}'");
256290
var propertyAccess = Expression.Property(parameter, property);
257-
return Expression.Bind(property, propertyAccess);
291+
return Expression.Bind(field, propertyAccess);
258292
}).ToArray();
259293

260-
var body = Expression.MemberInit(Expression.New(typeof(TEntity)), bindings);
261-
return Expression.Lambda<Func<TEntity, dynamic>>(body, parameter);
294+
// Get Microsoft.CSharp assembly where anonymous types are defined
295+
var dynamicAssembly = typeof(Microsoft.CSharp.RuntimeBinder.Binder).Assembly;
296+
297+
var createExpression = Expression.MemberInit(Expression.New(projectionType), bindings);
298+
299+
return Expression.Lambda<Func<TSource, object>>(createExpression, parameter);
262300
}
301+
302+
private static Type CreateProjectionType(Type input, string[] fields)
303+
{
304+
var fieldsList = fields.Select(field =>
305+
{
306+
var property = input.GetProperty(field, BindingFlags.IgnoreCase | BindingFlags.Public | BindingFlags.Instance)
307+
?? throw new InvalidOperationException($"Property '{field}' not found on type '{input}'");
308+
return new Field
309+
{
310+
Name = property.Name,
311+
Type = property.GetMemberType(),
312+
};
313+
}).ToList();
314+
315+
var name = input.Name + "Projection";
316+
return CreateAnonymousType(name, fieldsList);
317+
}
318+
319+
private static Type CreateAnonymousType(string name, ICollection<Field> fields)
320+
{
321+
AssemblyName dynamicAssemblyName = new AssemblyName("TempAssembly");
322+
AssemblyBuilder dynamicAssembly = AssemblyBuilder.DefineDynamicAssembly(dynamicAssemblyName, AssemblyBuilderAccess.Run);
323+
ModuleBuilder dynamicModule = dynamicAssembly.DefineDynamicModule("TempAssembly");
324+
325+
TypeBuilder dynamicAnonymousType = dynamicModule.DefineType(name, TypeAttributes.Public);
326+
327+
foreach (var field in fields)
328+
{
329+
dynamicAnonymousType.DefineField(field.Name, field.Type, FieldAttributes.Public);
330+
}
331+
332+
return dynamicAnonymousType.CreateType()!;
333+
}
334+
}
335+
336+
internal class Field
337+
{
338+
public string Name { get; set; }
339+
340+
public Type Type { get; set; }
263341
}

Diff for: NorthwindCRUD/Services/OrderService.cs

+1-1
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,7 @@ public OrderDb[] GetOrdersByEmployeeId(int id)
7373
public OrderDb[] GetOrdersByShipperId(int id)
7474
{
7575
return GetOrdersQuery()
76-
.Where(o => o.ShipVia == id)
76+
.Where(o => o.ShipperId == id)
7777
.ToArray();
7878
}
7979

0 commit comments

Comments
 (0)