diff --git a/QueryExecutor.cs b/QueryExecutor.cs index dcbe073..5138b83 100644 --- a/QueryExecutor.cs +++ b/QueryExecutor.cs @@ -1,9 +1,4 @@ -using AutoMapper; -using AutoMapper.Internal; -using AutoMapper.QueryableExtensions; -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Infrastructure; -using Swashbuckle.AspNetCore.SwaggerGen; +using System.ComponentModel.DataAnnotations; using System.Globalization; using System.Linq.Expressions; using System.Reflection; @@ -11,6 +6,12 @@ using System.Text.Json; using System.Text.Json.Nodes; using System.Text.Json.Serialization; +using AutoMapper; +using AutoMapper.Internal; +using AutoMapper.QueryableExtensions; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Swashbuckle.AspNetCore.SwaggerGen; namespace Infragistics.QueryBuilder.Executor { @@ -33,6 +34,20 @@ public static object[] Run(this IQueryable source, Qu return db is not null ? BuildQuery(db, source, query, mapper).ToArray() : Array.Empty(); } + public static object[] InvokeRunMethod(Type[] genericArgs, object?[] parameters) + { + var method = typeof(QueryExecutor) + .GetMethods(BindingFlags.Static | BindingFlags.Public) + .FirstOrDefault(m => + m.Name == "Run" && + m.IsGenericMethodDefinition && + m.GetGenericArguments().Length == genericArgs.Length) + ?.MakeGenericMethod(genericArgs); + + var result = method?.Invoke(null, parameters) ?? Array.Empty(); + return (object[])result; + } + private static IQueryable BuildQuery(DbContext db, IQueryable source, Query? query, IMapper? mapper = null) { if (query is null) @@ -222,9 +237,18 @@ private static Expression BuildInExpression(DbContext db, Query? query, Membe private static IEnumerable RunSubquery(DbContext db, Query? query) { - var t = query?.Entity.ToLower(CultureInfo.InvariantCulture) ?? string.Empty; - var p = db.GetType().GetProperty(t, BindingFlags.IgnoreCase | BindingFlags.Public | BindingFlags.Instance) ?? throw new InvalidOperationException($"Property '{t}' not found on type '{db.GetType()}'"); - return p.GetValue(db) is not IQueryable q ? Array.Empty() : [.. q.Run(query)]; + var propName = query?.Entity.ToLower(CultureInfo.InvariantCulture) ?? string.Empty; + var prop = db.GetType().GetProperty(propName, BindingFlags.IgnoreCase | BindingFlags.Public | BindingFlags.Instance) + ?? throw new InvalidOperationException($"Property '{propName}' not found on type '{db.GetType()}'"); + + var methods = typeof(QueryExecutor).GetMethods(BindingFlags.Static | BindingFlags.Public); + var method = methods?.FirstOrDefault(m => m.CustomAttributes.Count() == 1); + var dbSet = prop.GetValue(db) ?? throw new ValidationException($"DbSet property '{prop.Name}' is null in DbContext."); + var genericType = prop.PropertyType.GetGenericArguments().FirstOrDefault() ?? throw new ValidationException($"Missing DbSet generic type"); + var queryable = dbSet?.GetType().GetMethod("AsQueryable")?.Invoke(dbSet, null); + + return InvokeRunMethod([genericType], [queryable, query]); + } private static dynamic? ProjectField(object? obj, string field) @@ -242,13 +266,29 @@ private static Expression GetSearchValue(JsonValue? jsonVal, Type targetType) } var nonNullableType = Nullable.GetUnderlyingType(targetType) ?? targetType; - var value = jsonVal.Deserialize(targetType); - if (nonNullableType.IsEnum && value is string) + if (nonNullableType.IsEnum) { - return Expression.Constant(Enum.Parse(nonNullableType, (string)value)); + if (valueKind == JsonValueKind.String) + { + var enumValue = jsonVal.Deserialize(); + if (enumValue != null) + { + return Expression.Constant(Enum.Parse(nonNullableType, enumValue)); + } + } + else if (valueKind == JsonValueKind.Number) + { + var enumValue = jsonVal.Deserialize(); + if (enumValue != null) + { + return Expression.Constant(Enum.ToObject(nonNullableType, enumValue)); + } + } } + var value = jsonVal.Deserialize(targetType); + var convertedValue = Convert.ChangeType(value, nonNullableType, CultureInfo.InvariantCulture); return Expression.Constant(convertedValue, targetType); } diff --git a/Services/QueryBuilderService.cs b/Services/QueryBuilderService.cs index a9f981e..6cdabdc 100644 --- a/Services/QueryBuilderService.cs +++ b/Services/QueryBuilderService.cs @@ -1,8 +1,9 @@ -using AutoMapper; -using Microsoft.EntityFrameworkCore; -using Microsoft.Extensions.Logging; +using System.ComponentModel.DataAnnotations; using System.Globalization; using System.Reflection; +using AutoMapper; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Logging; namespace Infragistics.QueryBuilder.Executor { @@ -15,35 +16,25 @@ public Dictionary RunQuery(Query query) var t = query.Entity.ToLower(CultureInfo.InvariantCulture); var propInfo = db?.GetType().GetProperties() - .FirstOrDefault(p => p.PropertyType.IsGenericType && p.Name.ToLower(CultureInfo.InvariantCulture) == t && p.PropertyType.GetGenericTypeDefinition() == typeof(DbSet<>)); - if (propInfo != null) - { - var methods = typeof(QueryExecutor).GetMethods(BindingFlags.Static | BindingFlags.Public); - var method = methods?.FirstOrDefault(m => m.CustomAttributes.Count() == 2); + .FirstOrDefault(p => + p.PropertyType.IsGenericType && + p.Name.ToLower(CultureInfo.InvariantCulture) == t && + p.PropertyType.GetGenericTypeDefinition() == typeof(DbSet<>)) + ?? throw new InvalidOperationException($"Unknown entity {t}"); + + var dbSet = propInfo.GetValue(db) ?? throw new ValidationException($"DbSet property '{propInfo.Name}' is null in DbContext."); + var dbGenericType = dbSet.GetType().GenericTypeArguments.FirstOrDefault() ?? throw new ValidationException($"Missing DbSet generic type"); + + var resultProperty = typeof(TResults).GetProperty(t, BindingFlags.Instance | BindingFlags.Public | BindingFlags.IgnoreCase) + ?? throw new ValidationException($"Unknown entity {t}"); - var dbSet = propInfo.GetValue(db); - var dbGenericType = dbSet?.GetType()?.GenericTypeArguments.FirstOrDefault(); - if (dbGenericType != null && dbSet != null) - { - var dtoGenericType = typeof(TResults).GetProperty(propInfo.Name)?.PropertyType.GetElementType(); - if (dtoGenericType != null) - { - var genericMethod = method?.MakeGenericMethod(dbGenericType, dtoGenericType); + var dtoGenericType = resultProperty.PropertyType.GetElementType() ?? throw new ValidationException($"Missing Dto generic type"); - var asQueryableMethod = dbSet.GetType().GetMethod("AsQueryable"); - var queryable = asQueryableMethod?.Invoke(dbSet, null); - if (queryable != null) - { - if (genericMethod?.Invoke(null, [queryable, query, mapper]) is object[] propRes) - { - return new Dictionary { { propInfo.Name, propRes } }; - } - } - } - } - } + var queryable = dbSet.GetType().GetMethod("AsQueryable")?.Invoke(dbSet, null) + ?? throw new InvalidOperationException($"DbSet '{propInfo.Name}' does not support AsQueryable()."); - throw new InvalidOperationException($"Unknown entity {t}"); + var propRes = QueryExecutor.InvokeRunMethod([dbGenericType, dtoGenericType], [queryable, query, mapper]); + return new Dictionary { { propInfo.Name.ToLowerInvariant(), propRes } }; } } }