From b59fe69a333035392ef644c35cf37118f0664f3d Mon Sep 17 00:00:00 2001 From: Javier Coitino Date: Wed, 16 Oct 2024 18:28:22 -0300 Subject: [PATCH 01/41] Query Builder POC --- .../Controllers/QueryBuilderController.cs | 80 +++++++++++ NorthwindCRUD/Controllers/QueryExecutor.cs | 132 ++++++++++++++++++ 2 files changed, 212 insertions(+) create mode 100644 NorthwindCRUD/Controllers/QueryBuilderController.cs create mode 100644 NorthwindCRUD/Controllers/QueryExecutor.cs diff --git a/NorthwindCRUD/Controllers/QueryBuilderController.cs b/NorthwindCRUD/Controllers/QueryBuilderController.cs new file mode 100644 index 0000000..96448e5 --- /dev/null +++ b/NorthwindCRUD/Controllers/QueryBuilderController.cs @@ -0,0 +1,80 @@ +namespace QueryBuilder; + +using Microsoft.AspNetCore.Mvc; +using Newtonsoft.Json; +using NorthwindCRUD; +using NorthwindCRUD.Models.DbModels; + +public class QueryBuilderResult +{ + [JsonProperty(NullValueHandling = NullValueHandling.Ignore)] + public AddressDb[] Addresses { get; set; } + + [JsonProperty(NullValueHandling = NullValueHandling.Ignore)] + public CategoryDb[] Categories { get; set; } + + [JsonProperty(NullValueHandling = NullValueHandling.Ignore)] + public ProductDb[] Products { get; set; } + + [JsonProperty(NullValueHandling = NullValueHandling.Ignore)] + public RegionDb[] Regions { get; set; } + + [JsonProperty(NullValueHandling = NullValueHandling.Ignore)] + public TerritoryDb[] Territories { get; set; } + + [JsonProperty(NullValueHandling = NullValueHandling.Ignore)] + public EmployeeDb[] Employees { get; set; } + + [JsonProperty(NullValueHandling = NullValueHandling.Ignore)] + public CustomerDb[] Customers { get; set; } + + [JsonProperty(NullValueHandling = NullValueHandling.Ignore)] + public OrderDb[] Orders { get; set; } + + [JsonProperty(NullValueHandling = NullValueHandling.Ignore)] + public OrderDetailDb[] OrderDetails { get; set; } + + [JsonProperty(NullValueHandling = NullValueHandling.Ignore)] + public ShipperDb[] Shippers { get; set; } + + [JsonProperty(NullValueHandling = NullValueHandling.Ignore)] + public SupplierDb[] Suppliers { get; set; } +} + +[ApiController] +[Route("[controller]")] +public class QueryBuilderController : ControllerBase +{ + private readonly DataContext dataContext; + private readonly ILogger logger; + + public QueryBuilderController(DataContext dataContext, ILogger logger) + { + this.dataContext = dataContext; + this.logger = logger; + } + + [HttpPost("ExecuteQuery")] + [Consumes("application/json")] + [Produces("application/json")] + public ActionResult ExecuteQuery(Query query) + { + logger.LogInformation("Executing query for entity: {Entity}", query.Entity); +#pragma warning disable CS8601 // Possible null reference assignment. + return Ok(new QueryBuilderResult + { + Addresses = query.Entity == "Addresses" ? dataContext.Addresses.Run(query) : null, + Categories = query.Entity == "Categories" ? dataContext.Categories.Run(query) : null, + Products = query.Entity == "Products" ? dataContext.Products.Run(query) : null, + Regions = query.Entity == "Regions" ? dataContext.Regions.Run(query) : null, + Territories = query.Entity == "Territories" ? dataContext.Territories.Run(query) : null, + Employees = query.Entity == "Employees" ? dataContext.Employees.Run(query) : null, + Customers = query.Entity == "Customers" ? dataContext.Customers.Run(query) : null, + Orders = query.Entity == "Orders" ? dataContext.Orders.Run(query) : null, + OrderDetails = query.Entity == "OrderDetails" ? dataContext.OrderDetails.Run(query) : null, + Shippers = query.Entity == "Shippers" ? dataContext.Shippers.Run(query) : null, + Suppliers = query.Entity == "Suppliers" ? dataContext.Suppliers.Run(query) : null, + }); +#pragma warning restore CS8601 // Possible null reference assignment. + } +} \ No newline at end of file diff --git a/NorthwindCRUD/Controllers/QueryExecutor.cs b/NorthwindCRUD/Controllers/QueryExecutor.cs new file mode 100644 index 0000000..1a8a095 --- /dev/null +++ b/NorthwindCRUD/Controllers/QueryExecutor.cs @@ -0,0 +1,132 @@ +namespace QueryBuilder; + +using System.ComponentModel.DataAnnotations; +using System.Globalization; +using System.Linq.Expressions; +using System.Reflection; + +public enum FilterType +{ + And = 0, + Or = 1, +} + +public interface IQuery +{ + [Required] + public string Entity { get; set; } + + [Required] + public string[] ReturnFields { get; set; } + + [Required] + [System.Diagnostics.CodeAnalysis.SuppressMessage("Naming", "CA1716:Identifiers should not match keywords", Justification = "required name")] + public FilterType Operator { get; set; } + + [Required] + public IQueryFilter[] FilteringOperands { get; set; } +} + +public interface IQueryFilter +{ + [Required] + public string FieldName { get; set; } + + [Required] + public bool IgnoreCase { get; set; } + + [Required] + public string ConditionName { get; set; } + + public object? SearchVal { get; set; } + + public IQuery? SearchTree { get; set; } +} + +public class Query : IQuery +{ + public string Entity { get; set; } + + public string[] ReturnFields { get; set; } + + public FilterType Operator { get; set; } + + public IQueryFilter[] FilteringOperands { get; set; } +} + +public class QueryFilter : IQueryFilter +{ + public string FieldName { get; set; } + + public bool IgnoreCase { get; set; } + + public string ConditionName { get; set; } + + public object? SearchVal { get; set; } + + public IQuery? SearchTree { get; set; } +} + +public static class QueryExecutor +{ + public static TEntity[] Run(this IQueryable source, IQuery query) + { + var filterExpression = BuildExpression(query.FilteringOperands, query.Operator); + var filteredQuery = source.Where(filterExpression); + if (query.ReturnFields != null && query.ReturnFields.Any()) + { + // TODO: project required fields + } + + return filteredQuery.ToArray(); + } + + private static Expression> BuildExpression(IQueryFilter[] filters, FilterType filterType) + { + var parameter = Expression.Parameter(typeof(TEntity), "entity"); + var finalExpression = null as Expression; + foreach (var filter in filters) + { + var expression = BuildConditionExpression(filter, parameter); + if (finalExpression == null) + { + finalExpression = expression; + } + else + { + finalExpression = filterType == FilterType.And + ? Expression.AndAlso(finalExpression, expression) + : Expression.OrElse(finalExpression, expression); + } + } + + return finalExpression is not null + ? Expression.Lambda>(finalExpression, parameter) + : (TEntity _) => true; + } + + private static Expression BuildConditionExpression(IQueryFilter filter, ParameterExpression parameter) + { + var property = typeof(TEntity).GetProperty(filter.FieldName, BindingFlags.IgnoreCase | BindingFlags.Public | BindingFlags.Instance) ?? throw new InvalidOperationException($"Property '{filter.FieldName}' not found on type '{typeof(TEntity)}'"); + var left = Expression.Property(parameter, property); + var searchValue = Expression.Constant(Convert.ChangeType(filter.SearchVal, property.PropertyType, CultureInfo.InvariantCulture)); +#pragma warning disable CS8604 // Possible null reference argument. + Expression condition = filter.ConditionName.ToLower(CultureInfo.InvariantCulture) switch + { + "equals" => Expression.Equal(left, searchValue), + "contains" => Expression.Call(left, typeof(string).GetMethod("Contains", new[] { typeof(string) }), searchValue), + "startswith" => Expression.Call(left, typeof(string).GetMethod("StartsWith", new[] { typeof(string) }), searchValue), + "endswith" => Expression.Call(left, typeof(string).GetMethod("EndsWith", new[] { typeof(string) }), searchValue), + _ => throw new NotSupportedException($"Condition '{filter.ConditionName}' is not supported"), + }; +#pragma warning restore CS8604 // Possible null reference argument. + if (filter.IgnoreCase && left.Type == typeof(string)) + { + // TODO: handle case sensitivity for string types + // left = Expression.Call(left, "ToLower", null); + // searchValue = Expression.Constant(((string)filter.SearchVal).ToLower(CultureInfo.InvariantCulture)); + } + + return condition; + } +} \ No newline at end of file From 33f01a05fba840562e951f88d5ad34518f7cc06d Mon Sep 17 00:00:00 2001 From: Javier Coitino Date: Wed, 16 Oct 2024 18:40:05 -0300 Subject: [PATCH 02/41] Comment added --- NorthwindCRUD/Controllers/QueryExecutor.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/NorthwindCRUD/Controllers/QueryExecutor.cs b/NorthwindCRUD/Controllers/QueryExecutor.cs index 1a8a095..ce9b864 100644 --- a/NorthwindCRUD/Controllers/QueryExecutor.cs +++ b/NorthwindCRUD/Controllers/QueryExecutor.cs @@ -67,6 +67,9 @@ public class QueryFilter : IQueryFilter public IQuery? SearchTree { get; set; } } +/// +/// A generic query executor that can be used to execute queries on IQueryable data sources. +/// public static class QueryExecutor { public static TEntity[] Run(this IQueryable source, IQuery query) From 845356df6469e9371f28f7d6a8b156105bd254d8 Mon Sep 17 00:00:00 2001 From: Javier Coitino Date: Thu, 17 Oct 2024 04:41:58 -0300 Subject: [PATCH 03/41] Custom serializers added --- .../Controllers/QueryBuilderController.cs | 24 ++-- NorthwindCRUD/Controllers/QueryExecutor.cs | 115 +++++++++++++++++- NorthwindCRUD/Program.cs | 4 + 3 files changed, 127 insertions(+), 16 deletions(-) diff --git a/NorthwindCRUD/Controllers/QueryBuilderController.cs b/NorthwindCRUD/Controllers/QueryBuilderController.cs index 96448e5..9b99c79 100644 --- a/NorthwindCRUD/Controllers/QueryBuilderController.cs +++ b/NorthwindCRUD/Controllers/QueryBuilderController.cs @@ -4,6 +4,7 @@ namespace QueryBuilder; using Newtonsoft.Json; using NorthwindCRUD; using NorthwindCRUD.Models.DbModels; +using System.Globalization; public class QueryBuilderResult { @@ -60,20 +61,21 @@ public QueryBuilderController(DataContext dataContext, ILogger ExecuteQuery(Query query) { logger.LogInformation("Executing query for entity: {Entity}", query.Entity); + var t = query.Entity.ToLower(CultureInfo.InvariantCulture); #pragma warning disable CS8601 // Possible null reference assignment. return Ok(new QueryBuilderResult { - Addresses = query.Entity == "Addresses" ? dataContext.Addresses.Run(query) : null, - Categories = query.Entity == "Categories" ? dataContext.Categories.Run(query) : null, - Products = query.Entity == "Products" ? dataContext.Products.Run(query) : null, - Regions = query.Entity == "Regions" ? dataContext.Regions.Run(query) : null, - Territories = query.Entity == "Territories" ? dataContext.Territories.Run(query) : null, - Employees = query.Entity == "Employees" ? dataContext.Employees.Run(query) : null, - Customers = query.Entity == "Customers" ? dataContext.Customers.Run(query) : null, - Orders = query.Entity == "Orders" ? dataContext.Orders.Run(query) : null, - OrderDetails = query.Entity == "OrderDetails" ? dataContext.OrderDetails.Run(query) : null, - Shippers = query.Entity == "Shippers" ? dataContext.Shippers.Run(query) : null, - Suppliers = query.Entity == "Suppliers" ? dataContext.Suppliers.Run(query) : null, + Addresses = t == "addresses" ? dataContext.Addresses.Run(query) : null, + Categories = t == "categories" ? dataContext.Categories.Run(query) : null, + Products = t == "products" ? dataContext.Products.Run(query) : null, + Regions = t == "regions" ? dataContext.Regions.Run(query) : null, + Territories = t == "territories" ? dataContext.Territories.Run(query) : null, + Employees = t == "employees" ? dataContext.Employees.Run(query) : null, + Customers = t == "customers" ? dataContext.Customers.Run(query) : null, + Orders = t == "orders" ? dataContext.Orders.Run(query) : null, + OrderDetails = t == "orderdetails" ? dataContext.OrderDetails.Run(query) : null, + Shippers = t == "shippers" ? dataContext.Shippers.Run(query) : null, + Suppliers = t == "suppliers" ? dataContext.Suppliers.Run(query) : null, }); #pragma warning restore CS8601 // Possible null reference assignment. } diff --git a/NorthwindCRUD/Controllers/QueryExecutor.cs b/NorthwindCRUD/Controllers/QueryExecutor.cs index ce9b864..addd516 100644 --- a/NorthwindCRUD/Controllers/QueryExecutor.cs +++ b/NorthwindCRUD/Controllers/QueryExecutor.cs @@ -4,6 +4,9 @@ using System.Globalization; using System.Linq.Expressions; using System.Reflection; +using Newtonsoft.Json.Utilities; +using Newtonsoft.Json.Serialization; +using Newtonsoft.Json; public enum FilterType { @@ -36,13 +39,24 @@ public interface IQueryFilter public bool IgnoreCase { get; set; } [Required] - public string ConditionName { get; set; } + public IQueryFilterCondition Condition { get; set; } public object? SearchVal { get; set; } public IQuery? SearchTree { get; set; } } +public interface IQueryFilterCondition +{ + [Required] + public string Name { get; set; } + + [Required] + public bool IsUnary { get; set; } + + public string IconName { get; set; } +} + public class Query : IQuery { public string Entity { get; set; } @@ -60,13 +74,79 @@ public class QueryFilter : IQueryFilter public bool IgnoreCase { get; set; } - public string ConditionName { get; set; } + public IQueryFilterCondition Condition { get; set; } public object? SearchVal { get; set; } public IQuery? SearchTree { get; set; } } +public class QueryFilterCondition : IQueryFilterCondition +{ + public string Name { get; set; } + + public bool IsUnary { get; set; } + + public string IconName { get; set; } +} + +public class QueryConverter : JsonConverter +{ + public override bool CanConvert(Type objectType) + { + return objectType == typeof(IQuery); + } + + public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) + { + // Specify how to deserialize to a concrete class + return serializer.Deserialize(reader); + } + + public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) + { + serializer.Serialize(writer, value); + } +} + +public class QueryFilterConverter : JsonConverter +{ + public override bool CanConvert(Type objectType) + { + return objectType == typeof(IQueryFilter); + } + + public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) + { + // Specify how to deserialize to a concrete class + return serializer.Deserialize(reader); + } + + public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) + { + serializer.Serialize(writer, value); + } +} + +public class QueryFilterConditionConverter : JsonConverter +{ + public override bool CanConvert(Type objectType) + { + return objectType == typeof(IQueryFilterCondition); + } + + public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) + { + // Specify how to deserialize to a concrete class + return serializer.Deserialize(reader); + } + + public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) + { + serializer.Serialize(writer, value); + } +} + /// /// A generic query executor that can be used to execute queries on IQueryable data sources. /// @@ -112,15 +192,40 @@ private static Expression BuildConditionExpression(IQueryFilter filter, { var property = typeof(TEntity).GetProperty(filter.FieldName, BindingFlags.IgnoreCase | BindingFlags.Public | BindingFlags.Instance) ?? throw new InvalidOperationException($"Property '{filter.FieldName}' not found on type '{typeof(TEntity)}'"); var left = Expression.Property(parameter, property); - var searchValue = Expression.Constant(Convert.ChangeType(filter.SearchVal, property.PropertyType, CultureInfo.InvariantCulture)); + var targetType = property.PropertyType; + Expression searchValue; + if (targetType.IsGenericType && targetType.GetGenericTypeDefinition() == typeof(Nullable<>)) + { + targetType = Nullable.GetUnderlyingType(targetType); + } + + if (filter.SearchVal is long longValue && targetType == typeof(int)) + { + if (longValue >= int.MinValue && longValue <= int.MaxValue) + { + searchValue = Expression.Constant(Convert.ChangeType((int)longValue, targetType, CultureInfo.InvariantCulture)); + } + else + { + throw new OverflowException("The Int64 value is too large or too small to fit into an Int32."); + } + } + else + { + searchValue = Expression.Constant(Convert.ChangeType(filter.SearchVal, targetType, CultureInfo.InvariantCulture)); + } #pragma warning disable CS8604 // Possible null reference argument. - Expression condition = filter.ConditionName.ToLower(CultureInfo.InvariantCulture) switch + Expression condition = filter.Condition.Name.ToLower(CultureInfo.InvariantCulture) switch { "equals" => Expression.Equal(left, searchValue), + "lessthan" => Expression.LessThan(left, searchValue), + "greaterthan" => Expression.GreaterThan(left, searchValue), + "lessthanorequal" => Expression.LessThanOrEqual(left, searchValue), + "greaterthanorequal" => Expression.GreaterThanOrEqual(left, searchValue), "contains" => Expression.Call(left, typeof(string).GetMethod("Contains", new[] { typeof(string) }), searchValue), "startswith" => Expression.Call(left, typeof(string).GetMethod("StartsWith", new[] { typeof(string) }), searchValue), "endswith" => Expression.Call(left, typeof(string).GetMethod("EndsWith", new[] { typeof(string) }), searchValue), - _ => throw new NotSupportedException($"Condition '{filter.ConditionName}' is not supported"), + _ => throw new NotSupportedException($"Condition '{filter.Condition.Name}' is not supported"), }; #pragma warning restore CS8604 // Possible null reference argument. if (filter.IgnoreCase && left.Type == typeof(string)) diff --git a/NorthwindCRUD/Program.cs b/NorthwindCRUD/Program.cs index 2203523..d265a9e 100644 --- a/NorthwindCRUD/Program.cs +++ b/NorthwindCRUD/Program.cs @@ -12,6 +12,7 @@ using NorthwindCRUD.Filters; using NorthwindCRUD.Helpers; using NorthwindCRUD.Services; +using QueryBuilder; namespace NorthwindCRUD { @@ -40,6 +41,9 @@ public static void Main(string[] args) { options.SerializerSettings.ReferenceLoopHandling = Newtonsoft.Json.ReferenceLoopHandling.Ignore; options.SerializerSettings.Converters.Add(new StringEnumConverter()); + options.SerializerSettings.Converters.Add(new QueryConverter()); + options.SerializerSettings.Converters.Add(new QueryFilterConverter()); + options.SerializerSettings.Converters.Add(new QueryFilterConditionConverter()); }); builder.Services.AddEndpointsApiExplorer(); From e2d4dda91422420cca07f5ec35250db17468b1d8 Mon Sep 17 00:00:00 2001 From: Javier Coitino Date: Thu, 17 Oct 2024 19:47:16 -0300 Subject: [PATCH 04/41] code polishing --- .../Controllers/QueryBuilderController.cs | 45 +++++-------------- 1 file changed, 11 insertions(+), 34 deletions(-) diff --git a/NorthwindCRUD/Controllers/QueryBuilderController.cs b/NorthwindCRUD/Controllers/QueryBuilderController.cs index 9b99c79..c6ed548 100644 --- a/NorthwindCRUD/Controllers/QueryBuilderController.cs +++ b/NorthwindCRUD/Controllers/QueryBuilderController.cs @@ -8,38 +8,17 @@ namespace QueryBuilder; public class QueryBuilderResult { - [JsonProperty(NullValueHandling = NullValueHandling.Ignore)] - public AddressDb[] Addresses { get; set; } - - [JsonProperty(NullValueHandling = NullValueHandling.Ignore)] - public CategoryDb[] Categories { get; set; } - - [JsonProperty(NullValueHandling = NullValueHandling.Ignore)] - public ProductDb[] Products { get; set; } - - [JsonProperty(NullValueHandling = NullValueHandling.Ignore)] - public RegionDb[] Regions { get; set; } - - [JsonProperty(NullValueHandling = NullValueHandling.Ignore)] - public TerritoryDb[] Territories { get; set; } - - [JsonProperty(NullValueHandling = NullValueHandling.Ignore)] - public EmployeeDb[] Employees { get; set; } - - [JsonProperty(NullValueHandling = NullValueHandling.Ignore)] - public CustomerDb[] Customers { get; set; } - - [JsonProperty(NullValueHandling = NullValueHandling.Ignore)] - public OrderDb[] Orders { get; set; } - - [JsonProperty(NullValueHandling = NullValueHandling.Ignore)] - public OrderDetailDb[] OrderDetails { get; set; } - - [JsonProperty(NullValueHandling = NullValueHandling.Ignore)] - public ShipperDb[] Shippers { get; set; } - - [JsonProperty(NullValueHandling = NullValueHandling.Ignore)] - public SupplierDb[] Suppliers { get; set; } + [JsonProperty(NullValueHandling = NullValueHandling.Ignore)] public AddressDb[]? Addresses { get; set; } + [JsonProperty(NullValueHandling = NullValueHandling.Ignore)] public CategoryDb[]? Categories { get; set; } + [JsonProperty(NullValueHandling = NullValueHandling.Ignore)] public ProductDb[]? Products { get; set; } + [JsonProperty(NullValueHandling = NullValueHandling.Ignore)] public RegionDb[]? Regions { get; set; } + [JsonProperty(NullValueHandling = NullValueHandling.Ignore)] public TerritoryDb[]? Territories { get; set; } + [JsonProperty(NullValueHandling = NullValueHandling.Ignore)] public EmployeeDb[]? Employees { get; set; } + [JsonProperty(NullValueHandling = NullValueHandling.Ignore)] public CustomerDb[]? Customers { get; set; } + [JsonProperty(NullValueHandling = NullValueHandling.Ignore)] public OrderDb[]? Orders { get; set; } + [JsonProperty(NullValueHandling = NullValueHandling.Ignore)] public OrderDetailDb[]? OrderDetails { get; set; } + [JsonProperty(NullValueHandling = NullValueHandling.Ignore)] public ShipperDb[]? Shippers { get; set; } + [JsonProperty(NullValueHandling = NullValueHandling.Ignore)] public SupplierDb[]? Suppliers { get; set; } } [ApiController] @@ -62,7 +41,6 @@ public ActionResult ExecuteQuery(Query query) { logger.LogInformation("Executing query for entity: {Entity}", query.Entity); var t = query.Entity.ToLower(CultureInfo.InvariantCulture); -#pragma warning disable CS8601 // Possible null reference assignment. return Ok(new QueryBuilderResult { Addresses = t == "addresses" ? dataContext.Addresses.Run(query) : null, @@ -77,6 +55,5 @@ public ActionResult ExecuteQuery(Query query) Shippers = t == "shippers" ? dataContext.Shippers.Run(query) : null, Suppliers = t == "suppliers" ? dataContext.Suppliers.Run(query) : null, }); -#pragma warning restore CS8601 // Possible null reference assignment. } } \ No newline at end of file From 9a49df708889991a719590e8d4d564511579cc25 Mon Sep 17 00:00:00 2001 From: Javier Coitino Date: Sat, 19 Oct 2024 11:35:18 -0300 Subject: [PATCH 05/41] Map to DTOs --- .../Controllers/QueryBuilderController.cs | 56 +++++++++++-------- 1 file changed, 32 insertions(+), 24 deletions(-) diff --git a/NorthwindCRUD/Controllers/QueryBuilderController.cs b/NorthwindCRUD/Controllers/QueryBuilderController.cs index c6ed548..3874e8e 100644 --- a/NorthwindCRUD/Controllers/QueryBuilderController.cs +++ b/NorthwindCRUD/Controllers/QueryBuilderController.cs @@ -1,24 +1,29 @@ namespace QueryBuilder; +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.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 AddressDb[]? Addresses { get; set; } - [JsonProperty(NullValueHandling = NullValueHandling.Ignore)] public CategoryDb[]? Categories { get; set; } - [JsonProperty(NullValueHandling = NullValueHandling.Ignore)] public ProductDb[]? Products { get; set; } - [JsonProperty(NullValueHandling = NullValueHandling.Ignore)] public RegionDb[]? Regions { get; set; } - [JsonProperty(NullValueHandling = NullValueHandling.Ignore)] public TerritoryDb[]? Territories { get; set; } - [JsonProperty(NullValueHandling = NullValueHandling.Ignore)] public EmployeeDb[]? Employees { get; set; } - [JsonProperty(NullValueHandling = NullValueHandling.Ignore)] public CustomerDb[]? Customers { get; set; } - [JsonProperty(NullValueHandling = NullValueHandling.Ignore)] public OrderDb[]? Orders { get; set; } - [JsonProperty(NullValueHandling = NullValueHandling.Ignore)] public OrderDetailDb[]? OrderDetails { get; set; } - [JsonProperty(NullValueHandling = NullValueHandling.Ignore)] public ShipperDb[]? Shippers { get; set; } - [JsonProperty(NullValueHandling = NullValueHandling.Ignore)] public SupplierDb[]? Suppliers { get; set; } + [JsonProperty(NullValueHandling = NullValueHandling.Ignore)] public AddressDto[]? Addresses { get; set; } + [JsonProperty(NullValueHandling = NullValueHandling.Ignore)] public CategoryDto[]? Categories { get; set; } + [JsonProperty(NullValueHandling = NullValueHandling.Ignore)] public ProductDto[]? Products { get; set; } + [JsonProperty(NullValueHandling = NullValueHandling.Ignore)] public RegionDto[]? Regions { get; set; } + [JsonProperty(NullValueHandling = NullValueHandling.Ignore)] public TerritoryDto[]? Territories { get; set; } + [JsonProperty(NullValueHandling = NullValueHandling.Ignore)] public EmployeeDto[]? Employees { get; set; } + [JsonProperty(NullValueHandling = NullValueHandling.Ignore)] public CustomerDto[]? Customers { get; set; } + [JsonProperty(NullValueHandling = NullValueHandling.Ignore)] public OrderDto[]? Orders { get; set; } + [JsonProperty(NullValueHandling = NullValueHandling.Ignore)] public OrderDetailDto[]? OrderDetails { get; set; } + [JsonProperty(NullValueHandling = NullValueHandling.Ignore)] public ShipperDto[]? Shippers { get; set; } + [JsonProperty(NullValueHandling = NullValueHandling.Ignore)] public SupplierDto[]? Suppliers { get; set; } } [ApiController] @@ -26,34 +31,37 @@ public class QueryBuilderResult public class QueryBuilderController : ControllerBase { private readonly DataContext dataContext; + private readonly IMapper mapper; private readonly ILogger logger; - public QueryBuilderController(DataContext dataContext, ILogger logger) + public QueryBuilderController(DataContext dataContext, IMapper mapper, ILogger logger) { this.dataContext = dataContext; + this.mapper = mapper; this.logger = logger; } [HttpPost("ExecuteQuery")] [Consumes("application/json")] [Produces("application/json")] + [SuppressMessage("StyleCop.CSharp.SpacingRules", "SA1025:Code should not contain multiple whitespace in a row", Justification = "...")] public ActionResult ExecuteQuery(Query query) { logger.LogInformation("Executing query for entity: {Entity}", query.Entity); var t = query.Entity.ToLower(CultureInfo.InvariantCulture); return Ok(new QueryBuilderResult { - Addresses = t == "addresses" ? dataContext.Addresses.Run(query) : null, - Categories = t == "categories" ? dataContext.Categories.Run(query) : null, - Products = t == "products" ? dataContext.Products.Run(query) : null, - Regions = t == "regions" ? dataContext.Regions.Run(query) : null, - Territories = t == "territories" ? dataContext.Territories.Run(query) : null, - Employees = t == "employees" ? dataContext.Employees.Run(query) : null, - Customers = t == "customers" ? dataContext.Customers.Run(query) : null, - Orders = t == "orders" ? dataContext.Orders.Run(query) : null, - OrderDetails = t == "orderdetails" ? dataContext.OrderDetails.Run(query) : null, - Shippers = t == "shippers" ? dataContext.Shippers.Run(query) : null, - Suppliers = t == "suppliers" ? dataContext.Suppliers.Run(query) : null, + 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, }); } } \ No newline at end of file From 245d062ce2f21d9a342bac4fe23e4ce90a7149fd Mon Sep 17 00:00:00 2001 From: Javier Coitino Date: Sat, 19 Oct 2024 11:36:54 -0300 Subject: [PATCH 06/41] code polishing --- .../Controllers/QueryBuilderController.cs | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/NorthwindCRUD/Controllers/QueryBuilderController.cs b/NorthwindCRUD/Controllers/QueryBuilderController.cs index 3874e8e..7daa456 100644 --- a/NorthwindCRUD/Controllers/QueryBuilderController.cs +++ b/NorthwindCRUD/Controllers/QueryBuilderController.cs @@ -13,17 +13,17 @@ namespace QueryBuilder; [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; } - [JsonProperty(NullValueHandling = NullValueHandling.Ignore)] public CategoryDto[]? Categories { get; set; } - [JsonProperty(NullValueHandling = NullValueHandling.Ignore)] public ProductDto[]? Products { get; set; } - [JsonProperty(NullValueHandling = NullValueHandling.Ignore)] public RegionDto[]? Regions { get; set; } - [JsonProperty(NullValueHandling = NullValueHandling.Ignore)] public TerritoryDto[]? Territories { get; set; } - [JsonProperty(NullValueHandling = NullValueHandling.Ignore)] public EmployeeDto[]? Employees { get; set; } - [JsonProperty(NullValueHandling = NullValueHandling.Ignore)] public CustomerDto[]? Customers { get; set; } - [JsonProperty(NullValueHandling = NullValueHandling.Ignore)] public OrderDto[]? Orders { get; set; } + [JsonProperty(NullValueHandling = NullValueHandling.Ignore)] public AddressDto[]? Addresses { get; set; } + [JsonProperty(NullValueHandling = NullValueHandling.Ignore)] public CategoryDto[]? Categories { get; set; } + [JsonProperty(NullValueHandling = NullValueHandling.Ignore)] public ProductDto[]? Products { get; set; } + [JsonProperty(NullValueHandling = NullValueHandling.Ignore)] public RegionDto[]? Regions { get; set; } + [JsonProperty(NullValueHandling = NullValueHandling.Ignore)] public TerritoryDto[]? Territories { get; set; } + [JsonProperty(NullValueHandling = NullValueHandling.Ignore)] public EmployeeDto[]? Employees { get; set; } + [JsonProperty(NullValueHandling = NullValueHandling.Ignore)] public CustomerDto[]? Customers { get; set; } + [JsonProperty(NullValueHandling = NullValueHandling.Ignore)] public OrderDto[]? Orders { get; set; } [JsonProperty(NullValueHandling = NullValueHandling.Ignore)] public OrderDetailDto[]? OrderDetails { get; set; } - [JsonProperty(NullValueHandling = NullValueHandling.Ignore)] public ShipperDto[]? Shippers { get; set; } - [JsonProperty(NullValueHandling = NullValueHandling.Ignore)] public SupplierDto[]? Suppliers { get; set; } + [JsonProperty(NullValueHandling = NullValueHandling.Ignore)] public ShipperDto[]? Shippers { get; set; } + [JsonProperty(NullValueHandling = NullValueHandling.Ignore)] public SupplierDto[]? Suppliers { get; set; } } [ApiController] From 54e742c01d96c1f2b736233918514b7c551ea9b2 Mon Sep 17 00:00:00 2001 From: Javier Coitino Date: Sat, 19 Oct 2024 11:37:44 -0300 Subject: [PATCH 07/41] SC Supression --- NorthwindCRUD/Controllers/QueryBuilderController.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/NorthwindCRUD/Controllers/QueryBuilderController.cs b/NorthwindCRUD/Controllers/QueryBuilderController.cs index 7daa456..8ea1837 100644 --- a/NorthwindCRUD/Controllers/QueryBuilderController.cs +++ b/NorthwindCRUD/Controllers/QueryBuilderController.cs @@ -9,6 +9,7 @@ namespace QueryBuilder; 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 From 8a1ee2285537c2fd5bcf4ecaaeee3f5ac7ab8675 Mon Sep 17 00:00:00 2001 From: Javier Coitino Date: Sat, 19 Oct 2024 12:07:15 -0300 Subject: [PATCH 08/41] Code polishing --- NorthwindCRUD/Controllers/QueryBuilderController.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/NorthwindCRUD/Controllers/QueryBuilderController.cs b/NorthwindCRUD/Controllers/QueryBuilderController.cs index 8ea1837..bb7c36f 100644 --- a/NorthwindCRUD/Controllers/QueryBuilderController.cs +++ b/NorthwindCRUD/Controllers/QueryBuilderController.cs @@ -29,6 +29,7 @@ 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; @@ -45,7 +46,6 @@ public QueryBuilderController(DataContext dataContext, IMapper mapper, ILogger ExecuteQuery(Query query) { logger.LogInformation("Executing query for entity: {Entity}", query.Entity); From 6d4813717f53c8c7c4394f8e2f8301f2a8e4151b Mon Sep 17 00:00:00 2001 From: Javier Coitino Date: Sat, 19 Oct 2024 15:21:03 -0300 Subject: [PATCH 09/41] Project requested columns only --- NorthwindCRUD/Controllers/QueryExecutor.cs | 151 +++++++++------------ 1 file changed, 66 insertions(+), 85 deletions(-) diff --git a/NorthwindCRUD/Controllers/QueryExecutor.cs b/NorthwindCRUD/Controllers/QueryExecutor.cs index addd516..c2dcd27 100644 --- a/NorthwindCRUD/Controllers/QueryExecutor.cs +++ b/NorthwindCRUD/Controllers/QueryExecutor.cs @@ -1,11 +1,9 @@ namespace QueryBuilder; -using System.ComponentModel.DataAnnotations; +using System.Diagnostics.CodeAnalysis; using System.Globalization; using System.Linq.Expressions; using System.Reflection; -using Newtonsoft.Json.Utilities; -using Newtonsoft.Json.Serialization; using Newtonsoft.Json; public enum FilterType @@ -16,29 +14,22 @@ public enum FilterType public interface IQuery { - [Required] public string Entity { get; set; } - [Required] public string[] ReturnFields { get; set; } - [Required] - [System.Diagnostics.CodeAnalysis.SuppressMessage("Naming", "CA1716:Identifiers should not match keywords", Justification = "required name")] + [SuppressMessage("Naming", "CA1716:Identifiers should not match keywords", Justification = "required name")] public FilterType Operator { get; set; } - [Required] public IQueryFilter[] FilteringOperands { get; set; } } public interface IQueryFilter { - [Required] public string FieldName { get; set; } - [Required] public bool IgnoreCase { get; set; } - [Required] public IQueryFilterCondition Condition { get; set; } public object? SearchVal { get; set; } @@ -48,10 +39,8 @@ public interface IQueryFilter public interface IQueryFilterCondition { - [Required] public string Name { get; set; } - [Required] public bool IsUnary { get; set; } public string IconName { get; set; } @@ -92,59 +81,29 @@ public class QueryFilterCondition : IQueryFilterCondition public class QueryConverter : JsonConverter { - public override bool CanConvert(Type objectType) - { - return objectType == typeof(IQuery); - } + public override bool CanConvert(Type objectType) => objectType == typeof(IQuery); - public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) - { - // Specify how to deserialize to a concrete class - return serializer.Deserialize(reader); - } + public override object? ReadJson(JsonReader reader, Type objectType, object? existingValue, JsonSerializer serializer) => serializer.Deserialize(reader); - public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) - { - serializer.Serialize(writer, value); - } + public override void WriteJson(JsonWriter writer, object? value, JsonSerializer serializer) => serializer.Serialize(writer, value); } public class QueryFilterConverter : JsonConverter { - public override bool CanConvert(Type objectType) - { - return objectType == typeof(IQueryFilter); - } + public override bool CanConvert(Type objectType) => objectType == typeof(IQueryFilter); - public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) - { - // Specify how to deserialize to a concrete class - return serializer.Deserialize(reader); - } + public override object? ReadJson(JsonReader reader, Type objectType, object? existingValue, JsonSerializer serializer) => serializer.Deserialize(reader); - public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) - { - serializer.Serialize(writer, value); - } + public override void WriteJson(JsonWriter writer, object? value, JsonSerializer serializer) => serializer.Serialize(writer, value); } public class QueryFilterConditionConverter : JsonConverter { - public override bool CanConvert(Type objectType) - { - return objectType == typeof(IQueryFilterCondition); - } + public override bool CanConvert(Type objectType) => objectType == typeof(IQueryFilterCondition); - public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) - { - // Specify how to deserialize to a concrete class - return serializer.Deserialize(reader); - } + public override object? ReadJson(JsonReader reader, Type objectType, object? existingValue, JsonSerializer serializer) => serializer.Deserialize(reader); - public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) - { - serializer.Serialize(writer, value); - } + public override void WriteJson(JsonWriter writer, object? value, JsonSerializer serializer) => serializer.Serialize(writer, value); } /// @@ -152,16 +111,20 @@ public override void WriteJson(JsonWriter writer, object value, JsonSerializer s /// public static class QueryExecutor { - public static TEntity[] Run(this IQueryable source, IQuery query) + public static object[] Run(this IQueryable source, IQuery query) { var filterExpression = BuildExpression(query.FilteringOperands, query.Operator); var filteredQuery = source.Where(filterExpression); if (query.ReturnFields != null && query.ReturnFields.Any()) { - // TODO: project required fields + var projectionExpression = BuildProjectionExpression(query.ReturnFields); + var projectedQuery = filteredQuery.Select(projectionExpression); + return projectedQuery.ToArray(); + } + else + { + return filteredQuery.ToArray() as object[]; } - - return filteredQuery.ToArray(); } private static Expression> BuildExpression(IQueryFilter[] filters, FilterType filterType) @@ -190,31 +153,11 @@ private static Expression> BuildExpression(IQueryFi private static Expression BuildConditionExpression(IQueryFilter filter, ParameterExpression parameter) { - var property = typeof(TEntity).GetProperty(filter.FieldName, BindingFlags.IgnoreCase | BindingFlags.Public | BindingFlags.Instance) ?? throw new InvalidOperationException($"Property '{filter.FieldName}' not found on type '{typeof(TEntity)}'"); + var property = typeof(TEntity).GetProperty(filter.FieldName, BindingFlags.IgnoreCase | BindingFlags.Public | BindingFlags.Instance) + ?? throw new InvalidOperationException($"Property '{filter.FieldName}' not found on type '{typeof(TEntity)}'"); var left = Expression.Property(parameter, property); - var targetType = property.PropertyType; - Expression searchValue; - if (targetType.IsGenericType && targetType.GetGenericTypeDefinition() == typeof(Nullable<>)) - { - targetType = Nullable.GetUnderlyingType(targetType); - } - - if (filter.SearchVal is long longValue && targetType == typeof(int)) - { - if (longValue >= int.MinValue && longValue <= int.MaxValue) - { - searchValue = Expression.Constant(Convert.ChangeType((int)longValue, targetType, CultureInfo.InvariantCulture)); - } - else - { - throw new OverflowException("The Int64 value is too large or too small to fit into an Int32."); - } - } - else - { - searchValue = Expression.Constant(Convert.ChangeType(filter.SearchVal, targetType, CultureInfo.InvariantCulture)); - } -#pragma warning disable CS8604 // Possible null reference argument. + var targetType = GetPropertyType(property); + var searchValue = GetSearchValue(filter.SearchVal, targetType); Expression condition = filter.Condition.Name.ToLower(CultureInfo.InvariantCulture) switch { "equals" => Expression.Equal(left, searchValue), @@ -222,19 +165,57 @@ private static Expression BuildConditionExpression(IQueryFilter filter, "greaterthan" => Expression.GreaterThan(left, searchValue), "lessthanorequal" => Expression.LessThanOrEqual(left, searchValue), "greaterthanorequal" => Expression.GreaterThanOrEqual(left, searchValue), - "contains" => Expression.Call(left, typeof(string).GetMethod("Contains", new[] { typeof(string) }), searchValue), - "startswith" => Expression.Call(left, typeof(string).GetMethod("StartsWith", new[] { typeof(string) }), searchValue), - "endswith" => Expression.Call(left, typeof(string).GetMethod("EndsWith", new[] { typeof(string) }), searchValue), + "contains" => Expression.Call(left, typeof(string).GetMethod("Contains", new[] { typeof(string) })!, searchValue), + "startswith" => Expression.Call(left, typeof(string).GetMethod("StartsWith", new[] { typeof(string) })!, searchValue), + "endswith" => Expression.Call(left, typeof(string).GetMethod("EndsWith", new[] { typeof(string) })!, searchValue), _ => throw new NotSupportedException($"Condition '{filter.Condition.Name}' is not supported"), }; -#pragma warning restore CS8604 // Possible null reference argument. if (filter.IgnoreCase && left.Type == typeof(string)) { - // TODO: handle case sensitivity for string types + // TODO: Implement case-insensitive comparison // left = Expression.Call(left, "ToLower", null); // searchValue = Expression.Constant(((string)filter.SearchVal).ToLower(CultureInfo.InvariantCulture)); } return condition; } + + private static Type GetPropertyType(PropertyInfo property) + { + var targetType = property.PropertyType; + if (targetType.IsGenericType && targetType.GetGenericTypeDefinition() == typeof(Nullable<>)) + { + return Nullable.GetUnderlyingType(targetType) ?? targetType; + } + else + { + return targetType; + } + } + + private static Expression GetSearchValue(object? value, Type targetType) + { + if (value == null) + { + return Expression.Constant(null, targetType); + } + + var nonNullableType = Nullable.GetUnderlyingType(targetType) ?? targetType; + var convertedValue = Convert.ChangeType(value, nonNullableType, CultureInfo.InvariantCulture); + return Expression.Constant(convertedValue, targetType); + } + + private static Expression> BuildProjectionExpression(string[] returnFields) + { + var parameter = Expression.Parameter(typeof(TEntity), "entity"); + var bindings = returnFields.Select(field => + { + var property = typeof(TEntity).GetProperty(field, BindingFlags.IgnoreCase | BindingFlags.Public | BindingFlags.Instance) ?? throw new InvalidOperationException($"Property '{field}' not found on type '{typeof(TEntity)}'"); + var propertyAccess = Expression.Property(parameter, property); + return Expression.Bind(property, propertyAccess); + }).ToArray(); + + var body = Expression.MemberInit(Expression.New(typeof(TEntity)), bindings); + return Expression.Lambda>(body, parameter); + } } \ No newline at end of file From 690c5e4b62f01dfe8abd0d640add4ecb51327176 Mon Sep 17 00:00:00 2001 From: Javier Coitino Date: Sat, 19 Oct 2024 18:48:34 -0300 Subject: [PATCH 10/41] Code polishing --- NorthwindCRUD/Controllers/QueryExecutor.cs | 44 ++++++++++++++-------- 1 file changed, 28 insertions(+), 16 deletions(-) diff --git a/NorthwindCRUD/Controllers/QueryExecutor.cs b/NorthwindCRUD/Controllers/QueryExecutor.cs index c2dcd27..8c08b63 100644 --- a/NorthwindCRUD/Controllers/QueryExecutor.cs +++ b/NorthwindCRUD/Controllers/QueryExecutor.cs @@ -109,6 +109,8 @@ public class QueryFilterConditionConverter : JsonConverter /// /// A generic query executor that can be used to execute queries on IQueryable data sources. /// +[SuppressMessage("StyleCop.CSharp.SpacingRules", "SA1009:Closing parenthesis should be spaced correctly", Justification = "...")] +[SuppressMessage("StyleCop.CSharp.SpacingRules", "SA1025:Code should not contain multiple whitespace in a row", Justification = "...")] public static class QueryExecutor { public static object[] Run(this IQueryable source, IQuery query) @@ -153,24 +155,34 @@ private static Expression> BuildExpression(IQueryFi private static Expression BuildConditionExpression(IQueryFilter filter, ParameterExpression parameter) { - var property = typeof(TEntity).GetProperty(filter.FieldName, BindingFlags.IgnoreCase | BindingFlags.Public | BindingFlags.Instance) - ?? throw new InvalidOperationException($"Property '{filter.FieldName}' not found on type '{typeof(TEntity)}'"); - var left = Expression.Property(parameter, property); - var targetType = GetPropertyType(property); - var searchValue = GetSearchValue(filter.SearchVal, targetType); - Expression condition = filter.Condition.Name.ToLower(CultureInfo.InvariantCulture) switch + var property = typeof(TEntity).GetProperty(filter.FieldName, BindingFlags.IgnoreCase | BindingFlags.Public | BindingFlags.Instance) ?? throw new InvalidOperationException($"Property '{filter.FieldName}' not found on type '{typeof(TEntity)}'"); + var field = Expression.Property(parameter, property); + var targetType = GetPropertyType(property); + var searchValue = GetSearchValue(filter.SearchVal, targetType); + Expression condition = filter.Condition.Name switch { - "equals" => Expression.Equal(left, searchValue), - "lessthan" => Expression.LessThan(left, searchValue), - "greaterthan" => Expression.GreaterThan(left, searchValue), - "lessthanorequal" => Expression.LessThanOrEqual(left, searchValue), - "greaterthanorequal" => Expression.GreaterThanOrEqual(left, searchValue), - "contains" => Expression.Call(left, typeof(string).GetMethod("Contains", new[] { typeof(string) })!, searchValue), - "startswith" => Expression.Call(left, typeof(string).GetMethod("StartsWith", new[] { typeof(string) })!, searchValue), - "endswith" => Expression.Call(left, typeof(string).GetMethod("EndsWith", new[] { typeof(string) })!, searchValue), - _ => throw new NotSupportedException($"Condition '{filter.Condition.Name}' is not supported"), + "null" => Expression.Equal(field, Expression.Constant(null, targetType)), + "notNull" => Expression.NotEqual(field, Expression.Constant(null, targetType)), + "empty" => Expression.Equal(field, Expression.Constant(string.Empty, targetType)), // TODO: Implement empty condition + "notEmpty" => Expression.NotEqual(field, Expression.Constant(string.Empty, targetType)), // TODO: Implement not empty condition + "equals" => Expression.Equal(field, searchValue), + "doesNotEqual" => Expression.NotEqual(field, searchValue), + "in" => throw new NotImplementedException("Not implemented"), + "notIn" => throw new NotImplementedException("Not implemented"), + "contains" => Expression.Call(field, typeof(string).GetMethod("Contains", new[] { typeof(string) })!, searchValue), + "doesNotContain" => Expression.Not(Expression.Call(field, typeof(string).GetMethod("Contains", new[] { typeof(string) })!, searchValue)), + "startsWith" => Expression.Call(field, typeof(string).GetMethod("StartsWith", new[] { typeof(string) })!, searchValue), + "endsWith" => Expression.Call(field, typeof(string).GetMethod("EndsWith", new[] { typeof(string) })!, searchValue), + "greaterThan" => Expression.GreaterThan(field, searchValue), + "lessThan" => Expression.LessThan(field, searchValue), + "greaterThanOrEqualTo" => Expression.GreaterThanOrEqual(field, searchValue), + "lessThanOrEqualTo" => Expression.LessThanOrEqual(field, searchValue), + "all" => throw new NotImplementedException("Not implemented"), + "true" => Expression.Equal(field, Expression.Constant(true)), + "false" => Expression.Equal(field, Expression.Constant(false)), + _ => throw new NotImplementedException("Not implemented"), }; - if (filter.IgnoreCase && left.Type == typeof(string)) + if (filter.IgnoreCase && field.Type == typeof(string)) { // TODO: Implement case-insensitive comparison // left = Expression.Call(left, "ToLower", null); From a23350067e7fd0399f888256a8405b527c481434 Mon Sep 17 00:00:00 2001 From: Javier Coitino Date: Sat, 19 Oct 2024 19:23:34 -0300 Subject: [PATCH 11/41] Subqueries implemented --- NorthwindCRUD/Controllers/QueryExecutor.cs | 39 ++++++++++++++++------ 1 file changed, 28 insertions(+), 11 deletions(-) diff --git a/NorthwindCRUD/Controllers/QueryExecutor.cs b/NorthwindCRUD/Controllers/QueryExecutor.cs index 8c08b63..a2ba8af 100644 --- a/NorthwindCRUD/Controllers/QueryExecutor.cs +++ b/NorthwindCRUD/Controllers/QueryExecutor.cs @@ -113,19 +113,24 @@ public class QueryFilterConditionConverter : JsonConverter [SuppressMessage("StyleCop.CSharp.SpacingRules", "SA1025:Code should not contain multiple whitespace in a row", Justification = "...")] public static class QueryExecutor { - public static object[] Run(this IQueryable source, IQuery query) + public static object[] Run(this IQueryable source, IQuery query) { - var filterExpression = BuildExpression(query.FilteringOperands, query.Operator); + return BuildQuery(source, query).ToArray(); + } + + private static IQueryable BuildQuery(IQueryable source, IQuery query) + { + var filterExpression = BuildExpression(query.FilteringOperands, query.Operator); var filteredQuery = source.Where(filterExpression); if (query.ReturnFields != null && query.ReturnFields.Any()) { - var projectionExpression = BuildProjectionExpression(query.ReturnFields); + var projectionExpression = BuildProjectionExpression(query.ReturnFields); var projectedQuery = filteredQuery.Select(projectionExpression); - return projectedQuery.ToArray(); + return projectedQuery; } else { - return filteredQuery.ToArray() as object[]; + return filteredQuery; } } @@ -159,6 +164,7 @@ private static Expression BuildConditionExpression(IQueryFilter filter, var field = Expression.Property(parameter, property); var targetType = GetPropertyType(property); var searchValue = GetSearchValue(filter.SearchVal, targetType); + var searchTree = BuildSubquery(filter.SearchTree); Expression condition = filter.Condition.Name switch { "null" => Expression.Equal(field, Expression.Constant(null, targetType)), @@ -167,8 +173,8 @@ private static Expression BuildConditionExpression(IQueryFilter filter, "notEmpty" => Expression.NotEqual(field, Expression.Constant(string.Empty, targetType)), // TODO: Implement not empty condition "equals" => Expression.Equal(field, searchValue), "doesNotEqual" => Expression.NotEqual(field, searchValue), - "in" => throw new NotImplementedException("Not implemented"), - "notIn" => throw new NotImplementedException("Not implemented"), + "in" => Expression.Call(typeof(Enumerable), "Contains", new[] { targetType }, searchTree, field), + "notIn" => Expression.Not(Expression.Call(typeof(Enumerable), "Contains", new[] { targetType }, searchTree, field)), "contains" => Expression.Call(field, typeof(string).GetMethod("Contains", new[] { typeof(string) })!, searchValue), "doesNotContain" => Expression.Not(Expression.Call(field, typeof(string).GetMethod("Contains", new[] { typeof(string) })!, searchValue)), "startsWith" => Expression.Call(field, typeof(string).GetMethod("StartsWith", new[] { typeof(string) })!, searchValue), @@ -178,20 +184,31 @@ private static Expression BuildConditionExpression(IQueryFilter filter, "greaterThanOrEqualTo" => Expression.GreaterThanOrEqual(field, searchValue), "lessThanOrEqualTo" => Expression.LessThanOrEqual(field, searchValue), "all" => throw new NotImplementedException("Not implemented"), - "true" => Expression.Equal(field, Expression.Constant(true)), - "false" => Expression.Equal(field, Expression.Constant(false)), + "true" => Expression.IsTrue(field), + "false" => Expression.IsFalse(field), _ => throw new NotImplementedException("Not implemented"), }; if (filter.IgnoreCase && field.Type == typeof(string)) { // TODO: Implement case-insensitive comparison - // left = Expression.Call(left, "ToLower", null); - // searchValue = Expression.Constant(((string)filter.SearchVal).ToLower(CultureInfo.InvariantCulture)); } return condition; } + private static Expression BuildSubquery(IQuery? query) + { + if (query == null) + { + return Expression.Constant(true); + } + + var parameter = Expression.Parameter(typeof(object), "entity"); + var filterExpression = BuildExpression(query.FilteringOperands, query.Operator); + var lambda = Expression.Lambda>(filterExpression.Body, parameter); + return lambda.Body; + } + private static Type GetPropertyType(PropertyInfo property) { var targetType = property.PropertyType; From 0d2274133f63975b61cd4467a7ea0d1a696d55e3 Mon Sep 17 00:00:00 2001 From: Javier Coitino Date: Sat, 19 Oct 2024 19:31:27 -0300 Subject: [PATCH 12/41] SQL Generator implemented --- NorthwindCRUD/Controllers/QueryExecutor.cs | 60 ++++++++++++++++++++++ 1 file changed, 60 insertions(+) diff --git a/NorthwindCRUD/Controllers/QueryExecutor.cs b/NorthwindCRUD/Controllers/QueryExecutor.cs index a2ba8af..ef1c32f 100644 --- a/NorthwindCRUD/Controllers/QueryExecutor.cs +++ b/NorthwindCRUD/Controllers/QueryExecutor.cs @@ -247,4 +247,64 @@ private static Expression> BuildProjectionExpression>(body, parameter); } +} + +[SuppressMessage("StyleCop.CSharp.SpacingRules", "SA1009:Closing parenthesis should be spaced correctly", Justification = "...")] +[SuppressMessage("StyleCop.CSharp.SpacingRules", "SA1025:Code should not contain multiple whitespace in a row", Justification = "...")] +public static class SqlGenerator +{ + public static string GenerateSql(IQuery query) + { + var selectClause = BuildSelectClause(query); + var whereClause = BuildWhereClause(query.FilteringOperands, query.Operator); + return $"{selectClause} {whereClause};"; + } + + private static string BuildSelectClause(IQuery query) + { + var fields = query.ReturnFields != null && query.ReturnFields.Any() + ? string.Join(", ", query.ReturnFields) + : "*"; + return $"SELECT {fields} FROM {query.Entity}"; + } + + private static string BuildWhereClause(IQueryFilter[] filters, FilterType filterType) + { + if (filters == null || !filters.Any()) + { + return string.Empty; + } + + var conditions = filters.Select(BuildCondition).ToArray(); + var conjunction = filterType == FilterType.And ? " AND " : " OR "; + return $"WHERE {string.Join(conjunction, conditions)}"; + } + + private static string BuildCondition(IQueryFilter filter) + { + var field = filter.FieldName; + var condition = filter.Condition.Name; + var value = filter.SearchVal != null ? $"'{filter.SearchVal}'" : "NULL"; + var subquery = filter.SearchTree != null ? $"({GenerateSql(filter.SearchTree)})" : string.Empty; + return condition switch + { + "null" => $"{field} IS NULL", + "notNull" => $"{field} IS NOT NULL", + "empty" => $"{field} = ''", + "notEmpty" => $"{field} <> ''", + "equals" => $"{field} = {value}", + "doesNotEqual" => $"{field} <> {value}", + "in" => $"{field} IN ({subquery})", + "notIn" => $"{field} NOT IN ({subquery})", + "contains" => $"{field} LIKE '%{filter.SearchVal}%'", + "doesNotContain" => $"{field} NOT LIKE '%{filter.SearchVal}%'", + "startsWith" => $"{field} LIKE '{filter.SearchVal}%'", + "endsWith" => $"{field} LIKE '%{filter.SearchVal}'", + "greaterThan" => $"{field} > {value}", + "lessThan" => $"{field} < {value}", + "greaterThanOrEqualTo" => $"{field} >= {value}", + "lessThanOrEqualTo" => $"{field} <= {value}", + _ => throw new NotImplementedException($"Condition '{condition}' is not implemented"), + }; + } } \ No newline at end of file From 020efd08d4e030912874ee601754f90578e43336 Mon Sep 17 00:00:00 2001 From: Javier Coitino Date: Sat, 19 Oct 2024 20:10:52 -0300 Subject: [PATCH 13/41] Resolve subqueries --- .../Controllers/QueryBuilderController.cs | 22 ++++++------- NorthwindCRUD/Controllers/QueryExecutor.cs | 33 ++++++++++--------- 2 files changed, 29 insertions(+), 26 deletions(-) diff --git a/NorthwindCRUD/Controllers/QueryBuilderController.cs b/NorthwindCRUD/Controllers/QueryBuilderController.cs index bb7c36f..60847ba 100644 --- a/NorthwindCRUD/Controllers/QueryBuilderController.cs +++ b/NorthwindCRUD/Controllers/QueryBuilderController.cs @@ -52,17 +52,17 @@ public ActionResult ExecuteQuery(Query query) var t = query.Entity.ToLower(CultureInfo.InvariantCulture); return Ok(new QueryBuilderResult { - 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, + Addresses = t == "addresses" ? mapper.Map(dataContext.Addresses.Run(query, dataContext)) : null, + Categories = t == "categories" ? mapper.Map(dataContext.Categories.Run(query, dataContext)) : null, + Products = t == "products" ? mapper.Map(dataContext.Products.Run(query, dataContext)) : null, + Regions = t == "regions" ? mapper.Map(dataContext.Regions.Run(query, dataContext)) : null, + Territories = t == "territories" ? mapper.Map(dataContext.Territories.Run(query, dataContext)) : null, + Employees = t == "employees" ? mapper.Map(dataContext.Employees.Run(query, dataContext)) : null, + Customers = t == "customers" ? mapper.Map(dataContext.Customers.Run(query, dataContext)) : null, + Orders = t == "orders" ? mapper.Map(dataContext.Orders.Run(query, dataContext)) : null, + OrderDetails = t == "orderdetails" ? mapper.Map(dataContext.OrderDetails.Run(query, dataContext)) : null, + Shippers = t == "shippers" ? mapper.Map(dataContext.Shippers.Run(query, dataContext)) : null, + Suppliers = t == "suppliers" ? mapper.Map(dataContext.Suppliers.Run(query, dataContext)) : null, }); } } \ No newline at end of file diff --git a/NorthwindCRUD/Controllers/QueryExecutor.cs b/NorthwindCRUD/Controllers/QueryExecutor.cs index ef1c32f..4e4b34b 100644 --- a/NorthwindCRUD/Controllers/QueryExecutor.cs +++ b/NorthwindCRUD/Controllers/QueryExecutor.cs @@ -4,6 +4,7 @@ using System.Globalization; using System.Linq.Expressions; using System.Reflection; +using Microsoft.EntityFrameworkCore; using Newtonsoft.Json; public enum FilterType @@ -113,14 +114,14 @@ public class QueryFilterConditionConverter : JsonConverter [SuppressMessage("StyleCop.CSharp.SpacingRules", "SA1025:Code should not contain multiple whitespace in a row", Justification = "...")] public static class QueryExecutor { - public static object[] Run(this IQueryable source, IQuery query) + public static object[] Run(this IQueryable source, IQuery query, DbContext db) { - return BuildQuery(source, query).ToArray(); + return BuildQuery(source, query, db).ToArray(); } - private static IQueryable BuildQuery(IQueryable source, IQuery query) + private static IQueryable BuildQuery(IQueryable source, IQuery query, DbContext db) { - var filterExpression = BuildExpression(query.FilteringOperands, query.Operator); + var filterExpression = BuildExpression(query.FilteringOperands, query.Operator, db); var filteredQuery = source.Where(filterExpression); if (query.ReturnFields != null && query.ReturnFields.Any()) { @@ -134,13 +135,13 @@ private static IQueryable BuildQuery(IQueryable source, IQuery q } } - private static Expression> BuildExpression(IQueryFilter[] filters, FilterType filterType) + private static Expression> BuildExpression(IQueryFilter[] filters, FilterType filterType, DbContext db) { var parameter = Expression.Parameter(typeof(TEntity), "entity"); var finalExpression = null as Expression; foreach (var filter in filters) { - var expression = BuildConditionExpression(filter, parameter); + var expression = BuildConditionExpression(filter, parameter, db); if (finalExpression == null) { finalExpression = expression; @@ -158,13 +159,13 @@ private static Expression> BuildExpression(IQueryFi : (TEntity _) => true; } - private static Expression BuildConditionExpression(IQueryFilter filter, ParameterExpression parameter) + private static Expression BuildConditionExpression(IQueryFilter filter, ParameterExpression parameter, DbContext db) { var property = typeof(TEntity).GetProperty(filter.FieldName, BindingFlags.IgnoreCase | BindingFlags.Public | BindingFlags.Instance) ?? throw new InvalidOperationException($"Property '{filter.FieldName}' not found on type '{typeof(TEntity)}'"); var field = Expression.Property(parameter, property); var targetType = GetPropertyType(property); var searchValue = GetSearchValue(filter.SearchVal, targetType); - var searchTree = BuildSubquery(filter.SearchTree); + var searchTree = BuildSubquery(filter.SearchTree, db); Expression condition = filter.Condition.Name switch { "null" => Expression.Equal(field, Expression.Constant(null, targetType)), @@ -196,17 +197,17 @@ private static Expression BuildConditionExpression(IQueryFilter filter, return condition; } - private static Expression BuildSubquery(IQuery? query) + private static Expression BuildSubquery(IQuery? query, DbContext db) { - if (query == null) + if (query == null || db == null) { return Expression.Constant(true); } - var parameter = Expression.Parameter(typeof(object), "entity"); - var filterExpression = BuildExpression(query.FilteringOperands, query.Operator); - var lambda = Expression.Lambda>(filterExpression.Body, parameter); - return lambda.Body; + var dbSetProperty = db.GetType().GetProperty(query.Entity) ?? throw new InvalidOperationException($"Entity '{query.Entity}' not found in the DbContext."); + var dbSet = dbSetProperty.GetValue(db) as IQueryable ?? throw new InvalidOperationException($"Unable to get IQueryable for entity '{query.Entity}'."); + var subquery = BuildQuery(dbSet, query, db); + return subquery.Expression; } private static Type GetPropertyType(PropertyInfo property) @@ -249,7 +250,6 @@ private static Expression> BuildProjectionExpression $"{field} < {value}", "greaterThanOrEqualTo" => $"{field} >= {value}", "lessThanOrEqualTo" => $"{field} <= {value}", + "all" => throw new NotImplementedException("Not implemented"), + "true" => $"{field} = TRUE", + "false" => $"{field} = FALSE", _ => throw new NotImplementedException($"Condition '{condition}' is not implemented"), }; } From 5ba3441dc27ddb3f3164270e364f01d66cdccb51 Mon Sep 17 00:00:00 2001 From: Javier Coitino Date: Sat, 19 Oct 2024 21:39:15 -0300 Subject: [PATCH 14/41] Type issue fixed --- NorthwindCRUD/Controllers/QueryExecutor.cs | 38 +++++++++++----------- 1 file changed, 19 insertions(+), 19 deletions(-) diff --git a/NorthwindCRUD/Controllers/QueryExecutor.cs b/NorthwindCRUD/Controllers/QueryExecutor.cs index 4e4b34b..e826950 100644 --- a/NorthwindCRUD/Controllers/QueryExecutor.cs +++ b/NorthwindCRUD/Controllers/QueryExecutor.cs @@ -6,6 +6,7 @@ using System.Reflection; using Microsoft.EntityFrameworkCore; using Newtonsoft.Json; +using Swashbuckle.AspNetCore.SwaggerGen; public enum FilterType { @@ -114,25 +115,24 @@ public class QueryFilterConditionConverter : JsonConverter [SuppressMessage("StyleCop.CSharp.SpacingRules", "SA1025:Code should not contain multiple whitespace in a row", Justification = "...")] public static class QueryExecutor { - public static object[] Run(this IQueryable source, IQuery query, DbContext db) + public static TEntity[] Run(this IQueryable source, IQuery query, DbContext db) { return BuildQuery(source, query, db).ToArray(); } - private static IQueryable BuildQuery(IQueryable source, IQuery query, DbContext db) + private static IQueryable BuildQuery(IQueryable source, IQuery query, DbContext db) { - var filterExpression = BuildExpression(query.FilteringOperands, query.Operator, db); + var filterExpression = BuildExpression(query.FilteringOperands, query.Operator, db); var filteredQuery = source.Where(filterExpression); - if (query.ReturnFields != null && query.ReturnFields.Any()) - { - var projectionExpression = BuildProjectionExpression(query.ReturnFields); - var projectedQuery = filteredQuery.Select(projectionExpression); - return projectedQuery; - } - else - { - return filteredQuery; - } + + // TODO: project requested columns + // if (query.ReturnFields != null && query.ReturnFields.Any()) + // { + // var projectionExpression = BuildProjectionExpression(query.ReturnFields); + // var projectedQuery = filteredQuery.Select(projectionExpression); + // return projectedQuery; + // } + return filteredQuery; } private static Expression> BuildExpression(IQueryFilter[] filters, FilterType filterType, DbContext db) @@ -163,15 +163,15 @@ private static Expression BuildConditionExpression(IQueryFilter filter, { var property = typeof(TEntity).GetProperty(filter.FieldName, BindingFlags.IgnoreCase | BindingFlags.Public | BindingFlags.Instance) ?? throw new InvalidOperationException($"Property '{filter.FieldName}' not found on type '{typeof(TEntity)}'"); var field = Expression.Property(parameter, property); - var targetType = GetPropertyType(property); + var targetType = property.PropertyType; var searchValue = GetSearchValue(filter.SearchVal, targetType); var searchTree = BuildSubquery(filter.SearchTree, db); Expression condition = filter.Condition.Name switch { - "null" => Expression.Equal(field, Expression.Constant(null, targetType)), - "notNull" => Expression.NotEqual(field, Expression.Constant(null, targetType)), - "empty" => Expression.Equal(field, Expression.Constant(string.Empty, targetType)), // TODO: Implement empty condition - "notEmpty" => Expression.NotEqual(field, Expression.Constant(string.Empty, targetType)), // TODO: Implement not empty condition + "null" => Expression.Equal(field, Expression.Constant(targetType.GetDefaultValue())), + "notNull" => Expression.NotEqual(field, Expression.Constant(targetType.GetDefaultValue())), + "empty" => Expression.Equal(field, Expression.Constant(targetType.GetDefaultValue())), + "notEmpty" => Expression.NotEqual(field, Expression.Constant(targetType.GetDefaultValue())), "equals" => Expression.Equal(field, searchValue), "doesNotEqual" => Expression.NotEqual(field, searchValue), "in" => Expression.Call(typeof(Enumerable), "Contains", new[] { targetType }, searchTree, field), @@ -227,7 +227,7 @@ private static Expression GetSearchValue(object? value, Type targetType) { if (value == null) { - return Expression.Constant(null, targetType); + return Expression.Constant(Activator.CreateInstance(targetType), targetType); } var nonNullableType = Nullable.GetUnderlyingType(targetType) ?? targetType; From a76c041c31095618fa18fb90502878041efdbcc0 Mon Sep 17 00:00:00 2001 From: Javier Coitino Date: Sat, 19 Oct 2024 22:04:13 -0300 Subject: [PATCH 15/41] Handle nullables --- NorthwindCRUD/Controllers/QueryExecutor.cs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/NorthwindCRUD/Controllers/QueryExecutor.cs b/NorthwindCRUD/Controllers/QueryExecutor.cs index e826950..7022f3d 100644 --- a/NorthwindCRUD/Controllers/QueryExecutor.cs +++ b/NorthwindCRUD/Controllers/QueryExecutor.cs @@ -4,6 +4,7 @@ using System.Globalization; using System.Linq.Expressions; using System.Reflection; +using AutoMapper.Internal; using Microsoft.EntityFrameworkCore; using Newtonsoft.Json; using Swashbuckle.AspNetCore.SwaggerGen; @@ -168,8 +169,8 @@ private static Expression BuildConditionExpression(IQueryFilter filter, var searchTree = BuildSubquery(filter.SearchTree, db); Expression condition = filter.Condition.Name switch { - "null" => Expression.Equal(field, Expression.Constant(targetType.GetDefaultValue())), - "notNull" => Expression.NotEqual(field, Expression.Constant(targetType.GetDefaultValue())), + "null" => targetType.IsNullableType() ? Expression.Equal(field, Expression.Constant(targetType.GetDefaultValue())) : Expression.Constant(false), + "notNull" => targetType.IsNullableType() ? Expression.NotEqual(field, Expression.Constant(targetType.GetDefaultValue())) : Expression.Constant(true), "empty" => Expression.Equal(field, Expression.Constant(targetType.GetDefaultValue())), "notEmpty" => Expression.NotEqual(field, Expression.Constant(targetType.GetDefaultValue())), "equals" => Expression.Equal(field, searchValue), From 556115d65614b91ac0059137155f8ed96f5475d6 Mon Sep 17 00:00:00 2001 From: Javier Coitino Date: Sun, 20 Oct 2024 01:08:36 -0300 Subject: [PATCH 16/41] subquery issue fixed --- NorthwindCRUD/Controllers/QueryExecutor.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/NorthwindCRUD/Controllers/QueryExecutor.cs b/NorthwindCRUD/Controllers/QueryExecutor.cs index 7022f3d..76e0c8b 100644 --- a/NorthwindCRUD/Controllers/QueryExecutor.cs +++ b/NorthwindCRUD/Controllers/QueryExecutor.cs @@ -205,7 +205,7 @@ private static Expression BuildSubquery(IQuery? query, DbContext db) return Expression.Constant(true); } - var dbSetProperty = db.GetType().GetProperty(query.Entity) ?? throw new InvalidOperationException($"Entity '{query.Entity}' not found in the DbContext."); + var dbSetProperty = db.GetType().GetProperty(query.Entity, BindingFlags.IgnoreCase | BindingFlags.Public | BindingFlags.Instance) ?? throw new InvalidOperationException($"Entity '{query.Entity}' not found in the DbContext."); var dbSet = dbSetProperty.GetValue(db) as IQueryable ?? throw new InvalidOperationException($"Unable to get IQueryable for entity '{query.Entity}'."); var subquery = BuildQuery(dbSet, query, db); return subquery.Expression; @@ -228,7 +228,7 @@ private static Expression GetSearchValue(object? value, Type targetType) { if (value == null) { - return Expression.Constant(Activator.CreateInstance(targetType), targetType); + return Expression.Constant(targetType.GetDefaultValue()); } var nonNullableType = Nullable.GetUnderlyingType(targetType) ?? targetType; From 67253484f21772e32e3a520913bdc01386219cfe Mon Sep 17 00:00:00 2001 From: Javier Coitino Date: Sun, 20 Oct 2024 11:31:48 -0300 Subject: [PATCH 17/41] Code polishing --- .../Controllers/QueryBuilderController.cs | 22 +++++------ NorthwindCRUD/Controllers/QueryExecutor.cs | 37 ++++++++----------- 2 files changed, 26 insertions(+), 33 deletions(-) diff --git a/NorthwindCRUD/Controllers/QueryBuilderController.cs b/NorthwindCRUD/Controllers/QueryBuilderController.cs index 60847ba..bb7c36f 100644 --- a/NorthwindCRUD/Controllers/QueryBuilderController.cs +++ b/NorthwindCRUD/Controllers/QueryBuilderController.cs @@ -52,17 +52,17 @@ public ActionResult ExecuteQuery(Query query) var t = query.Entity.ToLower(CultureInfo.InvariantCulture); return Ok(new QueryBuilderResult { - Addresses = t == "addresses" ? mapper.Map(dataContext.Addresses.Run(query, dataContext)) : null, - Categories = t == "categories" ? mapper.Map(dataContext.Categories.Run(query, dataContext)) : null, - Products = t == "products" ? mapper.Map(dataContext.Products.Run(query, dataContext)) : null, - Regions = t == "regions" ? mapper.Map(dataContext.Regions.Run(query, dataContext)) : null, - Territories = t == "territories" ? mapper.Map(dataContext.Territories.Run(query, dataContext)) : null, - Employees = t == "employees" ? mapper.Map(dataContext.Employees.Run(query, dataContext)) : null, - Customers = t == "customers" ? mapper.Map(dataContext.Customers.Run(query, dataContext)) : null, - Orders = t == "orders" ? mapper.Map(dataContext.Orders.Run(query, dataContext)) : null, - OrderDetails = t == "orderdetails" ? mapper.Map(dataContext.OrderDetails.Run(query, dataContext)) : null, - Shippers = t == "shippers" ? mapper.Map(dataContext.Shippers.Run(query, dataContext)) : null, - Suppliers = t == "suppliers" ? mapper.Map(dataContext.Suppliers.Run(query, dataContext)) : null, + 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, }); } } \ No newline at end of file diff --git a/NorthwindCRUD/Controllers/QueryExecutor.cs b/NorthwindCRUD/Controllers/QueryExecutor.cs index 76e0c8b..a4d7687 100644 --- a/NorthwindCRUD/Controllers/QueryExecutor.cs +++ b/NorthwindCRUD/Controllers/QueryExecutor.cs @@ -6,6 +6,7 @@ using System.Reflection; using AutoMapper.Internal; using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; using Newtonsoft.Json; using Swashbuckle.AspNetCore.SwaggerGen; @@ -116,14 +117,18 @@ public class QueryFilterConditionConverter : JsonConverter [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, IQuery query, DbContext db) + public static TEntity[] Run(this IQueryable source, IQuery query) { - return BuildQuery(source, query, db).ToArray(); + var infrastructure = source as IInfrastructure; + var serviceProvider = infrastructure!.Instance; + var currentDbContext = serviceProvider.GetService(typeof(ICurrentDbContext)) as ICurrentDbContext; + var db = currentDbContext!.Context; + return BuildQuery(db, source, query).ToArray(); } - private static IQueryable BuildQuery(IQueryable source, IQuery query, DbContext db) + private static IQueryable BuildQuery(DbContext db, IQueryable source, IQuery query) { - var filterExpression = BuildExpression(query.FilteringOperands, query.Operator, db); + var filterExpression = BuildExpression(db, source, query.FilteringOperands, query.Operator); var filteredQuery = source.Where(filterExpression); // TODO: project requested columns @@ -136,13 +141,13 @@ private static IQueryable BuildQuery(IQueryable sourc return filteredQuery; } - private static Expression> BuildExpression(IQueryFilter[] filters, FilterType filterType, DbContext db) + private static Expression> BuildExpression(DbContext db, IQueryable source, IQueryFilter[] filters, FilterType filterType) { var parameter = Expression.Parameter(typeof(TEntity), "entity"); var finalExpression = null as Expression; foreach (var filter in filters) { - var expression = BuildConditionExpression(filter, parameter, db); + var expression = BuildConditionExpression(db, source, filter, parameter); if (finalExpression == null) { finalExpression = expression; @@ -160,9 +165,10 @@ private static Expression> BuildExpression(IQueryFi : (TEntity _) => true; } - private static Expression BuildConditionExpression(IQueryFilter filter, ParameterExpression parameter, DbContext db) + private static Expression BuildConditionExpression(DbContext db, IQueryable source, IQueryFilter filter, ParameterExpression parameter) { - var property = typeof(TEntity).GetProperty(filter.FieldName, BindingFlags.IgnoreCase | BindingFlags.Public | BindingFlags.Instance) ?? throw new InvalidOperationException($"Property '{filter.FieldName}' not found on type '{typeof(TEntity)}'"); + 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}'"); var field = Expression.Property(parameter, property); var targetType = property.PropertyType; var searchValue = GetSearchValue(filter.SearchVal, targetType); @@ -207,23 +213,10 @@ private static Expression BuildSubquery(IQuery? query, DbContext db) var dbSetProperty = db.GetType().GetProperty(query.Entity, BindingFlags.IgnoreCase | BindingFlags.Public | BindingFlags.Instance) ?? throw new InvalidOperationException($"Entity '{query.Entity}' not found in the DbContext."); var dbSet = dbSetProperty.GetValue(db) as IQueryable ?? throw new InvalidOperationException($"Unable to get IQueryable for entity '{query.Entity}'."); - var subquery = BuildQuery(dbSet, query, db); + var subquery = BuildQuery(db, dbSet, query); return subquery.Expression; } - private static Type GetPropertyType(PropertyInfo property) - { - var targetType = property.PropertyType; - if (targetType.IsGenericType && targetType.GetGenericTypeDefinition() == typeof(Nullable<>)) - { - return Nullable.GetUnderlyingType(targetType) ?? targetType; - } - else - { - return targetType; - } - } - private static Expression GetSearchValue(object? value, Type targetType) { if (value == null) From c0e10b7be69a084591f8f4fda21691fdf50b5dbd Mon Sep 17 00:00:00 2001 From: Javier Coitino Date: Sun, 20 Oct 2024 12:33:34 -0300 Subject: [PATCH 18/41] Fix subquery type --- NorthwindCRUD/Controllers/QueryExecutor.cs | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/NorthwindCRUD/Controllers/QueryExecutor.cs b/NorthwindCRUD/Controllers/QueryExecutor.cs index a4d7687..e6a2f6d 100644 --- a/NorthwindCRUD/Controllers/QueryExecutor.cs +++ b/NorthwindCRUD/Controllers/QueryExecutor.cs @@ -211,12 +211,16 @@ private static Expression BuildSubquery(IQuery? query, DbContext db) return Expression.Constant(true); } - var dbSetProperty = db.GetType().GetProperty(query.Entity, BindingFlags.IgnoreCase | BindingFlags.Public | BindingFlags.Instance) ?? throw new InvalidOperationException($"Entity '{query.Entity}' not found in the DbContext."); - var dbSet = dbSetProperty.GetValue(db) as IQueryable ?? throw new InvalidOperationException($"Unable to get IQueryable for entity '{query.Entity}'."); - var subquery = BuildQuery(db, dbSet, query); - return subquery.Expression; + var dbSetProperty = db.GetType().GetProperty(query.Entity, BindingFlags.IgnoreCase | BindingFlags.Public | BindingFlags.Instance) + ?? throw new InvalidOperationException($"Entity '{query.Entity}' not found in the DbContext."); + var dbSet = dbSetProperty.GetValue(db) + ?? throw new InvalidOperationException($"Unable to get IQueryable for entity '{query.Entity}'."); + var buildQuery = typeof(QueryExecutor).GetMethod(nameof(BuildQuery), BindingFlags.NonPublic | BindingFlags.Static)!.MakeGenericMethod(dbSet.GetType().GetGenericArguments()[0]); + var subquery = buildQuery.Invoke(null, new object[] { db, dbSet, query }) as IQueryable; + return subquery!.Expression; } + private static Expression GetSearchValue(object? value, Type targetType) { if (value == null) From 0bd1be31e6185c4aa1d303f7236dcdf83d8443f3 Mon Sep 17 00:00:00 2001 From: Javier Coitino Date: Sun, 20 Oct 2024 13:36:57 -0300 Subject: [PATCH 19/41] Fix subqueries type issue --- NorthwindCRUD/Controllers/QueryExecutor.cs | 21 ++++++++++++--------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/NorthwindCRUD/Controllers/QueryExecutor.cs b/NorthwindCRUD/Controllers/QueryExecutor.cs index e6a2f6d..59e5f68 100644 --- a/NorthwindCRUD/Controllers/QueryExecutor.cs +++ b/NorthwindCRUD/Controllers/QueryExecutor.cs @@ -172,7 +172,6 @@ private static Expression BuildConditionExpression(DbContext db, IQuery var field = Expression.Property(parameter, property); var targetType = property.PropertyType; var searchValue = GetSearchValue(filter.SearchVal, targetType); - var searchTree = BuildSubquery(filter.SearchTree, db); Expression condition = filter.Condition.Name switch { "null" => targetType.IsNullableType() ? Expression.Equal(field, Expression.Constant(targetType.GetDefaultValue())) : Expression.Constant(false), @@ -181,8 +180,8 @@ private static Expression BuildConditionExpression(DbContext db, IQuery "notEmpty" => Expression.NotEqual(field, Expression.Constant(targetType.GetDefaultValue())), "equals" => Expression.Equal(field, searchValue), "doesNotEqual" => Expression.NotEqual(field, searchValue), - "in" => Expression.Call(typeof(Enumerable), "Contains", new[] { targetType }, searchTree, field), - "notIn" => Expression.Not(Expression.Call(typeof(Enumerable), "Contains", new[] { targetType }, searchTree, field)), + "in" => BuildSubquery(db, filter.SearchTree, field, filter.FieldName, targetType), + "notIn" => Expression.Not(BuildSubquery(db, filter.SearchTree, field, filter.FieldName, targetType)), "contains" => Expression.Call(field, typeof(string).GetMethod("Contains", new[] { typeof(string) })!, searchValue), "doesNotContain" => Expression.Not(Expression.Call(field, typeof(string).GetMethod("Contains", new[] { typeof(string) })!, searchValue)), "startsWith" => Expression.Call(field, typeof(string).GetMethod("StartsWith", new[] { typeof(string) })!, searchValue), @@ -204,7 +203,7 @@ private static Expression BuildConditionExpression(DbContext db, IQuery return condition; } - private static Expression BuildSubquery(IQuery? query, DbContext db) + private static Expression BuildSubquery(DbContext db, IQuery? query, MemberExpression field, string fieldName, Type targetType) { if (query == null || db == null) { @@ -213,14 +212,18 @@ private static Expression BuildSubquery(IQuery? query, DbContext db) var dbSetProperty = db.GetType().GetProperty(query.Entity, BindingFlags.IgnoreCase | BindingFlags.Public | BindingFlags.Instance) ?? throw new InvalidOperationException($"Entity '{query.Entity}' not found in the DbContext."); - var dbSet = dbSetProperty.GetValue(db) + var dbSet = dbSetProperty.GetValue(db) ?? throw new InvalidOperationException($"Unable to get IQueryable for entity '{query.Entity}'."); - var buildQuery = typeof(QueryExecutor).GetMethod(nameof(BuildQuery), BindingFlags.NonPublic | BindingFlags.Static)!.MakeGenericMethod(dbSet.GetType().GetGenericArguments()[0]); - var subquery = buildQuery.Invoke(null, new object[] { db, dbSet, query }) as IQueryable; - return subquery!.Expression; + var buildQuery = typeof(QueryExecutor).GetMethod(nameof(BuildQuery), BindingFlags.NonPublic | BindingFlags.Static)!.MakeGenericMethod(dbSet.GetType().GetGenericArguments()[0]); + var subquery = buildQuery.Invoke(null, new object[] { db, dbSet, query }) as IQueryable; + var parameter = Expression.Parameter(subquery!.ElementType, "x"); + var property = Expression.Property(parameter, fieldName); + var lambda = Expression.Lambda(property, parameter); + var projectedSubquery = Expression.Call(typeof(Queryable), "Select", new Type[] { subquery.ElementType, property.Type }, subquery.Expression, lambda ); + var containsMethod = typeof(Queryable).GetMethods().First(m => m.Name == "Contains" && m.GetParameters().Length == 2).MakeGenericMethod(property.Type); + return Expression.Call(containsMethod, projectedSubquery, field); } - private static Expression GetSearchValue(object? value, Type targetType) { if (value == null) From 5c70ddcbe9ece5ae2429fa3be01d2a1b1bf58533 Mon Sep 17 00:00:00 2001 From: Javier Coitino Date: Mon, 21 Oct 2024 17:19:20 -0300 Subject: [PATCH 20/41] Fix crash when null strings used in conditions --- NorthwindCRUD/Controllers/QueryExecutor.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/NorthwindCRUD/Controllers/QueryExecutor.cs b/NorthwindCRUD/Controllers/QueryExecutor.cs index 59e5f68..ba99d8d 100644 --- a/NorthwindCRUD/Controllers/QueryExecutor.cs +++ b/NorthwindCRUD/Controllers/QueryExecutor.cs @@ -228,7 +228,7 @@ private static Expression GetSearchValue(object? value, Type targetType) { if (value == null) { - return Expression.Constant(targetType.GetDefaultValue()); + return Expression.Constant(targetType == typeof(string) ? string.Empty : targetType.GetDefaultValue()); } var nonNullableType = Nullable.GetUnderlyingType(targetType) ?? targetType; From e1473bf3f3791ffb182701725748263b9ba8c3b5 Mon Sep 17 00:00:00 2001 From: Javier Coitino Date: Mon, 21 Oct 2024 17:53:23 -0300 Subject: [PATCH 21/41] Handle empty and notEmpty --- NorthwindCRUD/Controllers/QueryExecutor.cs | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/NorthwindCRUD/Controllers/QueryExecutor.cs b/NorthwindCRUD/Controllers/QueryExecutor.cs index ba99d8d..8a74b3b 100644 --- a/NorthwindCRUD/Controllers/QueryExecutor.cs +++ b/NorthwindCRUD/Controllers/QueryExecutor.cs @@ -172,12 +172,13 @@ private static Expression BuildConditionExpression(DbContext db, IQuery var field = Expression.Property(parameter, property); var targetType = property.PropertyType; var searchValue = GetSearchValue(filter.SearchVal, targetType); + var emptyValue = GetEmptyValue(targetType); Expression condition = filter.Condition.Name switch { "null" => targetType.IsNullableType() ? Expression.Equal(field, Expression.Constant(targetType.GetDefaultValue())) : Expression.Constant(false), "notNull" => targetType.IsNullableType() ? Expression.NotEqual(field, Expression.Constant(targetType.GetDefaultValue())) : Expression.Constant(true), - "empty" => Expression.Equal(field, Expression.Constant(targetType.GetDefaultValue())), - "notEmpty" => Expression.NotEqual(field, Expression.Constant(targetType.GetDefaultValue())), + "empty" => Expression.Equal(field, emptyValue), + "notEmpty" => Expression.NotEqual(field, emptyValue), "equals" => Expression.Equal(field, searchValue), "doesNotEqual" => Expression.NotEqual(field, searchValue), "in" => BuildSubquery(db, filter.SearchTree, field, filter.FieldName, targetType), @@ -228,7 +229,7 @@ private static Expression GetSearchValue(object? value, Type targetType) { if (value == null) { - return Expression.Constant(targetType == typeof(string) ? string.Empty : targetType.GetDefaultValue()); + return GetEmptyValue(targetType); } var nonNullableType = Nullable.GetUnderlyingType(targetType) ?? targetType; @@ -236,6 +237,11 @@ private static Expression GetSearchValue(object? value, Type targetType) return Expression.Constant(convertedValue, targetType); } + private static Expression GetEmptyValue(Type targetType) + { + return Expression.Constant(targetType == typeof(string) ? string.Empty : targetType.GetDefaultValue()); + } + private static Expression> BuildProjectionExpression(string[] returnFields) { var parameter = Expression.Parameter(typeof(TEntity), "entity"); From 28a1b44b6e4850d0be1634852ad6a9b9f8e54835 Mon Sep 17 00:00:00 2001 From: Javier Coitino Date: Mon, 21 Oct 2024 17:56:55 -0300 Subject: [PATCH 22/41] Empty includes isNull, I guess --- NorthwindCRUD/Controllers/QueryExecutor.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/NorthwindCRUD/Controllers/QueryExecutor.cs b/NorthwindCRUD/Controllers/QueryExecutor.cs index 8a74b3b..95923c1 100644 --- a/NorthwindCRUD/Controllers/QueryExecutor.cs +++ b/NorthwindCRUD/Controllers/QueryExecutor.cs @@ -177,8 +177,8 @@ private static Expression BuildConditionExpression(DbContext db, IQuery { "null" => targetType.IsNullableType() ? Expression.Equal(field, Expression.Constant(targetType.GetDefaultValue())) : Expression.Constant(false), "notNull" => targetType.IsNullableType() ? Expression.NotEqual(field, Expression.Constant(targetType.GetDefaultValue())) : Expression.Constant(true), - "empty" => Expression.Equal(field, emptyValue), - "notEmpty" => Expression.NotEqual(field, emptyValue), + "empty" => Expression.Or(Expression.Equal(field, emptyValue), targetType.IsNullableType() ? Expression.Equal(field, Expression.Constant(targetType.GetDefaultValue())) : Expression.Constant(false)), + "notEmpty" => Expression.And(Expression.NotEqual(field, emptyValue), targetType.IsNullableType() ? Expression.NotEqual(field, Expression.Constant(targetType.GetDefaultValue())) : Expression.Constant(true)), "equals" => Expression.Equal(field, searchValue), "doesNotEqual" => Expression.NotEqual(field, searchValue), "in" => BuildSubquery(db, filter.SearchTree, field, filter.FieldName, targetType), From 47548f283378a488651050190ed56de61d48425e Mon Sep 17 00:00:00 2001 From: Javier Coitino Date: Tue, 22 Oct 2024 00:21:48 -0300 Subject: [PATCH 23/41] Discriminated union imp for Query Filters --- NorthwindCRUD/Controllers/QueryExecutor.cs | 107 +++++++++++++-------- 1 file changed, 67 insertions(+), 40 deletions(-) diff --git a/NorthwindCRUD/Controllers/QueryExecutor.cs b/NorthwindCRUD/Controllers/QueryExecutor.cs index 95923c1..4a5ae78 100644 --- a/NorthwindCRUD/Controllers/QueryExecutor.cs +++ b/NorthwindCRUD/Controllers/QueryExecutor.cs @@ -30,15 +30,22 @@ public interface IQuery public interface IQueryFilter { - public string FieldName { get; set; } + // Basic condition + public string? FieldName { get; set; } - public bool IgnoreCase { get; set; } + public bool? IgnoreCase { get; set; } - public IQueryFilterCondition Condition { get; set; } + public IQueryFilterCondition? Condition { get; set; } public object? SearchVal { get; set; } public IQuery? SearchTree { get; set; } + + // And/Or + [SuppressMessage("Naming", "CA1716:Identifiers should not match keywords", Justification = "required name")] + public FilterType? Operator { get; set; } + + public IQueryFilter[] FilteringOperands { get; set; } } public interface IQueryFilterCondition @@ -63,15 +70,21 @@ public class Query : IQuery public class QueryFilter : IQueryFilter { - public string FieldName { get; set; } + // Basic condition + public string? FieldName { get; set; } - public bool IgnoreCase { get; set; } + public bool? IgnoreCase { get; set; } - public IQueryFilterCondition Condition { get; set; } + public IQueryFilterCondition? Condition { get; set; } public object? SearchVal { get; set; } public IQuery? SearchTree { get; set; } + + // And/Or + public FilterType? Operator { get => throw new NotImplementedException(); set => throw new NotImplementedException(); } + + public IQueryFilter[] FilteringOperands { get => throw new NotImplementedException(); set => throw new NotImplementedException(); } } public class QueryFilterCondition : IQueryFilterCondition @@ -114,7 +127,6 @@ public class QueryFilterConditionConverter : JsonConverter /// A generic query executor that can be used to execute queries on IQueryable data sources. /// [SuppressMessage("StyleCop.CSharp.SpacingRules", "SA1009:Closing parenthesis should be spaced correctly", Justification = "...")] -[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, IQuery query) @@ -167,41 +179,56 @@ private static Expression> BuildExpression(DbContex private static Expression BuildConditionExpression(DbContext db, IQueryable source, IQueryFilter filter, ParameterExpression parameter) { - 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}'"); - var field = Expression.Property(parameter, property); - var targetType = property.PropertyType; - var searchValue = GetSearchValue(filter.SearchVal, targetType); - var emptyValue = GetEmptyValue(targetType); - Expression condition = filter.Condition.Name switch - { - "null" => targetType.IsNullableType() ? Expression.Equal(field, Expression.Constant(targetType.GetDefaultValue())) : Expression.Constant(false), - "notNull" => targetType.IsNullableType() ? Expression.NotEqual(field, Expression.Constant(targetType.GetDefaultValue())) : Expression.Constant(true), - "empty" => Expression.Or(Expression.Equal(field, emptyValue), targetType.IsNullableType() ? Expression.Equal(field, Expression.Constant(targetType.GetDefaultValue())) : Expression.Constant(false)), - "notEmpty" => Expression.And(Expression.NotEqual(field, emptyValue), targetType.IsNullableType() ? Expression.NotEqual(field, Expression.Constant(targetType.GetDefaultValue())) : Expression.Constant(true)), - "equals" => Expression.Equal(field, searchValue), - "doesNotEqual" => Expression.NotEqual(field, searchValue), - "in" => BuildSubquery(db, filter.SearchTree, field, filter.FieldName, targetType), - "notIn" => Expression.Not(BuildSubquery(db, filter.SearchTree, field, filter.FieldName, targetType)), - "contains" => Expression.Call(field, typeof(string).GetMethod("Contains", new[] { typeof(string) })!, searchValue), - "doesNotContain" => Expression.Not(Expression.Call(field, typeof(string).GetMethod("Contains", new[] { typeof(string) })!, searchValue)), - "startsWith" => Expression.Call(field, typeof(string).GetMethod("StartsWith", new[] { typeof(string) })!, searchValue), - "endsWith" => Expression.Call(field, typeof(string).GetMethod("EndsWith", new[] { typeof(string) })!, searchValue), - "greaterThan" => Expression.GreaterThan(field, searchValue), - "lessThan" => Expression.LessThan(field, searchValue), - "greaterThanOrEqualTo" => Expression.GreaterThanOrEqual(field, searchValue), - "lessThanOrEqualTo" => Expression.LessThanOrEqual(field, searchValue), - "all" => throw new NotImplementedException("Not implemented"), - "true" => Expression.IsTrue(field), - "false" => Expression.IsFalse(field), - _ => throw new NotImplementedException("Not implemented"), - }; - if (filter.IgnoreCase && field.Type == typeof(string)) + if (filter.FieldName is not null && filter.IgnoreCase is not null && filter.Condition is not null) { - // TODO: Implement case-insensitive comparison + 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}'"); + var field = Expression.Property(parameter, property); + var targetType = property.PropertyType; + var searchValue = GetSearchValue(filter.SearchVal, targetType); + var emptyValue = GetEmptyValue(targetType); + Expression condition = filter.Condition.Name switch + { + "null" => targetType.IsNullableType() ? Expression.Equal(field, Expression.Constant(targetType.GetDefaultValue())) : Expression.Constant(false), + "notNull" => targetType.IsNullableType() ? Expression.NotEqual(field, Expression.Constant(targetType.GetDefaultValue())) : Expression.Constant(true), + "empty" => Expression.Or(Expression.Equal(field, emptyValue), targetType.IsNullableType() ? Expression.Equal(field, Expression.Constant(targetType.GetDefaultValue())) : Expression.Constant(false)), + "notEmpty" => Expression.And(Expression.NotEqual(field, emptyValue), targetType.IsNullableType() ? Expression.NotEqual(field, Expression.Constant(targetType.GetDefaultValue())) : Expression.Constant(true)), + "equals" => Expression.Equal(field, searchValue), + "doesNotEqual" => Expression.NotEqual(field, searchValue), + "in" => BuildSubquery(db, filter.SearchTree, field, filter.FieldName, targetType), + "notIn" => Expression.Not(BuildSubquery(db, filter.SearchTree, field, filter.FieldName, targetType)), + "contains" => Expression.Call(field, typeof(string).GetMethod("Contains", new[] { typeof(string) })!, searchValue), + "doesNotContain" => Expression.Not(Expression.Call(field, typeof(string).GetMethod("Contains", new[] { typeof(string) })!, searchValue)), + "startsWith" => Expression.Call(field, typeof(string).GetMethod("StartsWith", new[] { typeof(string) })!, searchValue), + "endsWith" => Expression.Call(field, typeof(string).GetMethod("EndsWith", new[] { typeof(string) })!, searchValue), + "greaterThan" => Expression.GreaterThan(field, searchValue), + "lessThan" => Expression.LessThan(field, searchValue), + "greaterThanOrEqualTo" => Expression.GreaterThanOrEqual(field, searchValue), + "lessThanOrEqualTo" => Expression.LessThanOrEqual(field, searchValue), + "all" => throw new NotImplementedException("Not implemented"), + "true" => Expression.IsTrue(field), + "false" => Expression.IsFalse(field), + _ => throw new NotImplementedException("Not implemented"), + }; + if (filter.IgnoreCase.Value && field.Type == typeof(string)) + { + // TODO: Implement case-insensitive comparison + } + + return condition; } + else + { + var subexpressions = filter.FilteringOperands?.Select(f => BuildConditionExpression(db, source, f, parameter)).ToArray(); + if (subexpressions == null || !subexpressions.Any()) + { + return Expression.Constant(true); + } - return condition; + return filter.Operator == FilterType.And + ? subexpressions.Aggregate(Expression.AndAlso) + : subexpressions.Aggregate(Expression.OrElse); + } } private static Expression BuildSubquery(DbContext db, IQuery? query, MemberExpression field, string fieldName, Type targetType) @@ -290,7 +317,7 @@ private static string BuildWhereClause(IQueryFilter[] filters, FilterType filter private static string BuildCondition(IQueryFilter filter) { var field = filter.FieldName; - var condition = filter.Condition.Name; + var condition = filter.Condition?.Name; var value = filter.SearchVal != null ? $"'{filter.SearchVal}'" : "NULL"; var subquery = filter.SearchTree != null ? $"({GenerateSql(filter.SearchTree)})" : string.Empty; return condition switch From b45da9158bcabdc7fcbf636c9d61e3e5d78445bd Mon Sep 17 00:00:00 2001 From: Javier Coitino Date: Tue, 22 Oct 2024 00:51:04 -0300 Subject: [PATCH 24/41] Remove NIEs --- NorthwindCRUD/Controllers/QueryExecutor.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/NorthwindCRUD/Controllers/QueryExecutor.cs b/NorthwindCRUD/Controllers/QueryExecutor.cs index 4a5ae78..148f996 100644 --- a/NorthwindCRUD/Controllers/QueryExecutor.cs +++ b/NorthwindCRUD/Controllers/QueryExecutor.cs @@ -82,9 +82,9 @@ public class QueryFilter : IQueryFilter public IQuery? SearchTree { get; set; } // And/Or - public FilterType? Operator { get => throw new NotImplementedException(); set => throw new NotImplementedException(); } + public FilterType? Operator { get; set; } - public IQueryFilter[] FilteringOperands { get => throw new NotImplementedException(); set => throw new NotImplementedException(); } + public IQueryFilter[] FilteringOperands { get; set; } } public class QueryFilterCondition : IQueryFilterCondition From 02a216c3622d2f0dd9f1f9bb8395cc810eb3a1d8 Mon Sep 17 00:00:00 2001 From: Javier Coitino Date: Wed, 23 Oct 2024 17:56:14 -0300 Subject: [PATCH 25/41] Enable column projection --- NorthwindCRUD/Controllers/QueryExecutor.cs | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/NorthwindCRUD/Controllers/QueryExecutor.cs b/NorthwindCRUD/Controllers/QueryExecutor.cs index 148f996..0c61ff2 100644 --- a/NorthwindCRUD/Controllers/QueryExecutor.cs +++ b/NorthwindCRUD/Controllers/QueryExecutor.cs @@ -142,15 +142,16 @@ private static IQueryable BuildQuery(DbContext db, IQueryable< { var filterExpression = BuildExpression(db, source, query.FilteringOperands, query.Operator); var filteredQuery = source.Where(filterExpression); - - // TODO: project requested columns - // if (query.ReturnFields != null && query.ReturnFields.Any()) - // { - // var projectionExpression = BuildProjectionExpression(query.ReturnFields); - // var projectedQuery = filteredQuery.Select(projectionExpression); - // return projectedQuery; - // } - return filteredQuery; + if (query.ReturnFields != null && query.ReturnFields.Any()) + { + var projectionExpression = BuildProjectionExpression(query.ReturnFields); + var projectedQuery = filteredQuery.Select(projectionExpression); + return projectedQuery.Cast(); + } + else + { + return filteredQuery; + } } private static Expression> BuildExpression(DbContext db, IQueryable source, IQueryFilter[] filters, FilterType filterType) From b6aa5fc7db8c8f596774a84186c69656de2b2442 Mon Sep 17 00:00:00 2001 From: Javier Coitino Date: Wed, 23 Oct 2024 21:37:50 -0300 Subject: [PATCH 26/41] IN subquery issue fixed --- NorthwindCRUD/Controllers/QueryExecutor.cs | 87 +++++++++++++++------- 1 file changed, 62 insertions(+), 25 deletions(-) diff --git a/NorthwindCRUD/Controllers/QueryExecutor.cs b/NorthwindCRUD/Controllers/QueryExecutor.cs index 0c61ff2..62c718d 100644 --- a/NorthwindCRUD/Controllers/QueryExecutor.cs +++ b/NorthwindCRUD/Controllers/QueryExecutor.cs @@ -129,24 +129,28 @@ public class QueryFilterConditionConverter : JsonConverter [SuppressMessage("StyleCop.CSharp.SpacingRules", "SA1009:Closing parenthesis should be spaced correctly", Justification = "...")] public static class QueryExecutor { - public static TEntity[] Run(this IQueryable source, IQuery query) + public static TEntity[] Run(this IQueryable source, IQuery? query) { var infrastructure = source as IInfrastructure; var serviceProvider = infrastructure!.Instance; var currentDbContext = serviceProvider.GetService(typeof(ICurrentDbContext)) as ICurrentDbContext; var db = currentDbContext!.Context; - return BuildQuery(db, source, query).ToArray(); + return db is not null ? BuildQuery(db, source, query).ToArray() : Array.Empty(); } - private static IQueryable BuildQuery(DbContext db, IQueryable source, IQuery query) + private static IQueryable BuildQuery(DbContext db, IQueryable source, IQuery? query) { + if (query is null) + { + throw new InvalidOperationException("Null query"); + } + var filterExpression = BuildExpression(db, source, query.FilteringOperands, query.Operator); var filteredQuery = source.Where(filterExpression); if (query.ReturnFields != null && query.ReturnFields.Any()) { var projectionExpression = BuildProjectionExpression(query.ReturnFields); - var projectedQuery = filteredQuery.Select(projectionExpression); - return projectedQuery.Cast(); + return filteredQuery.Select(projectionExpression).Cast(); } else { @@ -196,8 +200,8 @@ private static Expression BuildConditionExpression(DbContext db, IQuery "notEmpty" => Expression.And(Expression.NotEqual(field, emptyValue), targetType.IsNullableType() ? Expression.NotEqual(field, Expression.Constant(targetType.GetDefaultValue())) : Expression.Constant(true)), "equals" => Expression.Equal(field, searchValue), "doesNotEqual" => Expression.NotEqual(field, searchValue), - "in" => BuildSubquery(db, filter.SearchTree, field, filter.FieldName, targetType), - "notIn" => Expression.Not(BuildSubquery(db, filter.SearchTree, field, filter.FieldName, targetType)), + "in" => BuildInExpression(db, filter.SearchTree, field), + "notIn" => Expression.Not(BuildInExpression(db, filter.SearchTree, field)), "contains" => Expression.Call(field, typeof(string).GetMethod("Contains", new[] { typeof(string) })!, searchValue), "doesNotContain" => Expression.Not(Expression.Call(field, typeof(string).GetMethod("Contains", new[] { typeof(string) })!, searchValue)), "startsWith" => Expression.Call(field, typeof(string).GetMethod("StartsWith", new[] { typeof(string) })!, searchValue), @@ -232,28 +236,61 @@ private static Expression BuildConditionExpression(DbContext db, IQuery } } - private static Expression BuildSubquery(DbContext db, IQuery? query, MemberExpression field, string fieldName, Type targetType) + private static Expression BuildInExpression(DbContext db, IQuery? query, MemberExpression field) { - if (query == null || db == null) + if (field.Type == typeof(string)) + { + var d = RunSubquery(db, query).Select(x => (string)ProjectField(x, query?.ReturnFields[0] ?? string.Empty)).ToArray(); + return Expression.Call(typeof(Enumerable), "Contains", new[] { typeof(string) }, Expression.Constant(d), field); + } + else if (field.Type == typeof(bool) || field.Type == typeof(bool?)) + { + var d = RunSubquery(db, query).Select(x => (bool?)ProjectField(x, query?.ReturnFields[0] ?? string.Empty)).ToArray(); + return Expression.Call(typeof(Enumerable), "Contains", new[] { typeof(bool?) }, Expression.Constant(d), field); + } + else if (field.Type == typeof(int) || field.Type == typeof(int?)) + { + var d = RunSubquery(db, query).Select(x => (int?)ProjectField(x, query?.ReturnFields[0] ?? string.Empty)).ToArray(); + return Expression.Call(typeof(Enumerable), "Contains", new[] { typeof(int?) }, Expression.Constant(d), field); + } + else if (field.Type == typeof(decimal) || field.Type == typeof(decimal?)) + { + var d = RunSubquery(db, query).Select(x => (decimal?)ProjectField(x, query?.ReturnFields[0] ?? string.Empty)).ToArray(); + return Expression.Call(typeof(Enumerable), "Contains", new[] { typeof(decimal?) }, Expression.Constant(d), field); + } + else if (field.Type == typeof(float) || field.Type == typeof(float?)) { - return Expression.Constant(true); + var d = RunSubquery(db, query).Select(x => (float?)ProjectField(x, query?.ReturnFields[0] ?? string.Empty)).ToArray(); + return Expression.Call(typeof(Enumerable), "Contains", new[] { typeof(float?) }, Expression.Constant(d), field); } + else if (field.Type == typeof(DateTime) || field.Type == typeof(DateTime?)) + { + var d = RunSubquery(db, query).Select(x => (DateTime?)ProjectField(x, query?.ReturnFields[0] ?? string.Empty)).ToArray(); + return Expression.Call(typeof(Enumerable), "Contains", new[] { typeof(DateTime) }, Expression.Constant(d), field); + } + else + { + throw new InvalidOperationException($"Type '{field.Type}' not supported for 'IN' operation"); + } + } - var dbSetProperty = db.GetType().GetProperty(query.Entity, BindingFlags.IgnoreCase | BindingFlags.Public | BindingFlags.Instance) - ?? throw new InvalidOperationException($"Entity '{query.Entity}' not found in the DbContext."); - var dbSet = dbSetProperty.GetValue(db) - ?? throw new InvalidOperationException($"Unable to get IQueryable for entity '{query.Entity}'."); - var buildQuery = typeof(QueryExecutor).GetMethod(nameof(BuildQuery), BindingFlags.NonPublic | BindingFlags.Static)!.MakeGenericMethod(dbSet.GetType().GetGenericArguments()[0]); - var subquery = buildQuery.Invoke(null, new object[] { db, dbSet, query }) as IQueryable; - var parameter = Expression.Parameter(subquery!.ElementType, "x"); - var property = Expression.Property(parameter, fieldName); - var lambda = Expression.Lambda(property, parameter); - var projectedSubquery = Expression.Call(typeof(Queryable), "Select", new Type[] { subquery.ElementType, property.Type }, subquery.Expression, lambda ); - var containsMethod = typeof(Queryable).GetMethods().First(m => m.Name == "Contains" && m.GetParameters().Length == 2).MakeGenericMethod(property.Type); - return Expression.Call(containsMethod, projectedSubquery, field); + private static IEnumerable RunSubquery(DbContext db, IQuery? query) + { + var t = query?.Entity.ToLower(CultureInfo.InvariantCulture); + var p = db.GetType().GetProperty(t, BindingFlags.IgnoreCase | BindingFlags.Public | BindingFlags.Instance) ?? throw new InvalidOperationException($"Property '{t}' not found on type '{db.GetType()}'"); + var q = p.GetValue(db) as IQueryable; + return q is null + ? Array.Empty() + : q.ToArray(); + } + + private static dynamic? ProjectField(dynamic? obj, string field) + { + var property = obj?.GetType().GetProperty(field, BindingFlags.IgnoreCase | BindingFlags.Public | BindingFlags.Instance) ?? throw new InvalidOperationException($"Property '{field}' not found on type '{obj?.GetType()}'"); + return property?.GetValue(obj); } - private static Expression GetSearchValue(object? value, Type targetType) + private static Expression GetSearchValue(dynamic? value, Type targetType) { if (value == null) { @@ -270,7 +307,7 @@ 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 => @@ -281,7 +318,7 @@ private static Expression> BuildProjectionExpression>(body, parameter); + return Expression.Lambda>(body, parameter); } } From 201ea8e8393517b9dbad7c95f0e0cb66b9b516c6 Mon Sep 17 00:00:00 2001 From: Javier Coitino Date: Wed, 23 Oct 2024 22:29:00 -0300 Subject: [PATCH 27/41] In subquery issue fixed --- NorthwindCRUD/Controllers/QueryExecutor.cs | 51 +++++++++++++++++----- 1 file changed, 39 insertions(+), 12 deletions(-) diff --git a/NorthwindCRUD/Controllers/QueryExecutor.cs b/NorthwindCRUD/Controllers/QueryExecutor.cs index 62c718d..4e06f61 100644 --- a/NorthwindCRUD/Controllers/QueryExecutor.cs +++ b/NorthwindCRUD/Controllers/QueryExecutor.cs @@ -8,6 +8,7 @@ using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Newtonsoft.Json; +using NorthwindCRUD; using Swashbuckle.AspNetCore.SwaggerGen; public enum FilterType @@ -134,11 +135,11 @@ public static TEntity[] Run(this IQueryable source, IQuery? qu var infrastructure = source as IInfrastructure; var serviceProvider = infrastructure!.Instance; var currentDbContext = serviceProvider.GetService(typeof(ICurrentDbContext)) as ICurrentDbContext; - var db = currentDbContext!.Context; + var db = currentDbContext!.Context as DataContext; return db is not null ? BuildQuery(db, source, query).ToArray() : Array.Empty(); } - private static IQueryable BuildQuery(DbContext db, IQueryable source, IQuery? query) + private static IQueryable BuildQuery(DataContext db, IQueryable source, IQuery? query) { if (query is null) { @@ -158,7 +159,7 @@ private static IQueryable BuildQuery(DbContext db, IQueryable< } } - private static Expression> BuildExpression(DbContext db, IQueryable source, IQueryFilter[] filters, FilterType filterType) + private static Expression> BuildExpression(DataContext db, IQueryable source, IQueryFilter[] filters, FilterType filterType) { var parameter = Expression.Parameter(typeof(TEntity), "entity"); var finalExpression = null as Expression; @@ -182,7 +183,7 @@ private static Expression> BuildExpression(DbContex : (TEntity _) => true; } - private static Expression BuildConditionExpression(DbContext db, IQueryable source, IQueryFilter filter, ParameterExpression parameter) + private static Expression BuildConditionExpression(DataContext db, IQueryable source, IQueryFilter filter, ParameterExpression parameter) { if (filter.FieldName is not null && filter.IgnoreCase is not null && filter.Condition is not null) { @@ -236,7 +237,7 @@ private static Expression BuildConditionExpression(DbContext db, IQuery } } - private static Expression BuildInExpression(DbContext db, IQuery? query, MemberExpression field) + private static Expression BuildInExpression(DataContext db, IQuery? query, MemberExpression field) { if (field.Type == typeof(string)) { @@ -274,14 +275,40 @@ private static Expression BuildInExpression(DbContext db, IQuery? query, MemberE } } - private static IEnumerable RunSubquery(DbContext db, IQuery? query) + private static IEnumerable RunSubquery(DataContext db, IQuery? query) { - var t = query?.Entity.ToLower(CultureInfo.InvariantCulture); - var p = db.GetType().GetProperty(t, BindingFlags.IgnoreCase | BindingFlags.Public | BindingFlags.Instance) ?? throw new InvalidOperationException($"Property '{t}' not found on type '{db.GetType()}'"); - var q = p.GetValue(db) as IQueryable; - return q is null - ? Array.Empty() - : q.ToArray(); + // var t = query?.Entity.ToLower(CultureInfo.InvariantCulture); + // var p = db.GetType().GetProperty(t, BindingFlags.IgnoreCase | BindingFlags.Public | BindingFlags.Instance) ?? throw new InvalidOperationException($"Property '{t}' not found on type '{db.GetType()}'"); + // var q = p.GetValue(db) as IQueryable; + // return q is null ? Array.Empty() : q.Run(query).ToArray(); + var t = query?.Entity.ToLower(CultureInfo.InvariantCulture) ?? string.Empty; + switch (t) + { + case "addresses": + return db.Suppliers.Run(query).ToArray(); + case "categories": + return db.Categories.Run(query).ToArray(); + case "products": + return db.Products.Run(query).ToArray(); + case "regions": + return db.Regions.Run(query).ToArray(); + case "territories": + return db.Territories.Run(query).ToArray(); + case "employees": + return db.Employees.Run(query).ToArray(); + case "customers": + return db.Customers.Run(query).ToArray(); + case "orders": + return db.Orders.Run(query).ToArray(); + case "orderdetails": + return db.OrderDetails.Run(query).ToArray(); + case "shippers": + return db.Shippers.Run(query).ToArray(); + case "suppliers": + return db.Suppliers.Run(query).ToArray(); + default: + return Array.Empty(); + } } private static dynamic? ProjectField(dynamic? obj, string field) From b36a8a6633225f70c560fce2442aa279fb99983f Mon Sep 17 00:00:00 2001 From: Javier Coitino Date: Fri, 8 Nov 2024 14:00:44 -0300 Subject: [PATCH 28/41] Code polishing --- NorthwindCRUD/Controllers/QueryExecutor.cs | 23 +++++++++------------- 1 file changed, 9 insertions(+), 14 deletions(-) diff --git a/NorthwindCRUD/Controllers/QueryExecutor.cs b/NorthwindCRUD/Controllers/QueryExecutor.cs index 4e06f61..acc44db 100644 --- a/NorthwindCRUD/Controllers/QueryExecutor.cs +++ b/NorthwindCRUD/Controllers/QueryExecutor.cs @@ -97,31 +97,26 @@ public class QueryFilterCondition : IQueryFilterCondition public string IconName { get; set; } } -public class QueryConverter : JsonConverter +public abstract class InterfaceToConcreteClassConverter : JsonConverter { - public override bool CanConvert(Type objectType) => objectType == typeof(IQuery); + public override bool CanConvert(Type objectType) => objectType == typeof(TI); - public override object? ReadJson(JsonReader reader, Type objectType, object? existingValue, JsonSerializer serializer) => serializer.Deserialize(reader); + public override object? ReadJson(JsonReader reader, Type objectType, object? existingValue, JsonSerializer serializer) => serializer.Deserialize(reader); public override void WriteJson(JsonWriter writer, object? value, JsonSerializer serializer) => serializer.Serialize(writer, value); + } -public class QueryFilterConverter : JsonConverter +public class QueryConverter : InterfaceToConcreteClassConverter { - public override bool CanConvert(Type objectType) => objectType == typeof(IQueryFilter); - - public override object? ReadJson(JsonReader reader, Type objectType, object? existingValue, JsonSerializer serializer) => serializer.Deserialize(reader); - - public override void WriteJson(JsonWriter writer, object? value, JsonSerializer serializer) => serializer.Serialize(writer, value); } -public class QueryFilterConditionConverter : JsonConverter +public class QueryFilterConverter : InterfaceToConcreteClassConverter { - public override bool CanConvert(Type objectType) => objectType == typeof(IQueryFilterCondition); - - public override object? ReadJson(JsonReader reader, Type objectType, object? existingValue, JsonSerializer serializer) => serializer.Deserialize(reader); +} - public override void WriteJson(JsonWriter writer, object? value, JsonSerializer serializer) => serializer.Serialize(writer, value); +public class QueryFilterConditionConverter : InterfaceToConcreteClassConverter +{ } /// From f92b4e834e3c064a9df9b2bafe7c04399ac8eda4 Mon Sep 17 00:00:00 2001 From: Javier Coitino Date: Sat, 9 Nov 2024 06:19:53 -0300 Subject: [PATCH 29/41] Code polishing --- NorthwindCRUD/Program.cs | 3 - .../QueryBuilder/Model/FilterType.cs | 7 + NorthwindCRUD/QueryBuilder/Model/Query.cs | 12 + .../QueryBuilder/Model/QueryFilter.cs | 20 ++ .../Model/QueryFilterCondition.cs | 10 + .../QueryExecutor.cs | 266 +++--------------- NorthwindCRUD/QueryBuilder/SqlGenerator.cs | 65 +++++ 7 files changed, 156 insertions(+), 227 deletions(-) create mode 100644 NorthwindCRUD/QueryBuilder/Model/FilterType.cs create mode 100644 NorthwindCRUD/QueryBuilder/Model/Query.cs create mode 100644 NorthwindCRUD/QueryBuilder/Model/QueryFilter.cs create mode 100644 NorthwindCRUD/QueryBuilder/Model/QueryFilterCondition.cs rename NorthwindCRUD/{Controllers => QueryBuilder}/QueryExecutor.cs (50%) create mode 100644 NorthwindCRUD/QueryBuilder/SqlGenerator.cs diff --git a/NorthwindCRUD/Program.cs b/NorthwindCRUD/Program.cs index 423117c..6a5b72a 100644 --- a/NorthwindCRUD/Program.cs +++ b/NorthwindCRUD/Program.cs @@ -43,9 +43,6 @@ public static void Main(string[] args) { options.SerializerSettings.ReferenceLoopHandling = Newtonsoft.Json.ReferenceLoopHandling.Ignore; options.SerializerSettings.Converters.Add(new StringEnumConverter()); - options.SerializerSettings.Converters.Add(new QueryConverter()); - options.SerializerSettings.Converters.Add(new QueryFilterConverter()); - options.SerializerSettings.Converters.Add(new QueryFilterConditionConverter()); }); builder.Services.AddEndpointsApiExplorer(); diff --git a/NorthwindCRUD/QueryBuilder/Model/FilterType.cs b/NorthwindCRUD/QueryBuilder/Model/FilterType.cs new file mode 100644 index 0000000..75d0748 --- /dev/null +++ b/NorthwindCRUD/QueryBuilder/Model/FilterType.cs @@ -0,0 +1,7 @@ +namespace QueryBuilder; + +public enum FilterType +{ + And = 0, + Or = 1, +} diff --git a/NorthwindCRUD/QueryBuilder/Model/Query.cs b/NorthwindCRUD/QueryBuilder/Model/Query.cs new file mode 100644 index 0000000..ae2ceb6 --- /dev/null +++ b/NorthwindCRUD/QueryBuilder/Model/Query.cs @@ -0,0 +1,12 @@ +namespace QueryBuilder; + +public class Query +{ + public string Entity { get; set; } + + public string[] ReturnFields { get; set; } + + public FilterType Operator { get; set; } + + public QueryFilter[] FilteringOperands { get; set; } +} diff --git a/NorthwindCRUD/QueryBuilder/Model/QueryFilter.cs b/NorthwindCRUD/QueryBuilder/Model/QueryFilter.cs new file mode 100644 index 0000000..e95d0be --- /dev/null +++ b/NorthwindCRUD/QueryBuilder/Model/QueryFilter.cs @@ -0,0 +1,20 @@ +namespace QueryBuilder; + +public class QueryFilter +{ + // Basic condition + public string? FieldName { get; set; } + + public bool? IgnoreCase { get; set; } + + public QueryFilterCondition? Condition { get; set; } + + public object? SearchVal { get; set; } + + public Query? SearchTree { get; set; } + + // And/Or + public FilterType? Operator { get; set; } + + public QueryFilter[] FilteringOperands { get; set; } +} diff --git a/NorthwindCRUD/QueryBuilder/Model/QueryFilterCondition.cs b/NorthwindCRUD/QueryBuilder/Model/QueryFilterCondition.cs new file mode 100644 index 0000000..50c6327 --- /dev/null +++ b/NorthwindCRUD/QueryBuilder/Model/QueryFilterCondition.cs @@ -0,0 +1,10 @@ +namespace QueryBuilder; + +public class QueryFilterCondition +{ + public string Name { get; set; } + + public bool IsUnary { get; set; } + + public string IconName { get; set; } +} diff --git a/NorthwindCRUD/Controllers/QueryExecutor.cs b/NorthwindCRUD/QueryBuilder/QueryExecutor.cs similarity index 50% rename from NorthwindCRUD/Controllers/QueryExecutor.cs rename to NorthwindCRUD/QueryBuilder/QueryExecutor.cs index acc44db..2365614 100644 --- a/NorthwindCRUD/Controllers/QueryExecutor.cs +++ b/NorthwindCRUD/QueryBuilder/QueryExecutor.cs @@ -7,125 +7,17 @@ using AutoMapper.Internal; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; -using Newtonsoft.Json; using NorthwindCRUD; using Swashbuckle.AspNetCore.SwaggerGen; -public enum FilterType -{ - And = 0, - Or = 1, -} - -public interface IQuery -{ - public string Entity { get; set; } - - public string[] ReturnFields { get; set; } - - [SuppressMessage("Naming", "CA1716:Identifiers should not match keywords", Justification = "required name")] - public FilterType Operator { get; set; } - - public IQueryFilter[] FilteringOperands { get; set; } -} - -public interface IQueryFilter -{ - // Basic condition - public string? FieldName { get; set; } - - public bool? IgnoreCase { get; set; } - - public IQueryFilterCondition? Condition { get; set; } - - public object? SearchVal { get; set; } - - public IQuery? SearchTree { get; set; } - - // And/Or - [SuppressMessage("Naming", "CA1716:Identifiers should not match keywords", Justification = "required name")] - public FilterType? Operator { get; set; } - - public IQueryFilter[] FilteringOperands { get; set; } -} - -public interface IQueryFilterCondition -{ - public string Name { get; set; } - - public bool IsUnary { get; set; } - - public string IconName { get; set; } -} - -public class Query : IQuery -{ - public string Entity { get; set; } - - public string[] ReturnFields { get; set; } - - public FilterType Operator { get; set; } - - public IQueryFilter[] FilteringOperands { get; set; } -} - -public class QueryFilter : IQueryFilter -{ - // Basic condition - public string? FieldName { get; set; } - - public bool? IgnoreCase { get; set; } - - public IQueryFilterCondition? Condition { get; set; } - - public object? SearchVal { get; set; } - - public IQuery? SearchTree { get; set; } - - // And/Or - public FilterType? Operator { get; set; } - - public IQueryFilter[] FilteringOperands { get; set; } -} - -public class QueryFilterCondition : IQueryFilterCondition -{ - public string Name { get; set; } - - public bool IsUnary { get; set; } - - public string IconName { get; set; } -} - -public abstract class InterfaceToConcreteClassConverter : JsonConverter -{ - public override bool CanConvert(Type objectType) => objectType == typeof(TI); - - public override object? ReadJson(JsonReader reader, Type objectType, object? existingValue, JsonSerializer serializer) => serializer.Deserialize(reader); - - public override void WriteJson(JsonWriter writer, object? value, JsonSerializer serializer) => serializer.Serialize(writer, value); - -} - -public class QueryConverter : InterfaceToConcreteClassConverter -{ -} - -public class QueryFilterConverter : InterfaceToConcreteClassConverter -{ -} - -public class QueryFilterConditionConverter : InterfaceToConcreteClassConverter -{ -} - /// /// A generic query executor that can be used to execute queries on IQueryable data sources. /// [SuppressMessage("StyleCop.CSharp.SpacingRules", "SA1009:Closing parenthesis should be spaced correctly", Justification = "...")] +[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, IQuery? query) + public static TEntity[] Run(this IQueryable source, Query? query) { var infrastructure = source as IInfrastructure; var serviceProvider = infrastructure!.Instance; @@ -134,7 +26,7 @@ public static TEntity[] Run(this IQueryable source, IQuery? qu return db is not null ? BuildQuery(db, source, query).ToArray() : Array.Empty(); } - private static IQueryable BuildQuery(DataContext db, IQueryable source, IQuery? query) + private static IQueryable BuildQuery(DataContext db, IQueryable source, Query? query) { if (query is null) { @@ -154,7 +46,7 @@ private static IQueryable BuildQuery(DataContext db, IQueryabl } } - private static Expression> BuildExpression(DataContext db, IQueryable source, IQueryFilter[] filters, FilterType filterType) + private static Expression> BuildExpression(DataContext db, IQueryable source, QueryFilter[] filters, FilterType filterType) { var parameter = Expression.Parameter(typeof(TEntity), "entity"); var finalExpression = null as Expression; @@ -178,7 +70,7 @@ private static Expression> BuildExpression(DataCont : (TEntity _) => true; } - private static Expression BuildConditionExpression(DataContext db, IQueryable source, IQueryFilter filter, ParameterExpression parameter) + 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) { @@ -190,26 +82,26 @@ private static Expression BuildConditionExpression(DataContext db, IQue var emptyValue = GetEmptyValue(targetType); Expression condition = filter.Condition.Name switch { - "null" => targetType.IsNullableType() ? Expression.Equal(field, Expression.Constant(targetType.GetDefaultValue())) : Expression.Constant(false), - "notNull" => targetType.IsNullableType() ? Expression.NotEqual(field, Expression.Constant(targetType.GetDefaultValue())) : Expression.Constant(true), - "empty" => Expression.Or(Expression.Equal(field, emptyValue), targetType.IsNullableType() ? Expression.Equal(field, Expression.Constant(targetType.GetDefaultValue())) : Expression.Constant(false)), - "notEmpty" => Expression.And(Expression.NotEqual(field, emptyValue), targetType.IsNullableType() ? Expression.NotEqual(field, Expression.Constant(targetType.GetDefaultValue())) : Expression.Constant(true)), - "equals" => Expression.Equal(field, searchValue), - "doesNotEqual" => Expression.NotEqual(field, searchValue), - "in" => BuildInExpression(db, filter.SearchTree, field), - "notIn" => Expression.Not(BuildInExpression(db, filter.SearchTree, field)), - "contains" => Expression.Call(field, typeof(string).GetMethod("Contains", new[] { typeof(string) })!, searchValue), - "doesNotContain" => Expression.Not(Expression.Call(field, typeof(string).GetMethod("Contains", new[] { typeof(string) })!, searchValue)), - "startsWith" => Expression.Call(field, typeof(string).GetMethod("StartsWith", new[] { typeof(string) })!, searchValue), - "endsWith" => Expression.Call(field, typeof(string).GetMethod("EndsWith", new[] { typeof(string) })!, searchValue), - "greaterThan" => Expression.GreaterThan(field, searchValue), - "lessThan" => Expression.LessThan(field, searchValue), - "greaterThanOrEqualTo" => Expression.GreaterThanOrEqual(field, searchValue), - "lessThanOrEqualTo" => Expression.LessThanOrEqual(field, searchValue), - "all" => throw new NotImplementedException("Not implemented"), - "true" => Expression.IsTrue(field), - "false" => Expression.IsFalse(field), - _ => throw new NotImplementedException("Not implemented"), + "null" => targetType.IsNullableType() ? Expression.Equal(field, Expression.Constant(targetType.GetDefaultValue())) : Expression.Constant(false), + "notNull" => targetType.IsNullableType() ? Expression.NotEqual(field, Expression.Constant(targetType.GetDefaultValue())) : Expression.Constant(true), + "empty" => Expression.Or(Expression.Equal(field, emptyValue), targetType.IsNullableType() ? Expression.Equal(field, Expression.Constant(targetType.GetDefaultValue())) : Expression.Constant(false)), + "notEmpty" => Expression.And(Expression.NotEqual(field, emptyValue), targetType.IsNullableType() ? Expression.NotEqual(field, Expression.Constant(targetType.GetDefaultValue())) : Expression.Constant(true)), + "equals" => Expression.Equal(field, searchValue), + "doesNotEqual" => Expression.NotEqual(field, searchValue), + "in" => BuildInExpression(db, filter.SearchTree, field), + "notIn" => Expression.Not(BuildInExpression(db, filter.SearchTree, field)), + "contains" => Expression.Call(field, typeof(string).GetMethod("Contains", new[] { typeof(string) })!, searchValue), + "doesNotContain" => Expression.Not(Expression.Call(field, typeof(string).GetMethod("Contains", new[] { typeof(string) })!, searchValue)), + "startsWith" => Expression.Call(field, typeof(string).GetMethod("StartsWith", new[] { typeof(string) })!, searchValue), + "endsWith" => Expression.Call(field, typeof(string).GetMethod("EndsWith", new[] { typeof(string) })!, searchValue), + "greaterThan" => Expression.GreaterThan(field, searchValue), + "lessThan" => Expression.LessThan(field, searchValue), + "greaterThanOrEqualTo" => Expression.GreaterThanOrEqual(field, searchValue), + "lessThanOrEqualTo" => Expression.LessThanOrEqual(field, searchValue), + "all" => throw new NotImplementedException("Not implemented"), + "true" => Expression.IsTrue(field), + "false" => Expression.IsFalse(field), + _ => throw new NotImplementedException("Not implemented"), }; if (filter.IgnoreCase.Value && field.Type == typeof(string)) { @@ -232,7 +124,7 @@ private static Expression BuildConditionExpression(DataContext db, IQue } } - private static Expression BuildInExpression(DataContext db, IQuery? query, MemberExpression field) + private static Expression BuildInExpression(DataContext db, Query? query, MemberExpression field) { if (field.Type == typeof(string)) { @@ -270,40 +162,28 @@ private static Expression BuildInExpression(DataContext db, IQuery? query, Membe } } - private static IEnumerable RunSubquery(DataContext db, IQuery? query) + private static IEnumerable RunSubquery(DataContext db, Query? query) { // var t = query?.Entity.ToLower(CultureInfo.InvariantCulture); // var p = db.GetType().GetProperty(t, BindingFlags.IgnoreCase | BindingFlags.Public | BindingFlags.Instance) ?? throw new InvalidOperationException($"Property '{t}' not found on type '{db.GetType()}'"); // var q = p.GetValue(db) as IQueryable; // return q is null ? Array.Empty() : q.Run(query).ToArray(); var t = query?.Entity.ToLower(CultureInfo.InvariantCulture) ?? string.Empty; - switch (t) - { - case "addresses": - return db.Suppliers.Run(query).ToArray(); - case "categories": - return db.Categories.Run(query).ToArray(); - case "products": - return db.Products.Run(query).ToArray(); - case "regions": - return db.Regions.Run(query).ToArray(); - case "territories": - return db.Territories.Run(query).ToArray(); - case "employees": - return db.Employees.Run(query).ToArray(); - case "customers": - return db.Customers.Run(query).ToArray(); - case "orders": - return db.Orders.Run(query).ToArray(); - case "orderdetails": - return db.OrderDetails.Run(query).ToArray(); - case "shippers": - return db.Shippers.Run(query).ToArray(); - case "suppliers": - return db.Suppliers.Run(query).ToArray(); - default: - return Array.Empty(); - } + return t switch + { + "addresses" => db.Suppliers.Run(query).ToArray(), + "categories" => db.Categories.Run(query).ToArray(), + "products" => db.Products.Run(query).ToArray(), + "regions" => db.Regions.Run(query).ToArray(), + "territories" => db.Territories.Run(query).ToArray(), + "employees" => db.Employees.Run(query).ToArray(), + "customers" => db.Customers.Run(query).ToArray(), + "orders" => db.Orders.Run(query).ToArray(), + "orderdetails" => db.OrderDetails.Run(query).ToArray(), + "shippers" => db.Shippers.Run(query).ToArray(), + "suppliers" => db.Suppliers.Run(query).ToArray(), + _ => Array.Empty(), + }; } private static dynamic? ProjectField(dynamic? obj, string field) @@ -343,65 +223,3 @@ private static Expression> BuildProjectionExpression>(body, parameter); } } - -[SuppressMessage("StyleCop.CSharp.SpacingRules", "SA1025:Code should not contain multiple whitespace in a row", Justification = "...")] -public static class SqlGenerator -{ - public static string GenerateSql(IQuery query) - { - var selectClause = BuildSelectClause(query); - var whereClause = BuildWhereClause(query.FilteringOperands, query.Operator); - return $"{selectClause} {whereClause};"; - } - - private static string BuildSelectClause(IQuery query) - { - var fields = query.ReturnFields != null && query.ReturnFields.Any() - ? string.Join(", ", query.ReturnFields) - : "*"; - return $"SELECT {fields} FROM {query.Entity}"; - } - - private static string BuildWhereClause(IQueryFilter[] filters, FilterType filterType) - { - if (filters == null || !filters.Any()) - { - return string.Empty; - } - - var conditions = filters.Select(BuildCondition).ToArray(); - var conjunction = filterType == FilterType.And ? " AND " : " OR "; - return $"WHERE {string.Join(conjunction, conditions)}"; - } - - private static string BuildCondition(IQueryFilter filter) - { - var field = filter.FieldName; - var condition = filter.Condition?.Name; - var value = filter.SearchVal != null ? $"'{filter.SearchVal}'" : "NULL"; - var subquery = filter.SearchTree != null ? $"({GenerateSql(filter.SearchTree)})" : string.Empty; - return condition switch - { - "null" => $"{field} IS NULL", - "notNull" => $"{field} IS NOT NULL", - "empty" => $"{field} = ''", - "notEmpty" => $"{field} <> ''", - "equals" => $"{field} = {value}", - "doesNotEqual" => $"{field} <> {value}", - "in" => $"{field} IN ({subquery})", - "notIn" => $"{field} NOT IN ({subquery})", - "contains" => $"{field} LIKE '%{filter.SearchVal}%'", - "doesNotContain" => $"{field} NOT LIKE '%{filter.SearchVal}%'", - "startsWith" => $"{field} LIKE '{filter.SearchVal}%'", - "endsWith" => $"{field} LIKE '%{filter.SearchVal}'", - "greaterThan" => $"{field} > {value}", - "lessThan" => $"{field} < {value}", - "greaterThanOrEqualTo" => $"{field} >= {value}", - "lessThanOrEqualTo" => $"{field} <= {value}", - "all" => throw new NotImplementedException("Not implemented"), - "true" => $"{field} = TRUE", - "false" => $"{field} = FALSE", - _ => throw new NotImplementedException($"Condition '{condition}' is not implemented"), - }; - } -} \ No newline at end of file diff --git a/NorthwindCRUD/QueryBuilder/SqlGenerator.cs b/NorthwindCRUD/QueryBuilder/SqlGenerator.cs new file mode 100644 index 0000000..b5e7d6a --- /dev/null +++ b/NorthwindCRUD/QueryBuilder/SqlGenerator.cs @@ -0,0 +1,65 @@ +namespace QueryBuilder; + +using System.Diagnostics.CodeAnalysis; + +[SuppressMessage("StyleCop.CSharp.SpacingRules", "SA1025:Code should not contain multiple whitespace in a row", Justification = "...")] +public static class SqlGenerator +{ + public static string GenerateSql(Query query) + { + var selectClause = BuildSelectClause(query); + var whereClause = BuildWhereClause(query.FilteringOperands, query.Operator); + return $"{selectClause} {whereClause};"; + } + + private static string BuildSelectClause(Query query) + { + var fields = query.ReturnFields != null && query.ReturnFields.Any() + ? string.Join(", ", query.ReturnFields) + : "*"; + return $"SELECT {fields} FROM {query.Entity}"; + } + + private static string BuildWhereClause(QueryFilter[] filters, FilterType filterType) + { + if (filters == null || !filters.Any()) + { + return string.Empty; + } + + var conditions = filters.Select(BuildCondition).ToArray(); + var conjunction = filterType == FilterType.And ? " AND " : " OR "; + return $"WHERE {string.Join(conjunction, conditions)}"; + } + + private static string BuildCondition(QueryFilter filter) + { + var field = filter.FieldName; + var condition = filter.Condition?.Name; + var value = filter.SearchVal != null ? $"'{filter.SearchVal}'" : "NULL"; + var subquery = filter.SearchTree != null ? $"({GenerateSql(filter.SearchTree)})" : string.Empty; + return condition switch + { + "null" => $"{field} IS NULL", + "notNull" => $"{field} IS NOT NULL", + "empty" => $"{field} = ''", + "notEmpty" => $"{field} <> ''", + "equals" => $"{field} = {value}", + "doesNotEqual" => $"{field} <> {value}", + "in" => $"{field} IN ({subquery})", + "notIn" => $"{field} NOT IN ({subquery})", + "contains" => $"{field} LIKE '%{filter.SearchVal}%'", + "doesNotContain" => $"{field} NOT LIKE '%{filter.SearchVal}%'", + "startsWith" => $"{field} LIKE '{filter.SearchVal}%'", + "endsWith" => $"{field} LIKE '%{filter.SearchVal}'", + "greaterThan" => $"{field} > {value}", + "lessThan" => $"{field} < {value}", + "greaterThanOrEqualTo" => $"{field} >= {value}", + "lessThanOrEqualTo" => $"{field} <= {value}", + "all" => throw new NotImplementedException("Not implemented"), + "true" => $"{field} = TRUE", + "false" => $"{field} = FALSE", + _ => throw new NotImplementedException($"Condition '{condition}' is not implemented"), + }; + } +} \ No newline at end of file From fd2d79e6c7cb3630b9bf7bba438f788455e01890 Mon Sep 17 00:00:00 2001 From: Javier Coitino Date: Sat, 9 Nov 2024 07:04:39 -0300 Subject: [PATCH 30/41] Code polishing --- NorthwindCRUD/QueryBuilder/QueryExecutor.cs | 58 +++++++++------------ 1 file changed, 24 insertions(+), 34 deletions(-) diff --git a/NorthwindCRUD/QueryBuilder/QueryExecutor.cs b/NorthwindCRUD/QueryBuilder/QueryExecutor.cs index 2365614..d5a06f7 100644 --- a/NorthwindCRUD/QueryBuilder/QueryExecutor.cs +++ b/NorthwindCRUD/QueryBuilder/QueryExecutor.cs @@ -126,40 +126,30 @@ private static Expression BuildConditionExpression(DataContext db, IQue private static Expression BuildInExpression(DataContext db, Query? query, MemberExpression field) { - if (field.Type == typeof(string)) - { - var d = RunSubquery(db, query).Select(x => (string)ProjectField(x, query?.ReturnFields[0] ?? string.Empty)).ToArray(); - return Expression.Call(typeof(Enumerable), "Contains", new[] { typeof(string) }, Expression.Constant(d), field); - } - else if (field.Type == typeof(bool) || field.Type == typeof(bool?)) - { - var d = RunSubquery(db, query).Select(x => (bool?)ProjectField(x, query?.ReturnFields[0] ?? string.Empty)).ToArray(); - return Expression.Call(typeof(Enumerable), "Contains", new[] { typeof(bool?) }, Expression.Constant(d), field); - } - else if (field.Type == typeof(int) || field.Type == typeof(int?)) - { - var d = RunSubquery(db, query).Select(x => (int?)ProjectField(x, query?.ReturnFields[0] ?? string.Empty)).ToArray(); - return Expression.Call(typeof(Enumerable), "Contains", new[] { typeof(int?) }, Expression.Constant(d), field); - } - else if (field.Type == typeof(decimal) || field.Type == typeof(decimal?)) - { - var d = RunSubquery(db, query).Select(x => (decimal?)ProjectField(x, query?.ReturnFields[0] ?? string.Empty)).ToArray(); - return Expression.Call(typeof(Enumerable), "Contains", new[] { typeof(decimal?) }, Expression.Constant(d), field); - } - else if (field.Type == typeof(float) || field.Type == typeof(float?)) - { - var d = RunSubquery(db, query).Select(x => (float?)ProjectField(x, query?.ReturnFields[0] ?? string.Empty)).ToArray(); - return Expression.Call(typeof(Enumerable), "Contains", new[] { typeof(float?) }, Expression.Constant(d), field); - } - else if (field.Type == typeof(DateTime) || field.Type == typeof(DateTime?)) - { - var d = RunSubquery(db, query).Select(x => (DateTime?)ProjectField(x, query?.ReturnFields[0] ?? string.Empty)).ToArray(); - return Expression.Call(typeof(Enumerable), "Contains", new[] { typeof(DateTime) }, Expression.Constant(d), field); - } - else - { - throw new InvalidOperationException($"Type '{field.Type}' not supported for 'IN' operation"); - } + return field.Type switch + { + { } t when t == typeof(string) => BuildInExpression(db, query, field), + { } t when t == typeof(bool) => BuildInExpression(db, query, field), + { } t when t == typeof(bool?) => BuildInExpression(db, query, field), + { } t when t == typeof(int) => BuildInExpression(db, query, field), + { } t when t == typeof(int?) => BuildInExpression(db, query, field), + { } t when t == typeof(decimal) => BuildInExpression(db, query, field), + { } t when t == typeof(decimal?) => BuildInExpression(db, query, field), + { } t when t == typeof(float) => BuildInExpression(db, query, field), + { } t when t == typeof(float?) => BuildInExpression(db, query, field), + { } t when t == typeof(DateTime) => BuildInExpression(db, query, field), + { } t when t == typeof(DateTime?) => BuildInExpression(db, query, field), + _ => throw new InvalidOperationException($"Type '{field.Type}' not supported for 'IN' operation"), + }; + } + + private static Expression BuildInExpression(DataContext db, Query? query, MemberExpression field) + { + var d = RunSubquery(db, query).Select(x => (T)ProjectField(x, query?.ReturnFields[0] ?? string.Empty)).ToArray(); + var m = typeof(Enumerable).GetMethods() + .FirstOrDefault(method => method.Name == nameof(Enumerable.Contains) && method.GetParameters().Length == 2) + ?.MakeGenericMethod(typeof(T)) ?? throw new InvalidOperationException("Missing method"); + return Expression.Call(m, Expression.Constant(d), field); } private static IEnumerable RunSubquery(DataContext db, Query? query) From aebb9592cb84d4e05275da4004967469fe2f6d78 Mon Sep 17 00:00:00 2001 From: Javier Coitino Date: Sat, 9 Nov 2024 07:23:22 -0300 Subject: [PATCH 31/41] Unused imports removed --- NorthwindCRUD/Program.cs | 5 ----- 1 file changed, 5 deletions(-) diff --git a/NorthwindCRUD/Program.cs b/NorthwindCRUD/Program.cs index 6a5b72a..8a6bb64 100644 --- a/NorthwindCRUD/Program.cs +++ b/NorthwindCRUD/Program.cs @@ -2,10 +2,6 @@ using AutoMapper; using GraphQL.AspNet.Configuration.Mvc; using Microsoft.AspNetCore.Authentication.JwtBearer; -using Microsoft.Data.Sqlite; -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Diagnostics; -using Microsoft.Extensions.Options; using Microsoft.IdentityModel.Tokens; using Microsoft.OpenApi.Models; using Newtonsoft.Json.Converters; @@ -14,7 +10,6 @@ using NorthwindCRUD.Middlewares; using NorthwindCRUD.Providers; using NorthwindCRUD.Services; -using QueryBuilder; namespace NorthwindCRUD { From 1c989336602b5daa783535842896b63986492625 Mon Sep 17 00:00:00 2001 From: Pablo Date: Tue, 4 Mar 2025 09:50:18 -0300 Subject: [PATCH 32/41] Update inQuery/notInQuery conditions for 19.1 --- NorthwindCRUD/QueryBuilder/QueryExecutor.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/NorthwindCRUD/QueryBuilder/QueryExecutor.cs b/NorthwindCRUD/QueryBuilder/QueryExecutor.cs index d5a06f7..1777c54 100644 --- a/NorthwindCRUD/QueryBuilder/QueryExecutor.cs +++ b/NorthwindCRUD/QueryBuilder/QueryExecutor.cs @@ -88,8 +88,8 @@ private static Expression BuildConditionExpression(DataContext db, IQue "notEmpty" => Expression.And(Expression.NotEqual(field, emptyValue), targetType.IsNullableType() ? Expression.NotEqual(field, Expression.Constant(targetType.GetDefaultValue())) : Expression.Constant(true)), "equals" => Expression.Equal(field, searchValue), "doesNotEqual" => Expression.NotEqual(field, searchValue), - "in" => BuildInExpression(db, filter.SearchTree, field), - "notIn" => Expression.Not(BuildInExpression(db, filter.SearchTree, field)), + "inQuery" => BuildInExpression(db, filter.SearchTree, field), + "notInQuery" => Expression.Not(BuildInExpression(db, filter.SearchTree, field)), "contains" => Expression.Call(field, typeof(string).GetMethod("Contains", new[] { typeof(string) })!, searchValue), "doesNotContain" => Expression.Not(Expression.Call(field, typeof(string).GetMethod("Contains", new[] { typeof(string) })!, searchValue)), "startsWith" => Expression.Call(field, typeof(string).GetMethod("StartsWith", new[] { typeof(string) })!, searchValue), From 01cfb6475ba44789fcb9cb04fb15dfdea628cf83 Mon Sep 17 00:00:00 2001 From: Zdravko Kolev Date: Tue, 4 Mar 2025 15:04:51 +0200 Subject: [PATCH 33/41] Potential fix for code scanning alert no. 20: Log entries created from user input Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com> --- NorthwindCRUD/Controllers/QueryBuilderController.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/NorthwindCRUD/Controllers/QueryBuilderController.cs b/NorthwindCRUD/Controllers/QueryBuilderController.cs index bb7c36f..aadf2a7 100644 --- a/NorthwindCRUD/Controllers/QueryBuilderController.cs +++ b/NorthwindCRUD/Controllers/QueryBuilderController.cs @@ -48,7 +48,8 @@ public QueryBuilderController(DataContext dataContext, IMapper mapper, ILogger ExecuteQuery(Query query) { - logger.LogInformation("Executing query for entity: {Entity}", query.Entity); + var sanitizedEntity = query.Entity.Replace("\r", "").Replace("\n", ""); + logger.LogInformation("Executing query for entity: {Entity}", sanitizedEntity); var t = query.Entity.ToLower(CultureInfo.InvariantCulture); return Ok(new QueryBuilderResult { From 18e98fb1c215b248e3a4949e12284854499ec2de Mon Sep 17 00:00:00 2001 From: zkolev Date: Tue, 4 Mar 2025 15:32:19 +0200 Subject: [PATCH 34/41] Fix failing ci --- .github/workflows/build-test-ci.yml | 25 +++++++++++++------------ 1 file changed, 13 insertions(+), 12 deletions(-) diff --git a/.github/workflows/build-test-ci.yml b/.github/workflows/build-test-ci.yml index f5fa006..cb27290 100644 --- a/.github/workflows/build-test-ci.yml +++ b/.github/workflows/build-test-ci.yml @@ -7,24 +7,25 @@ on: pull_request: branches: - main + jobs: build-and-test: runs-on: ubuntu-latest steps: - - name: Checkout code - uses: actions/checkout@v3 + - name: Checkout code + uses: actions/checkout@v3 - - name: Setup .NET - uses: actions/setup-dotnet@v3 - with: - dotnet-version: 6.x + - name: Setup .NET + uses: actions/setup-dotnet@v3 + with: + dotnet-version: '6.x' - - name: Restore dependencies - run: dotnet restore + - name: Restore dependencies + run: dotnet restore - - name: Build - run: dotnet build --configuration Release + - name: Build + run: dotnet build --configuration Release - - name: Run tests - run: dotnet test --configuration Release --no-build \ No newline at end of file + - name: Run tests + run: dotnet test --configuration Release --no-build From 1d2e82a6913ddcef2cc7e1282ab65b206b093216 Mon Sep 17 00:00:00 2001 From: zkolev Date: Tue, 4 Mar 2025 15:36:20 +0200 Subject: [PATCH 35/41] Fix string empty error --- NorthwindCRUD/Controllers/QueryBuilderController.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/NorthwindCRUD/Controllers/QueryBuilderController.cs b/NorthwindCRUD/Controllers/QueryBuilderController.cs index aadf2a7..8d8263a 100644 --- a/NorthwindCRUD/Controllers/QueryBuilderController.cs +++ b/NorthwindCRUD/Controllers/QueryBuilderController.cs @@ -48,7 +48,7 @@ public QueryBuilderController(DataContext dataContext, IMapper mapper, ILogger ExecuteQuery(Query query) { - var sanitizedEntity = query.Entity.Replace("\r", "").Replace("\n", ""); + 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 From a987b248f164c2b94a6f7a6555ce1e8c2b41231b Mon Sep 17 00:00:00 2001 From: Javier Coitino Date: Wed, 12 Mar 2025 19:33:00 -0300 Subject: [PATCH 36/41] Missing operators added --- NorthwindCRUD/QueryBuilder/QueryExecutor.cs | 12 +++++++++++- NorthwindCRUD/QueryBuilder/SqlGenerator.cs | 17 ++++++++++++++--- 2 files changed, 25 insertions(+), 4 deletions(-) diff --git a/NorthwindCRUD/QueryBuilder/QueryExecutor.cs b/NorthwindCRUD/QueryBuilder/QueryExecutor.cs index 1777c54..eec48a2 100644 --- a/NorthwindCRUD/QueryBuilder/QueryExecutor.cs +++ b/NorthwindCRUD/QueryBuilder/QueryExecutor.cs @@ -98,7 +98,17 @@ private static Expression BuildConditionExpression(DataContext db, IQue "lessThan" => Expression.LessThan(field, searchValue), "greaterThanOrEqualTo" => Expression.GreaterThanOrEqual(field, searchValue), "lessThanOrEqualTo" => Expression.LessThanOrEqual(field, searchValue), - "all" => throw new NotImplementedException("Not implemented"), + "before" => Expression.LessThan(Expression.Call(field, typeof(string).GetMethod("CompareTo", new[] { typeof(string) })!, searchValue), Expression.Constant(0)), + "after" => Expression.GreaterThan(Expression.Call(field, typeof(string).GetMethod("CompareTo", new[] { typeof(string) })!, searchValue), Expression.Constant(0)), + "today" => Expression.Call(field, typeof(string).GetMethod("StartsWith", new[] { typeof(string) })!, Expression.Constant(DateTime.Now.Date.ToString("yyyy-MM-dd", CultureInfo.InvariantCulture))), + "yesterday" => Expression.Call(field, typeof(string).GetMethod("StartsWith", new[] { typeof(string) })!, Expression.Constant(DateTime.Now.Date.AddDays(-1).ToString("yyyy-MM-dd", CultureInfo.InvariantCulture))), + "thisMonth" => Expression.Call(field, typeof(string).GetMethod("StartsWith", new[] { typeof(string) })!, Expression.Constant(DateTime.Now.Date.ToString("yyyy-MM", CultureInfo.InvariantCulture))), + "lastMonth" => Expression.Call(field, typeof(string).GetMethod("StartsWith", new[] { typeof(string) })!, Expression.Constant(DateTime.Now.Date.AddMonths(-1).ToString("yyyy-MM", CultureInfo.InvariantCulture))), + "nextMonth" => Expression.Call(field, typeof(string).GetMethod("StartsWith", new[] { typeof(string) })!, Expression.Constant(DateTime.Now.Date.AddMonths(1).ToString("yyyy-MM", CultureInfo.InvariantCulture))), + "thisYear" => Expression.Call(field, typeof(string).GetMethod("StartsWith", new[] { typeof(string) })!, Expression.Constant(DateTime.Now.Date.ToString("yyyy", CultureInfo.InvariantCulture))), + "lastYear" => Expression.Call(field, typeof(string).GetMethod("StartsWith", new[] { typeof(string) })!, Expression.Constant(DateTime.Now.Date.AddYears(-1).ToString("yyyy", CultureInfo.InvariantCulture))), + "nextYear" => Expression.Call(field, typeof(string).GetMethod("StartsWith", new[] { typeof(string) })!, Expression.Constant(DateTime.Now.Date.AddYears(1).ToString("yyyy", CultureInfo.InvariantCulture))), + "all" => Expression.Constant(true), "true" => Expression.IsTrue(field), "false" => Expression.IsFalse(field), _ => throw new NotImplementedException("Not implemented"), diff --git a/NorthwindCRUD/QueryBuilder/SqlGenerator.cs b/NorthwindCRUD/QueryBuilder/SqlGenerator.cs index b5e7d6a..dad48f6 100644 --- a/NorthwindCRUD/QueryBuilder/SqlGenerator.cs +++ b/NorthwindCRUD/QueryBuilder/SqlGenerator.cs @@ -1,6 +1,7 @@ namespace QueryBuilder; using System.Diagnostics.CodeAnalysis; +using System.Globalization; [SuppressMessage("StyleCop.CSharp.SpacingRules", "SA1025:Code should not contain multiple whitespace in a row", Justification = "...")] public static class SqlGenerator @@ -46,8 +47,8 @@ private static string BuildCondition(QueryFilter filter) "notEmpty" => $"{field} <> ''", "equals" => $"{field} = {value}", "doesNotEqual" => $"{field} <> {value}", - "in" => $"{field} IN ({subquery})", - "notIn" => $"{field} NOT IN ({subquery})", + "inQuery" => $"{field} IN ({subquery})", + "notInQuery" => $"{field} NOT IN ({subquery})", "contains" => $"{field} LIKE '%{filter.SearchVal}%'", "doesNotContain" => $"{field} NOT LIKE '%{filter.SearchVal}%'", "startsWith" => $"{field} LIKE '{filter.SearchVal}%'", @@ -56,7 +57,17 @@ private static string BuildCondition(QueryFilter filter) "lessThan" => $"{field} < {value}", "greaterThanOrEqualTo" => $"{field} >= {value}", "lessThanOrEqualTo" => $"{field} <= {value}", - "all" => throw new NotImplementedException("Not implemented"), + "before" => $"{field} < {value}", + "after" => $"{field} > {value}", + "today" => $"{field} LIKE '{DateTime.Now.Date.ToString("yyyy-MM-dd", CultureInfo.InvariantCulture)}%'", + "yesterday" => $"{field} LIKE '{DateTime.Now.Date.AddDays(-1).ToString("yyyy-MM-dd", CultureInfo.InvariantCulture)}%'", + "thisMonth" => $"{field} LIKE '{DateTime.Now.Date.ToString("yyyy-MM", CultureInfo.InvariantCulture)}%'", + "lastMonth" => $"{field} LIKE '{DateTime.Now.Date.AddMonths(-1).ToString("yyyy-MM", CultureInfo.InvariantCulture)}%'", + "nextMonth" => $"{field} LIKE '{DateTime.Now.Date.AddMonths(1).ToString("yyyy-MM", CultureInfo.InvariantCulture)}%'", + "thisYear" => $"{field} LIKE '{DateTime.Now.Date.ToString("yyyy", CultureInfo.InvariantCulture)}%'", + "lastYear" => $"{field} LIKE '{DateTime.Now.Date.AddYears(-1).ToString("yyyy", CultureInfo.InvariantCulture)}%'", + "nextYear" => $"{field} LIKE '{DateTime.Now.Date.AddYears(1).ToString("yyyy", CultureInfo.InvariantCulture)}%'", + "all" => "TRUE", "true" => $"{field} = TRUE", "false" => $"{field} = FALSE", _ => throw new NotImplementedException($"Condition '{condition}' is not implemented"), From e5c720605e5298ecde28b63f6f93dc0beab6904b Mon Sep 17 00:00:00 2001 From: Javier Coitino Date: Wed, 12 Mar 2025 20:25:28 -0300 Subject: [PATCH 37/41] Add missing operators --- NorthwindCRUD/QueryBuilder/QueryExecutor.cs | 4 ++++ NorthwindCRUD/QueryBuilder/SqlGenerator.cs | 7 ++++++- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/NorthwindCRUD/QueryBuilder/QueryExecutor.cs b/NorthwindCRUD/QueryBuilder/QueryExecutor.cs index eec48a2..21958ed 100644 --- a/NorthwindCRUD/QueryBuilder/QueryExecutor.cs +++ b/NorthwindCRUD/QueryBuilder/QueryExecutor.cs @@ -108,6 +108,10 @@ private static Expression BuildConditionExpression(DataContext db, IQue "thisYear" => Expression.Call(field, typeof(string).GetMethod("StartsWith", new[] { typeof(string) })!, Expression.Constant(DateTime.Now.Date.ToString("yyyy", CultureInfo.InvariantCulture))), "lastYear" => Expression.Call(field, typeof(string).GetMethod("StartsWith", new[] { typeof(string) })!, Expression.Constant(DateTime.Now.Date.AddYears(-1).ToString("yyyy", CultureInfo.InvariantCulture))), "nextYear" => Expression.Call(field, typeof(string).GetMethod("StartsWith", new[] { typeof(string) })!, Expression.Constant(DateTime.Now.Date.AddYears(1).ToString("yyyy", CultureInfo.InvariantCulture))), + "at" => Expression.Equal(field, searchValue), + "not_at" => Expression.NotEqual(field, searchValue), + "at_before" => Expression.LessThan(Expression.Call(field, typeof(string).GetMethod("CompareTo", new[] { typeof(string) })!, searchValue), Expression.Constant(0)), + "at_after" => Expression.GreaterThan(Expression.Call(field, typeof(string).GetMethod("CompareTo", new[] { typeof(string) })!, searchValue), Expression.Constant(0)), "all" => Expression.Constant(true), "true" => Expression.IsTrue(field), "false" => Expression.IsFalse(field), diff --git a/NorthwindCRUD/QueryBuilder/SqlGenerator.cs b/NorthwindCRUD/QueryBuilder/SqlGenerator.cs index dad48f6..359466d 100644 --- a/NorthwindCRUD/QueryBuilder/SqlGenerator.cs +++ b/NorthwindCRUD/QueryBuilder/SqlGenerator.cs @@ -47,6 +47,7 @@ private static string BuildCondition(QueryFilter filter) "notEmpty" => $"{field} <> ''", "equals" => $"{field} = {value}", "doesNotEqual" => $"{field} <> {value}", + "in" => $"{field} IN ({value})", "inQuery" => $"{field} IN ({subquery})", "notInQuery" => $"{field} NOT IN ({subquery})", "contains" => $"{field} LIKE '%{filter.SearchVal}%'", @@ -67,10 +68,14 @@ private static string BuildCondition(QueryFilter filter) "thisYear" => $"{field} LIKE '{DateTime.Now.Date.ToString("yyyy", CultureInfo.InvariantCulture)}%'", "lastYear" => $"{field} LIKE '{DateTime.Now.Date.AddYears(-1).ToString("yyyy", CultureInfo.InvariantCulture)}%'", "nextYear" => $"{field} LIKE '{DateTime.Now.Date.AddYears(1).ToString("yyyy", CultureInfo.InvariantCulture)}%'", + "at" => $"{field} = {value}", + "not_at" => $"{field} <> {value}", + "at_before" => $"{field} < {value}", + "at_after" => $"{field} > {value}", "all" => "TRUE", "true" => $"{field} = TRUE", "false" => $"{field} = FALSE", - _ => throw new NotImplementedException($"Condition '{condition}' is not implemented"), + _ => $"{field} {condition} {value}", }; } } \ No newline at end of file From e75a48acc617d5575f8e718f57409ee8d22f1554 Mon Sep 17 00:00:00 2001 From: Javier Coitino Date: Thu, 13 Mar 2025 07:00:37 -0300 Subject: [PATCH 38/41] Code polishing --- NorthwindCRUD/QueryBuilder/QueryExecutor.cs | 68 +++++++++++++++------ 1 file changed, 51 insertions(+), 17 deletions(-) diff --git a/NorthwindCRUD/QueryBuilder/QueryExecutor.cs b/NorthwindCRUD/QueryBuilder/QueryExecutor.cs index 21958ed..4338adf 100644 --- a/NorthwindCRUD/QueryBuilder/QueryExecutor.cs +++ b/NorthwindCRUD/QueryBuilder/QueryExecutor.cs @@ -13,7 +13,6 @@ /// /// A generic query executor that can be used to execute queries on IQueryable data sources. /// -[SuppressMessage("StyleCop.CSharp.SpacingRules", "SA1009:Closing parenthesis should be spaced correctly", Justification = "...")] [SuppressMessage("StyleCop.CSharp.SpacingRules", "SA1025:Code should not contain multiple whitespace in a row", Justification = "...")] public static class QueryExecutor { @@ -80,6 +79,7 @@ private static Expression BuildConditionExpression(DataContext db, IQue var targetType = property.PropertyType; var searchValue = GetSearchValue(filter.SearchVal, targetType); var emptyValue = GetEmptyValue(targetType); + var now = DateTime.Now.Date; Expression condition = filter.Condition.Name switch { "null" => targetType.IsNullableType() ? Expression.Equal(field, Expression.Constant(targetType.GetDefaultValue())) : Expression.Constant(false), @@ -90,28 +90,28 @@ private static Expression BuildConditionExpression(DataContext db, IQue "doesNotEqual" => Expression.NotEqual(field, searchValue), "inQuery" => BuildInExpression(db, filter.SearchTree, field), "notInQuery" => Expression.Not(BuildInExpression(db, filter.SearchTree, field)), - "contains" => Expression.Call(field, typeof(string).GetMethod("Contains", new[] { typeof(string) })!, searchValue), - "doesNotContain" => Expression.Not(Expression.Call(field, typeof(string).GetMethod("Contains", new[] { typeof(string) })!, searchValue)), - "startsWith" => Expression.Call(field, typeof(string).GetMethod("StartsWith", new[] { typeof(string) })!, searchValue), - "endsWith" => Expression.Call(field, typeof(string).GetMethod("EndsWith", new[] { typeof(string) })!, searchValue), + "contains" => CallContains(field, searchValue), + "doesNotContain" => Expression.Not(CallContains(field, searchValue)), + "startsWith" => CallStartsWith(field, searchValue), + "endsWith" => CallEndsWith(field, searchValue), "greaterThan" => Expression.GreaterThan(field, searchValue), "lessThan" => Expression.LessThan(field, searchValue), "greaterThanOrEqualTo" => Expression.GreaterThanOrEqual(field, searchValue), "lessThanOrEqualTo" => Expression.LessThanOrEqual(field, searchValue), - "before" => Expression.LessThan(Expression.Call(field, typeof(string).GetMethod("CompareTo", new[] { typeof(string) })!, searchValue), Expression.Constant(0)), - "after" => Expression.GreaterThan(Expression.Call(field, typeof(string).GetMethod("CompareTo", new[] { typeof(string) })!, searchValue), Expression.Constant(0)), - "today" => Expression.Call(field, typeof(string).GetMethod("StartsWith", new[] { typeof(string) })!, Expression.Constant(DateTime.Now.Date.ToString("yyyy-MM-dd", CultureInfo.InvariantCulture))), - "yesterday" => Expression.Call(field, typeof(string).GetMethod("StartsWith", new[] { typeof(string) })!, Expression.Constant(DateTime.Now.Date.AddDays(-1).ToString("yyyy-MM-dd", CultureInfo.InvariantCulture))), - "thisMonth" => Expression.Call(field, typeof(string).GetMethod("StartsWith", new[] { typeof(string) })!, Expression.Constant(DateTime.Now.Date.ToString("yyyy-MM", CultureInfo.InvariantCulture))), - "lastMonth" => Expression.Call(field, typeof(string).GetMethod("StartsWith", new[] { typeof(string) })!, Expression.Constant(DateTime.Now.Date.AddMonths(-1).ToString("yyyy-MM", CultureInfo.InvariantCulture))), - "nextMonth" => Expression.Call(field, typeof(string).GetMethod("StartsWith", new[] { typeof(string) })!, Expression.Constant(DateTime.Now.Date.AddMonths(1).ToString("yyyy-MM", CultureInfo.InvariantCulture))), - "thisYear" => Expression.Call(field, typeof(string).GetMethod("StartsWith", new[] { typeof(string) })!, Expression.Constant(DateTime.Now.Date.ToString("yyyy", CultureInfo.InvariantCulture))), - "lastYear" => Expression.Call(field, typeof(string).GetMethod("StartsWith", new[] { typeof(string) })!, Expression.Constant(DateTime.Now.Date.AddYears(-1).ToString("yyyy", CultureInfo.InvariantCulture))), - "nextYear" => Expression.Call(field, typeof(string).GetMethod("StartsWith", new[] { typeof(string) })!, Expression.Constant(DateTime.Now.Date.AddYears(1).ToString("yyyy", CultureInfo.InvariantCulture))), + "before" => Expression.LessThan(CallCompare(field, searchValue), Expression.Constant(0)), + "after" => Expression.GreaterThan(CallCompare(field, searchValue), Expression.Constant(0)), + "today" => CallStartsWith(field, now.ToString("yyyy-MM-dd", CultureInfo.InvariantCulture)), + "yesterday" => CallStartsWith(field, now.AddDays(-1).ToString("yyyy-MM-dd", CultureInfo.InvariantCulture)), + "thisMonth" => CallStartsWith(field, now.ToString("yyyy-MM", CultureInfo.InvariantCulture)), + "lastMonth" => CallStartsWith(field, now.AddMonths(-1).ToString("yyyy-MM", CultureInfo.InvariantCulture)), + "nextMonth" => CallStartsWith(field, now.AddMonths(1).ToString("yyyy-MM", CultureInfo.InvariantCulture)), + "thisYear" => CallStartsWith(field, now.ToString("yyyy", CultureInfo.InvariantCulture)), + "lastYear" => CallStartsWith(field, now.AddYears(-1).ToString("yyyy", CultureInfo.InvariantCulture)), + "nextYear" => CallStartsWith(field, now.AddYears(1).ToString("yyyy", CultureInfo.InvariantCulture)), "at" => Expression.Equal(field, searchValue), "not_at" => Expression.NotEqual(field, searchValue), - "at_before" => Expression.LessThan(Expression.Call(field, typeof(string).GetMethod("CompareTo", new[] { typeof(string) })!, searchValue), Expression.Constant(0)), - "at_after" => Expression.GreaterThan(Expression.Call(field, typeof(string).GetMethod("CompareTo", new[] { typeof(string) })!, searchValue), Expression.Constant(0)), + "at_before" => Expression.LessThan(CallCompare(field, searchValue), Expression.Constant(0)), + "at_after" => Expression.GreaterThan(CallCompare(field, searchValue), Expression.Constant(0)), "all" => Expression.Constant(true), "true" => Expression.IsTrue(field), "false" => Expression.IsFalse(field), @@ -138,6 +138,40 @@ private static Expression BuildConditionExpression(DataContext db, IQue } } + private static Expression CallContains(Expression field, Expression searchValue) + { + var containsMethod = typeof(string).GetMethod("Contains", new[] { typeof(string) }); + return Expression.Call(field, containsMethod!, searchValue); + } + + private static Expression CallStartsWith(Expression field, Expression searchValue) + { + var startsWithMethod = typeof(string).GetMethod("StartsWith", new[] { typeof(string) }); + return Expression.Call(field, startsWithMethod!, searchValue); + } + + private static Expression CallStartsWith(Expression field, string dateLiteral) + { + return CallStartsWith(field, Expression.Constant(dateLiteral)); + } + + private static Expression CallEndsWith(Expression field, Expression searchValue) + { + var endsWithMethod = typeof(string).GetMethod("EndsWith", new[] { typeof(string) }); + return Expression.Call(field, endsWithMethod!, searchValue); + } + + private static Expression CallEndsWith(Expression field, string dateLiteral) + { + return CallEndsWith(field, Expression.Constant(dateLiteral)); + } + + private static Expression CallCompare(Expression field, Expression searchValue) + { + var compareMethod = typeof(string).GetMethod("Compare", new[] { typeof(string), typeof(string) }); + return Expression.Call(compareMethod!, field, searchValue); + } + private static Expression BuildInExpression(DataContext db, Query? query, MemberExpression field) { return field.Type switch From d2787454d71a590369b2a780e0a6d0e2e1d06a71 Mon Sep 17 00:00:00 2001 From: Pablo Date: Thu, 13 Mar 2025 10:50:24 -0300 Subject: [PATCH 39/41] Configure Newtonsoft to not mangle dates --- NorthwindCRUD/Program.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/NorthwindCRUD/Program.cs b/NorthwindCRUD/Program.cs index 8a6bb64..8ba88f8 100644 --- a/NorthwindCRUD/Program.cs +++ b/NorthwindCRUD/Program.cs @@ -4,6 +4,7 @@ using Microsoft.AspNetCore.Authentication.JwtBearer; using Microsoft.IdentityModel.Tokens; using Microsoft.OpenApi.Models; +using Newtonsoft.Json; using Newtonsoft.Json.Converters; using NorthwindCRUD.Filters; using NorthwindCRUD.Helpers; @@ -36,7 +37,8 @@ public static void Main(string[] args) options.SuppressImplicitRequiredAttributeForNonNullableReferenceTypes = true; }).AddNewtonsoftJson(options => { - options.SerializerSettings.ReferenceLoopHandling = Newtonsoft.Json.ReferenceLoopHandling.Ignore; + options.SerializerSettings.DateParseHandling = DateParseHandling.None; + options.SerializerSettings.ReferenceLoopHandling = ReferenceLoopHandling.Ignore; options.SerializerSettings.Converters.Add(new StringEnumConverter()); }); From 2cd40fad0a97af73d6406729f53ee7352d262d7d Mon Sep 17 00:00:00 2001 From: Javier Coitino Date: Thu, 13 Mar 2025 11:31:19 -0300 Subject: [PATCH 40/41] Fix true and false ops --- NorthwindCRUD/QueryBuilder/QueryExecutor.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/NorthwindCRUD/QueryBuilder/QueryExecutor.cs b/NorthwindCRUD/QueryBuilder/QueryExecutor.cs index 4338adf..580c16d 100644 --- a/NorthwindCRUD/QueryBuilder/QueryExecutor.cs +++ b/NorthwindCRUD/QueryBuilder/QueryExecutor.cs @@ -113,8 +113,8 @@ private static Expression BuildConditionExpression(DataContext db, IQue "at_before" => Expression.LessThan(CallCompare(field, searchValue), Expression.Constant(0)), "at_after" => Expression.GreaterThan(CallCompare(field, searchValue), Expression.Constant(0)), "all" => Expression.Constant(true), - "true" => Expression.IsTrue(field), - "false" => Expression.IsFalse(field), + "true" => Expression.Equal(field, Expression.Constant(true)), + "false" => Expression.Equal(field, Expression.Constant(false)), _ => throw new NotImplementedException("Not implemented"), }; if (filter.IgnoreCase.Value && field.Type == typeof(string)) From 92358df6f8b45ccbec1b88b83379df06a404cdca Mon Sep 17 00:00:00 2001 From: Javier Coitino Date: Thu, 13 Mar 2025 12:24:36 -0300 Subject: [PATCH 41/41] code polishing --- NorthwindCRUD/QueryBuilder/QueryExecutor.cs | 26 ++++++++++----------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/NorthwindCRUD/QueryBuilder/QueryExecutor.cs b/NorthwindCRUD/QueryBuilder/QueryExecutor.cs index 580c16d..0b4faa8 100644 --- a/NorthwindCRUD/QueryBuilder/QueryExecutor.cs +++ b/NorthwindCRUD/QueryBuilder/QueryExecutor.cs @@ -79,7 +79,7 @@ private static Expression BuildConditionExpression(DataContext db, IQue var targetType = property.PropertyType; var searchValue = GetSearchValue(filter.SearchVal, targetType); var emptyValue = GetEmptyValue(targetType); - var now = DateTime.Now.Date; + var today = DateTime.Now.Date; Expression condition = filter.Condition.Name switch { "null" => targetType.IsNullableType() ? Expression.Equal(field, Expression.Constant(targetType.GetDefaultValue())) : Expression.Constant(false), @@ -100,14 +100,14 @@ private static Expression BuildConditionExpression(DataContext db, IQue "lessThanOrEqualTo" => Expression.LessThanOrEqual(field, searchValue), "before" => Expression.LessThan(CallCompare(field, searchValue), Expression.Constant(0)), "after" => Expression.GreaterThan(CallCompare(field, searchValue), Expression.Constant(0)), - "today" => CallStartsWith(field, now.ToString("yyyy-MM-dd", CultureInfo.InvariantCulture)), - "yesterday" => CallStartsWith(field, now.AddDays(-1).ToString("yyyy-MM-dd", CultureInfo.InvariantCulture)), - "thisMonth" => CallStartsWith(field, now.ToString("yyyy-MM", CultureInfo.InvariantCulture)), - "lastMonth" => CallStartsWith(field, now.AddMonths(-1).ToString("yyyy-MM", CultureInfo.InvariantCulture)), - "nextMonth" => CallStartsWith(field, now.AddMonths(1).ToString("yyyy-MM", CultureInfo.InvariantCulture)), - "thisYear" => CallStartsWith(field, now.ToString("yyyy", CultureInfo.InvariantCulture)), - "lastYear" => CallStartsWith(field, now.AddYears(-1).ToString("yyyy", CultureInfo.InvariantCulture)), - "nextYear" => CallStartsWith(field, now.AddYears(1).ToString("yyyy", CultureInfo.InvariantCulture)), + "today" => CallStartsWith(field, today.ToString("yyyy-MM-dd", CultureInfo.InvariantCulture)), + "yesterday" => CallStartsWith(field, today.AddDays(-1).ToString("yyyy-MM-dd", CultureInfo.InvariantCulture)), + "thisMonth" => CallStartsWith(field, today.ToString("yyyy-MM", CultureInfo.InvariantCulture)), + "lastMonth" => CallStartsWith(field, today.AddMonths(-1).ToString("yyyy-MM", CultureInfo.InvariantCulture)), + "nextMonth" => CallStartsWith(field, today.AddMonths(1).ToString("yyyy-MM", CultureInfo.InvariantCulture)), + "thisYear" => CallStartsWith(field, today.ToString("yyyy", CultureInfo.InvariantCulture)), + "lastYear" => CallStartsWith(field, today.AddYears(-1).ToString("yyyy", CultureInfo.InvariantCulture)), + "nextYear" => CallStartsWith(field, today.AddYears(1).ToString("yyyy", CultureInfo.InvariantCulture)), "at" => Expression.Equal(field, searchValue), "not_at" => Expression.NotEqual(field, searchValue), "at_before" => Expression.LessThan(CallCompare(field, searchValue), Expression.Constant(0)), @@ -150,9 +150,9 @@ private static Expression CallStartsWith(Expression field, Expression searchValu return Expression.Call(field, startsWithMethod!, searchValue); } - private static Expression CallStartsWith(Expression field, string dateLiteral) + private static Expression CallStartsWith(Expression field, string literal) { - return CallStartsWith(field, Expression.Constant(dateLiteral)); + return CallStartsWith(field, Expression.Constant(literal)); } private static Expression CallEndsWith(Expression field, Expression searchValue) @@ -161,9 +161,9 @@ private static Expression CallEndsWith(Expression field, Expression searchValue) return Expression.Call(field, endsWithMethod!, searchValue); } - private static Expression CallEndsWith(Expression field, string dateLiteral) + private static Expression CallEndsWith(Expression field, string literal) { - return CallEndsWith(field, Expression.Constant(dateLiteral)); + return CallEndsWith(field, Expression.Constant(literal)); } private static Expression CallCompare(Expression field, Expression searchValue)