Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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