Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions NorthwindCRUD/Controllers/OrdersController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -222,9 +222,9 @@ public ActionResult<CustomerDto> GetShipperByOrderId(int id)
try
{
var order = this.orderService.GetById(id);
if (order != null)
if (order?.ShipperId != null)
{
var shipper = this.shipperService.GetById(order.ShipVia);
var shipper = this.shipperService.GetById(order.ShipperId.Value);

if (shipper != null)
{
Expand Down
40 changes: 23 additions & 17 deletions NorthwindCRUD/Controllers/QueryBuilderController.cs
Original file line number Diff line number Diff line change
@@ -1,17 +1,17 @@
namespace QueryBuilder;

using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using AutoMapper;
using Microsoft.AspNetCore.Mvc;
using Newtonsoft.Json;
using NorthwindCRUD;
using NorthwindCRUD.Models.DbModels;
using NorthwindCRUD.Models.Dtos;
using System.Diagnostics.CodeAnalysis;
using System.Globalization;

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

[ApiController]
[Route("[controller]")]
[SuppressMessage("StyleCop.CSharp.SpacingRules", "SA1025:Code should not contain multiple whitespace in a row", Justification = "...")]
public class QueryBuilderController : ControllerBase
{
private readonly DataContext dataContext;
Expand All @@ -51,19 +50,26 @@ public ActionResult<QueryBuilderResult> ExecuteQuery(Query query)
var sanitizedEntity = query.Entity.Replace("\r", string.Empty).Replace("\n", string.Empty);
logger.LogInformation("Executing query for entity: {Entity}", sanitizedEntity);
var t = query.Entity.ToLower(CultureInfo.InvariantCulture);
return Ok(new QueryBuilderResult
return Ok(new Dictionary<string, object[]?>
{
Addresses = t == "addresses" ? mapper.Map<AddressDto[]>(dataContext.Addresses.Run(query)) : null,
Categories = t == "categories" ? mapper.Map<CategoryDto[]>(dataContext.Categories.Run(query)) : null,
Products = t == "products" ? mapper.Map<ProductDto[]>(dataContext.Products.Run(query)) : null,
Regions = t == "regions" ? mapper.Map<RegionDto[]>(dataContext.Regions.Run(query)) : null,
Territories = t == "territories" ? mapper.Map<TerritoryDto[]>(dataContext.Territories.Run(query)) : null,
Employees = t == "employees" ? mapper.Map<EmployeeDto[]>(dataContext.Employees.Run(query)) : null,
Customers = t == "customers" ? mapper.Map<CustomerDto[]>(dataContext.Customers.Run(query)) : null,
Orders = t == "orders" ? mapper.Map<OrderDto[]>(dataContext.Orders.Run(query)) : null,
OrderDetails = t == "orderdetails" ? mapper.Map<OrderDetailDto[]>(dataContext.OrderDetails.Run(query)) : null,
Shippers = t == "shippers" ? mapper.Map<ShipperDto[]>(dataContext.Shippers.Run(query)) : null,
Suppliers = t == "suppliers" ? mapper.Map<SupplierDto[]>(dataContext.Suppliers.Run(query)) : null,
{
t,
t switch
{
"addresses" => dataContext.Addresses.Run<AddressDb, AddressDto>(query, mapper),
"categories" => dataContext.Categories.Run<CategoryDb, CategoryDto>(query, mapper),
"products" => dataContext.Products.Run<ProductDb, ProductDto>(query, mapper),
"regions" => dataContext.Regions.Run<RegionDb, RegionDto>(query, mapper),
"territories" => dataContext.Territories.Run<TerritoryDb, TerritoryDto>(query, mapper),
"employees" => dataContext.Employees.Run<EmployeeDb, EmployeeDto>(query, mapper),
"customers" => dataContext.Customers.Run<CustomerDb, CustomerDto>(query, mapper),
"orders" => dataContext.Orders.Run<OrderDb, OrderDto>(query, mapper),
"orderdetails" => dataContext.OrderDetails.Run<OrderDetailDb, OrderDetailDto>(query, mapper),
"shippers" => dataContext.Shippers.Run<ShipperDb, ShipperDto>(query, mapper),
"suppliers" => dataContext.Suppliers.Run<SupplierDb, SupplierDto>(query, mapper),
_ => throw new InvalidOperationException($"Unknown entity ${t}"),
}
},
});
}
}
}
22 changes: 0 additions & 22 deletions NorthwindCRUD/Helpers/Enums.cs

This file was deleted.

2 changes: 1 addition & 1 deletion NorthwindCRUD/Models/Contracts/IOrder.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
using NorthwindCRUD.Models.Dtos;
using static NorthwindCRUD.Helpers.Enums;
using NorthwindCRUD.Models.Enums;

namespace NorthwindCRUD.Models.Contracts
{
Expand Down
3 changes: 2 additions & 1 deletion NorthwindCRUD/Models/DbModels/OrderDb.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using NorthwindCRUD.Models.Enums;

namespace NorthwindCRUD.Models.DbModels
{
Expand Down Expand Up @@ -29,7 +30,7 @@ public OrderDb()

public string RequiredDate { get; set; }

public int ShipVia { get; set; }
public Shipping ShipVia { get; set; }

public double Freight { get; set; }

Expand Down
2 changes: 1 addition & 1 deletion NorthwindCRUD/Models/Dtos/OrderDto.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
using System.ComponentModel.DataAnnotations;
using NorthwindCRUD.Models.Contracts;
using static NorthwindCRUD.Helpers.Enums;
using NorthwindCRUD.Models.Enums;

namespace NorthwindCRUD.Models.Dtos
{
Expand Down
19 changes: 19 additions & 0 deletions NorthwindCRUD/Models/Enums/Enums.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
using System.Runtime.Serialization;

namespace NorthwindCRUD.Models.Enums
{
public enum Shipping
{
[EnumMember(Value = "SeaFreight")]
SeaFreight,

[EnumMember(Value = "GroundTransport")]
GroundTransport,

[EnumMember(Value = "AirCargo")]
AirCargo,

[EnumMember(Value = "Mail")]
Mail,
}
}
2 changes: 1 addition & 1 deletion NorthwindCRUD/NorthwindCRUD.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@
</PackageReference>
<PackageReference Include="Microsoft.VisualStudio.Web.CodeGeneration.Design" Version="6.0.10" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.1" />
<PackageReference Include="StyleCop.Analyzers" Version="1.1.118">
<PackageReference Include="StyleCop.Analyzers" Version="1.2.0-beta.556">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
Expand Down
110 changes: 94 additions & 16 deletions NorthwindCRUD/QueryBuilder/QueryExecutor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,10 @@
using System.Globalization;
using System.Linq.Expressions;
using System.Reflection;
using System.Reflection.Emit;
using AutoMapper;
using AutoMapper.Internal;
using AutoMapper.QueryableExtensions;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using NorthwindCRUD;
Expand All @@ -16,16 +19,21 @@
[SuppressMessage("StyleCop.CSharp.SpacingRules", "SA1025:Code should not contain multiple whitespace in a row", Justification = "...")]
public static class QueryExecutor
{
public static TEntity[] Run<TEntity>(this IQueryable<TEntity> source, Query? query)
public static object[] Run<TEntity>(this IQueryable<TEntity> source, Query? query)
{
return source.Run<TEntity, TEntity>(query);
}

public static object[] Run<TSource, TTarget>(this IQueryable<TSource> source, Query? query, IMapper? mapper = null)
{
var infrastructure = source as IInfrastructure<IServiceProvider>;
var serviceProvider = infrastructure!.Instance;
var currentDbContext = serviceProvider.GetService(typeof(ICurrentDbContext)) as ICurrentDbContext;
var db = currentDbContext!.Context as DataContext;
return db is not null ? BuildQuery(db, source, query).ToArray() : Array.Empty<TEntity>();
return db is not null ? BuildQuery<TSource, TTarget>(db, source, query, mapper).ToArray() : Array.Empty<object>();
}

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

var filterExpression = BuildExpression(db, source, query.FilteringOperands, query.Operator);
var filteredQuery = source.Where(filterExpression);
if (query.ReturnFields != null && query.ReturnFields.Any())
if (query.ReturnFields != null && query.ReturnFields.Any() && !query.ReturnFields.Contains("*"))
{
if (mapper is not null)
{
var projectionExpression = BuildProjectionExpression<TTarget, TTarget>(query.ReturnFields);
return filteredQuery.ProjectTo<TTarget>(mapper.ConfigurationProvider).Select(projectionExpression);
}
else
{
var projectionExpression = BuildProjectionExpression<TSource, TTarget>(query.ReturnFields);
return filteredQuery.Select(projectionExpression);
}
}
else if (mapper is not null)
{
var projectionExpression = BuildProjectionExpression<TEntity>(query.ReturnFields);
return filteredQuery.Select(projectionExpression).Cast<TEntity>();
return (IQueryable<object>)filteredQuery.ProjectTo<TTarget>(mapper.ConfigurationProvider);
}
else
{
return filteredQuery;
return filteredQuery.Cast<object>();
}
}

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

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

var nonNullableType = Nullable.GetUnderlyingType(targetType) ?? targetType;

if (nonNullableType.IsEnum && value is string)
{
return Expression.Constant(Enum.Parse(nonNullableType, value));
}

var convertedValue = Convert.ChangeType(value, nonNullableType, CultureInfo.InvariantCulture);
return Expression.Constant(convertedValue, targetType);
}
Expand All @@ -247,17 +273,69 @@ private static Expression GetEmptyValue(Type targetType)
return Expression.Constant(targetType == typeof(string) ? string.Empty : targetType.GetDefaultValue());
}

private static Expression<Func<TEntity, dynamic>> BuildProjectionExpression<TEntity>(string[] returnFields)
private static Expression<Func<TSource, object>> BuildProjectionExpression<TSource, TTarget>(string[] returnFields)
{
var parameter = Expression.Parameter(typeof(TEntity), "entity");
var bindings = returnFields.Select(field =>
var tagetEntityType = typeof(TTarget);
var dbEntityType = typeof(TSource);

// Create the anonymous projection type
var projectionType = CreateProjectionType(tagetEntityType, returnFields);

var parameter = Expression.Parameter(dbEntityType, "entity");

var bindings = returnFields.Select(fieldName =>
{
var property = typeof(TEntity).GetProperty(field, BindingFlags.IgnoreCase | BindingFlags.Public | BindingFlags.Instance) ?? throw new InvalidOperationException($"Property '{field}' not found on type '{typeof(TEntity)}'");
var property = dbEntityType.GetProperty(fieldName, BindingFlags.IgnoreCase | BindingFlags.Public | BindingFlags.Instance) ?? throw new InvalidOperationException($"Property '{fieldName}' not found on type '{dbEntityType}");
var field = projectionType.GetField(fieldName, BindingFlags.IgnoreCase | BindingFlags.Public | BindingFlags.Instance) ?? throw new InvalidOperationException($"Property '{fieldName}' not found on type '{projectionType}'");
var propertyAccess = Expression.Property(parameter, property);
return Expression.Bind(property, propertyAccess);
return Expression.Bind(field, propertyAccess);
}).ToArray();

var body = Expression.MemberInit(Expression.New(typeof(TEntity)), bindings);
return Expression.Lambda<Func<TEntity, dynamic>>(body, parameter);
// Get Microsoft.CSharp assembly where anonymous types are defined
var dynamicAssembly = typeof(Microsoft.CSharp.RuntimeBinder.Binder).Assembly;

var createExpression = Expression.MemberInit(Expression.New(projectionType), bindings);

return Expression.Lambda<Func<TSource, object>>(createExpression, parameter);
}

private static Type CreateProjectionType(Type input, string[] fields)
{
var fieldsList = fields.Select(field =>
{
var property = input.GetProperty(field, BindingFlags.IgnoreCase | BindingFlags.Public | BindingFlags.Instance)
?? throw new InvalidOperationException($"Property '{field}' not found on type '{input}'");
return new Field
{
Name = property.Name,
Type = property.GetMemberType(),
};
}).ToList();

var name = input.Name + "Projection";
return CreateAnonymousType(name, fieldsList);
}

private static Type CreateAnonymousType(string name, ICollection<Field> fields)
{
AssemblyName dynamicAssemblyName = new AssemblyName("TempAssembly");
AssemblyBuilder dynamicAssembly = AssemblyBuilder.DefineDynamicAssembly(dynamicAssemblyName, AssemblyBuilderAccess.Run);
ModuleBuilder dynamicModule = dynamicAssembly.DefineDynamicModule("TempAssembly");

TypeBuilder dynamicAnonymousType = dynamicModule.DefineType(name, TypeAttributes.Public);

foreach (var field in fields)
{
dynamicAnonymousType.DefineField(field.Name, field.Type, FieldAttributes.Public);
}

return dynamicAnonymousType.CreateType()!;
}
}

internal class Field
{
public string Name { get; set; }

public Type Type { get; set; }
}
2 changes: 1 addition & 1 deletion NorthwindCRUD/Services/OrderService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ public OrderDb[] GetOrdersByEmployeeId(int id)
public OrderDb[] GetOrdersByShipperId(int id)
{
return GetOrdersQuery()
.Where(o => o.ShipVia == id)
.Where(o => o.ShipperId == id)
.ToArray();
}

Expand Down