Skip to content

Commit dada7dc

Browse files
committed
Make sure to return only the projected fields #55
1 parent d247434 commit dada7dc

File tree

2 files changed

+108
-28
lines changed

2 files changed

+108
-28
lines changed

NorthwindCRUD/Controllers/QueryBuilderController.cs

Lines changed: 21 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ namespace QueryBuilder;
44
using Microsoft.AspNetCore.Mvc;
55
using Newtonsoft.Json;
66
using NorthwindCRUD;
7+
using NorthwindCRUD.Models.DbModels;
78
using NorthwindCRUD.Models.Dtos;
89
using System.Diagnostics.CodeAnalysis;
910
using System.Globalization;
@@ -51,19 +52,26 @@ public ActionResult<QueryBuilderResult> ExecuteQuery(Query query)
5152
var sanitizedEntity = query.Entity.Replace("\r", string.Empty).Replace("\n", string.Empty);
5253
logger.LogInformation("Executing query for entity: {Entity}", sanitizedEntity);
5354
var t = query.Entity.ToLower(CultureInfo.InvariantCulture);
54-
return Ok(new QueryBuilderResult
55+
return Ok(new Dictionary<string, object[]?>
5556
{
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,
57+
{
58+
t,
59+
t switch
60+
{
61+
"addresses" => dataContext.Addresses.Run<AddressDb, AddressDto>(query, mapper),
62+
"categories" => dataContext.Categories.Run<CategoryDb, CategoryDto>(query, mapper),
63+
"products" => dataContext.Products.Run<ProductDb, ProductDto>(query, mapper),
64+
"regions" => dataContext.Regions.Run<RegionDb, RegionDto>(query, mapper),
65+
"territories" => dataContext.Territories.Run<TerritoryDb, TerritoryDto>(query, mapper),
66+
"employees" => dataContext.Employees.Run<EmployeeDb, EmployeeDto>(query, mapper),
67+
"customers" => dataContext.Customers.Run<CustomerDb, CustomerDto>(query, mapper),
68+
"orders" => dataContext.Orders.Run<OrderDb, OrderDto>(query, mapper),
69+
"orderdetails" => dataContext.OrderDetails.Run<OrderDetailDb, OrderDetailDto>(query, mapper),
70+
"shippers" => dataContext.Shippers.Run<ShipperDb, ShipperDto>(query, mapper),
71+
"suppliers" => dataContext.Suppliers.Run<SupplierDb, SupplierDto>(query, mapper),
72+
_ => throw new InvalidOperationException($"Unknown entity ${t}")
73+
}
74+
},
6775
});
6876
}
69-
}
77+
}

NorthwindCRUD/QueryBuilder/QueryExecutor.cs

Lines changed: 87 additions & 15 deletions
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
{
@@ -36,12 +44,24 @@ private static IQueryable<TEntity> BuildQuery<TEntity>(DataContext db, IQueryabl
3644
var filteredQuery = source.Where(filterExpression);
3745
if (query.ReturnFields != null && query.ReturnFields.Any())
3846
{
39-
var projectionExpression = BuildProjectionExpression<TEntity>(query.ReturnFields);
40-
return filteredQuery.Select(projectionExpression).Cast<TEntity>();
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)
59+
{
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
}
@@ -247,17 +267,69 @@ private static Expression GetEmptyValue(Type targetType)
247267
return Expression.Constant(targetType == typeof(string) ? string.Empty : targetType.GetDefaultValue());
248268
}
249269

250-
private static Expression<Func<TEntity, dynamic>> BuildProjectionExpression<TEntity>(string[] returnFields)
270+
private static Expression<Func<TSource, object>> BuildProjectionExpression<TSource, TTarget>(string[] returnFields)
251271
{
252-
var parameter = Expression.Parameter(typeof(TEntity), "entity");
253-
var bindings = returnFields.Select(field =>
272+
var tagetEntityType = typeof(TTarget);
273+
var dbEntityType = typeof(TSource);
274+
275+
// Create the anonymous projection type
276+
var projectionType = CreateProjectionType(tagetEntityType, returnFields);
277+
278+
var parameter = Expression.Parameter(dbEntityType, "entity");
279+
280+
var bindings = returnFields.Select(fieldName =>
254281
{
255-
var property = typeof(TEntity).GetProperty(field, BindingFlags.IgnoreCase | BindingFlags.Public | BindingFlags.Instance) ?? throw new InvalidOperationException($"Property '{field}' not found on type '{typeof(TEntity)}'");
282+
var property = dbEntityType.GetProperty(fieldName, BindingFlags.IgnoreCase | BindingFlags.Public | BindingFlags.Instance) ?? throw new InvalidOperationException($"Property '{fieldName}' not found on type '{dbEntityType}");
283+
var field = projectionType.GetField(fieldName, BindingFlags.IgnoreCase | BindingFlags.Public | BindingFlags.Instance) ?? throw new InvalidOperationException($"Property '{fieldName}' not found on type '{projectionType}'");
256284
var propertyAccess = Expression.Property(parameter, property);
257-
return Expression.Bind(property, propertyAccess);
285+
return Expression.Bind(field, propertyAccess);
258286
}).ToArray();
259287

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

0 commit comments

Comments
 (0)