diff --git a/NorthwindCRUD/Controllers/OrdersController.cs b/NorthwindCRUD/Controllers/OrdersController.cs index acd1e5f..039d9cd 100644 --- a/NorthwindCRUD/Controllers/OrdersController.cs +++ b/NorthwindCRUD/Controllers/OrdersController.cs @@ -222,9 +222,9 @@ public ActionResult 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) { diff --git a/NorthwindCRUD/Controllers/QueryBuilderController.cs b/NorthwindCRUD/Controllers/QueryBuilderController.cs index 8d8263a..e66140b 100644 --- a/NorthwindCRUD/Controllers/QueryBuilderController.cs +++ b/NorthwindCRUD/Controllers/QueryBuilderController.cs @@ -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; } @@ -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; @@ -51,19 +50,26 @@ public ActionResult 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 { - Addresses = t == "addresses" ? mapper.Map(dataContext.Addresses.Run(query)) : null, - Categories = t == "categories" ? mapper.Map(dataContext.Categories.Run(query)) : null, - Products = t == "products" ? mapper.Map(dataContext.Products.Run(query)) : null, - Regions = t == "regions" ? mapper.Map(dataContext.Regions.Run(query)) : null, - Territories = t == "territories" ? mapper.Map(dataContext.Territories.Run(query)) : null, - Employees = t == "employees" ? mapper.Map(dataContext.Employees.Run(query)) : null, - Customers = t == "customers" ? mapper.Map(dataContext.Customers.Run(query)) : null, - Orders = t == "orders" ? mapper.Map(dataContext.Orders.Run(query)) : null, - OrderDetails = t == "orderdetails" ? mapper.Map(dataContext.OrderDetails.Run(query)) : null, - Shippers = t == "shippers" ? mapper.Map(dataContext.Shippers.Run(query)) : null, - Suppliers = t == "suppliers" ? mapper.Map(dataContext.Suppliers.Run(query)) : null, + { + t, + t switch + { + "addresses" => dataContext.Addresses.Run(query, mapper), + "categories" => dataContext.Categories.Run(query, mapper), + "products" => dataContext.Products.Run(query, mapper), + "regions" => dataContext.Regions.Run(query, mapper), + "territories" => dataContext.Territories.Run(query, mapper), + "employees" => dataContext.Employees.Run(query, mapper), + "customers" => dataContext.Customers.Run(query, mapper), + "orders" => dataContext.Orders.Run(query, mapper), + "orderdetails" => dataContext.OrderDetails.Run(query, mapper), + "shippers" => dataContext.Shippers.Run(query, mapper), + "suppliers" => dataContext.Suppliers.Run(query, mapper), + _ => throw new InvalidOperationException($"Unknown entity {t}"), + } + }, }); } -} \ No newline at end of file +} diff --git a/NorthwindCRUD/Helpers/Enums.cs b/NorthwindCRUD/Helpers/Enums.cs deleted file mode 100644 index 73f5d12..0000000 --- a/NorthwindCRUD/Helpers/Enums.cs +++ /dev/null @@ -1,22 +0,0 @@ -using System.Runtime.Serialization; - -namespace NorthwindCRUD.Helpers -{ - public class Enums - { - public enum Shipping - { - [EnumMember(Value = "SeaFreight")] - SeaFreight, - - [EnumMember(Value = "GroundTransport")] - GroundTransport, - - [EnumMember(Value = "AirCargo")] - AirCargo, - - [EnumMember(Value = "Mail")] - Mail, - } - } -} diff --git a/NorthwindCRUD/Models/Contracts/IOrder.cs b/NorthwindCRUD/Models/Contracts/IOrder.cs index 2ef34b2..a427a8a 100644 --- a/NorthwindCRUD/Models/Contracts/IOrder.cs +++ b/NorthwindCRUD/Models/Contracts/IOrder.cs @@ -1,5 +1,5 @@ using NorthwindCRUD.Models.Dtos; -using static NorthwindCRUD.Helpers.Enums; +using NorthwindCRUD.Models.Enums; namespace NorthwindCRUD.Models.Contracts { diff --git a/NorthwindCRUD/Models/DbModels/OrderDb.cs b/NorthwindCRUD/Models/DbModels/OrderDb.cs index 94e3b41..0262fa9 100644 --- a/NorthwindCRUD/Models/DbModels/OrderDb.cs +++ b/NorthwindCRUD/Models/DbModels/OrderDb.cs @@ -1,5 +1,6 @@ using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations.Schema; +using NorthwindCRUD.Models.Enums; namespace NorthwindCRUD.Models.DbModels { @@ -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; } diff --git a/NorthwindCRUD/Models/Dtos/OrderDto.cs b/NorthwindCRUD/Models/Dtos/OrderDto.cs index f9ac40f..66a52bd 100644 --- a/NorthwindCRUD/Models/Dtos/OrderDto.cs +++ b/NorthwindCRUD/Models/Dtos/OrderDto.cs @@ -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 { diff --git a/NorthwindCRUD/Models/Enums/Enums.cs b/NorthwindCRUD/Models/Enums/Enums.cs new file mode 100644 index 0000000..dde328d --- /dev/null +++ b/NorthwindCRUD/Models/Enums/Enums.cs @@ -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, + } +} diff --git a/NorthwindCRUD/NorthwindCRUD.csproj b/NorthwindCRUD/NorthwindCRUD.csproj index d8276db..8ccbd76 100644 --- a/NorthwindCRUD/NorthwindCRUD.csproj +++ b/NorthwindCRUD/NorthwindCRUD.csproj @@ -39,7 +39,7 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/NorthwindCRUD/QueryBuilder/QueryExecutor.cs b/NorthwindCRUD/QueryBuilder/QueryExecutor.cs index 0b4faa8..68057d2 100644 --- a/NorthwindCRUD/QueryBuilder/QueryExecutor.cs +++ b/NorthwindCRUD/QueryBuilder/QueryExecutor.cs @@ -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; @@ -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(this IQueryable source, Query? query) + public static object[] Run(this IQueryable source, Query? query) + { + return source.Run(query); + } + + public static object[] Run(this IQueryable source, Query? query, IMapper? mapper = null) { var infrastructure = source as IInfrastructure; 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(); + return db is not null ? BuildQuery(db, source, query, mapper).ToArray() : Array.Empty(); } - private static IQueryable BuildQuery(DataContext db, IQueryable source, Query? query) + private static IQueryable BuildQuery(DataContext db, IQueryable source, Query? query, IMapper? mapper = null) { if (query is null) { @@ -34,14 +42,26 @@ private static IQueryable BuildQuery(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(query.ReturnFields); + return filteredQuery.ProjectTo(mapper.ConfigurationProvider).Select(projectionExpression); + } + else + { + var projectionExpression = BuildProjectionExpression(query.ReturnFields); + return filteredQuery.Select(projectionExpression); + } + } + else if (mapper is not null) { - var projectionExpression = BuildProjectionExpression(query.ReturnFields); - return filteredQuery.Select(projectionExpression).Cast(); + return (IQueryable)filteredQuery.ProjectTo(mapper.ConfigurationProvider); } else { - return filteredQuery; + return filteredQuery.Cast(); } } @@ -71,7 +91,7 @@ private static Expression> BuildExpression(DataCont private static Expression BuildConditionExpression(DataContext db, IQueryable 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}'"); @@ -117,7 +137,7 @@ private static Expression BuildConditionExpression(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 } @@ -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); } @@ -247,17 +273,69 @@ private static Expression GetEmptyValue(Type targetType) return Expression.Constant(targetType == typeof(string) ? string.Empty : targetType.GetDefaultValue()); } - private static Expression> BuildProjectionExpression(string[] returnFields) + private static Expression> BuildProjectionExpression(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>(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>(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 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; } } diff --git a/NorthwindCRUD/Services/OrderService.cs b/NorthwindCRUD/Services/OrderService.cs index 98892ef..ae29119 100644 --- a/NorthwindCRUD/Services/OrderService.cs +++ b/NorthwindCRUD/Services/OrderService.cs @@ -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(); }