287 lines
11 KiB
C#
287 lines
11 KiB
C#
using System;
|
|
using System.Collections;
|
|
using System.Collections.Generic;
|
|
using System.Linq;
|
|
using System.Linq.Expressions;
|
|
using System.Reflection;
|
|
|
|
namespace Generic.Services;
|
|
|
|
public static class AdvancedFilterExtension
|
|
{
|
|
public static IQueryable<T> ApplyAdvancedFilter<T, TFilter>(this IQueryable<T> query, TFilter filter)
|
|
where TFilter : class
|
|
{
|
|
if (filter == null) return query;
|
|
|
|
var entityType = typeof(T);
|
|
var filterProperties = typeof(TFilter).GetProperties()
|
|
.Where(p => p.GetValue(filter) != null)
|
|
.ToDictionary(p => p.Name, p => p.GetValue(filter));
|
|
|
|
if (!filterProperties.Any()) return query;
|
|
|
|
// 1. Map Entity Properties with their Paths and Levels
|
|
var entityMap = ScanEntity(entityType);
|
|
|
|
// 2. Process by Level (Priority)
|
|
var maxLevel = entityMap.Any() ? entityMap.Max(m => m.Level) : 0;
|
|
|
|
var parameter = Expression.Parameter(entityType, "x");
|
|
|
|
// We keep track of which filter properties we've already applied
|
|
var appliedFilterKeys = new HashSet<string>();
|
|
|
|
for (int level = 1; level <= maxLevel; level++)
|
|
{
|
|
var levelProperties = entityMap.Where(m => m.Level == level).ToList();
|
|
|
|
foreach (var meta in levelProperties)
|
|
{
|
|
// If this property name exists in our filter and hasn't been applied yet
|
|
if (filterProperties.TryGetValue(meta.Name, out var filterValue) && !appliedFilterKeys.Contains(meta.Name))
|
|
{
|
|
// If it's a root property (Level 1)
|
|
if (meta.Level == 1)
|
|
{
|
|
var comparison = BuildComparison(parameter, meta.PropertyInfo, filterValue);
|
|
if (comparison != null)
|
|
{
|
|
var lambda = Expression.Lambda<Func<T, bool>>(comparison, parameter);
|
|
query = query.Where(lambda);
|
|
appliedFilterKeys.Add(meta.Name);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// It's a nested property. We need to find the "Branch Root" (the first collection in the path)
|
|
var branchInfo = FindBranchRoot(meta);
|
|
if (branchInfo != null)
|
|
{
|
|
// Discover all other filter properties that belong to this same branch
|
|
var branchFilters = DiscoverBranchFilters(branchInfo, entityMap, filterProperties, appliedFilterKeys);
|
|
|
|
// Apply them all together in one .Any() or nested .Where()
|
|
query = ApplyBranchFilter(query, parameter, branchInfo, branchFilters);
|
|
|
|
foreach (var usedKey in branchFilters.Keys) appliedFilterKeys.Add(usedKey);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return query;
|
|
}
|
|
|
|
private static List<PropertyMetadata> ScanEntity(Type type, int level = 1, string path = "", PropertyMetadata parent = null)
|
|
{
|
|
var metaList = new List<PropertyMetadata>();
|
|
var props = type.GetProperties(BindingFlags.Public | BindingFlags.Instance);
|
|
|
|
foreach (var p in props)
|
|
{
|
|
var currentPath = string.IsNullOrEmpty(path) ? p.Name : $"{path}.{p.Name}";
|
|
var meta = new PropertyMetadata
|
|
{
|
|
Name = p.Name,
|
|
Path = currentPath,
|
|
Level = level,
|
|
PropertyInfo = p,
|
|
PropertyType = p.PropertyType,
|
|
Parent = parent
|
|
};
|
|
|
|
metaList.Add(meta);
|
|
|
|
// If it's a complex type (not a system type or enum), dive deeper
|
|
if (IsComplexType(p.PropertyType))
|
|
{
|
|
var nextType = GetUnderlyingType(p.PropertyType);
|
|
if (nextType != type) // Prevent infinite recursion for parent/child relations
|
|
{
|
|
metaList.AddRange(ScanEntity(nextType, level + 1, currentPath, meta));
|
|
}
|
|
}
|
|
}
|
|
|
|
return metaList;
|
|
}
|
|
|
|
private static bool IsComplexType(Type type)
|
|
{
|
|
if (type.IsPrimitive || type == typeof(string) || type == typeof(Guid) || type == typeof(DateTime) || type == typeof(decimal) || type == typeof(double) || type.IsEnum)
|
|
return false;
|
|
|
|
var underlying = GetUnderlyingType(type);
|
|
return underlying != type || (!underlying.Namespace?.StartsWith("System") ?? true);
|
|
}
|
|
|
|
private static Type GetUnderlyingType(Type type)
|
|
{
|
|
if (type.IsGenericType && (type.GetGenericTypeDefinition() == typeof(List<>) || type.GetGenericTypeDefinition() == typeof(ICollection<>) || type.GetGenericTypeDefinition() == typeof(IEnumerable<>)))
|
|
return type.GetGenericArguments()[0];
|
|
|
|
if (type.IsArray) return type.GetElementType();
|
|
|
|
return type;
|
|
}
|
|
|
|
private static PropertyMetadata FindBranchRoot(PropertyMetadata meta)
|
|
{
|
|
var current = meta;
|
|
PropertyMetadata collectionRoot = null;
|
|
|
|
while (current != null)
|
|
{
|
|
if (IsCollection(current.PropertyType))
|
|
collectionRoot = current;
|
|
current = current.Parent;
|
|
}
|
|
|
|
return collectionRoot;
|
|
}
|
|
|
|
private static bool IsCollection(Type type) => type != typeof(string) && typeof(IEnumerable).IsAssignableFrom(type);
|
|
|
|
private static Dictionary<string, (PropertyMetadata Meta, object Value)> DiscoverBranchFilters(
|
|
PropertyMetadata branchRoot,
|
|
List<PropertyMetadata> allMeta,
|
|
Dictionary<string, object> filterValues,
|
|
HashSet<string> appliedKeys)
|
|
{
|
|
var results = new Dictionary<string, (PropertyMetadata, object)>();
|
|
|
|
// Find all metadata items that are descendants of this branch root
|
|
var descendants = allMeta.Where(m => m.Path.StartsWith(branchRoot.Path + ".")).ToList();
|
|
|
|
foreach (var d in descendants)
|
|
{
|
|
if (filterValues.TryGetValue(d.Name, out var val) && !appliedKeys.Contains(d.Name))
|
|
{
|
|
results[d.Name] = (d, val);
|
|
}
|
|
}
|
|
|
|
return results;
|
|
}
|
|
|
|
private static IQueryable<T> ApplyBranchFilter<T>(
|
|
IQueryable<T> query,
|
|
ParameterExpression rootParam,
|
|
PropertyMetadata branchRoot,
|
|
Dictionary<string, (PropertyMetadata Meta, object Value)> branchFilters)
|
|
{
|
|
var itemType = GetUnderlyingType(branchRoot.PropertyType);
|
|
var itemParam = Expression.Parameter(itemType, "t");
|
|
|
|
Expression branchExpression = null;
|
|
|
|
foreach (var bf in branchFilters.Values)
|
|
{
|
|
// Resolve path relative to the collection item
|
|
// Example: branchRoot is "Translations", bf.Meta.Path is "Translations.Info.Name"
|
|
// Relative path is "Info.Name"
|
|
var relativePath = bf.Meta.Path.Substring(branchRoot.Path.Length + 1);
|
|
var propertyExpr = BuildRelativePropertyExpression(itemParam, relativePath);
|
|
|
|
var comparison = BuildComparisonFromExpression(propertyExpr, bf.Value);
|
|
|
|
if (comparison != null)
|
|
{
|
|
branchExpression = branchExpression == null
|
|
? comparison
|
|
: Expression.AndAlso(branchExpression, comparison);
|
|
}
|
|
}
|
|
|
|
if (branchExpression == null) return query;
|
|
|
|
// Build: x => x.Translations.Any(t => t.Info.Name == "..." && t.Language == "...")
|
|
var anyMethod = typeof(Enumerable).GetMethods()
|
|
.First(m => m.Name == "Any" && m.GetParameters().Length == 2)
|
|
.MakeGenericMethod(itemType);
|
|
|
|
var collectionExpr = Expression.Property(rootParam, branchRoot.PropertyInfo);
|
|
var lambda = Expression.Lambda(branchExpression, itemParam);
|
|
var anyCall = Expression.Call(null, anyMethod, collectionExpr, lambda);
|
|
|
|
var finalLambda = Expression.Lambda<Func<T, bool>>(anyCall, rootParam);
|
|
return query.Where(finalLambda);
|
|
}
|
|
|
|
private static Expression BuildRelativePropertyExpression(Expression param, string path)
|
|
{
|
|
Expression expr = param;
|
|
foreach (var part in path.Split('.'))
|
|
{
|
|
expr = Expression.Property(expr, part);
|
|
}
|
|
return expr;
|
|
}
|
|
|
|
private static Expression? BuildComparison(Expression param, PropertyInfo prop, object? value)
|
|
{
|
|
var propertyExpr = Expression.Property(param, prop);
|
|
return BuildComparisonFromExpression(propertyExpr, value);
|
|
}
|
|
|
|
private static Expression? BuildComparisonFromExpression(Expression propertyExpr, object? value)
|
|
{
|
|
if (value == null) return null;
|
|
|
|
if (propertyExpr.Type == typeof(string))
|
|
{
|
|
var containsMethod = typeof(string).GetMethod("Contains", new[] { typeof(string) })!;
|
|
|
|
// x => x.Property.Contains(value)
|
|
var searchValue = Expression.Constant(value.ToString() ?? string.Empty, typeof(string));
|
|
|
|
return Expression.Call(propertyExpr, containsMethod, searchValue);
|
|
}
|
|
|
|
if (IsCollection(propertyExpr.Type) && GetUnderlyingType(propertyExpr.Type) == typeof(string))
|
|
{
|
|
var anyMethod = typeof(Enumerable).GetMethods()
|
|
.First(m => m.Name == "Any" && m.GetParameters().Length == 2)
|
|
.MakeGenericMethod(typeof(string));
|
|
|
|
var itemParam = Expression.Parameter(typeof(string), "s");
|
|
var containsMethod = typeof(string).GetMethod("Contains", new[] { typeof(string) })!;
|
|
|
|
var searchValue = Expression.Constant(value.ToString() ?? string.Empty, typeof(string));
|
|
var itemContains = Expression.Call(itemParam, containsMethod, searchValue);
|
|
|
|
var lambda = Expression.Lambda(itemContains, itemParam);
|
|
return Expression.Call(null, anyMethod, propertyExpr, lambda);
|
|
}
|
|
|
|
// Handle Nullable types for equality
|
|
var memberType = propertyExpr.Type;
|
|
if (memberType.IsGenericType && memberType.GetGenericTypeDefinition() == typeof(Nullable<>))
|
|
{
|
|
propertyExpr = Expression.Convert(propertyExpr, memberType.GetGenericArguments()[0]);
|
|
}
|
|
|
|
try
|
|
{
|
|
var convertedValue = Convert.ChangeType(value, propertyExpr.Type);
|
|
return Expression.Equal(propertyExpr, Expression.Constant(convertedValue, propertyExpr.Type));
|
|
}
|
|
catch
|
|
{
|
|
return Expression.Equal(propertyExpr, Expression.Constant(value, propertyExpr.Type));
|
|
}
|
|
}
|
|
|
|
private class PropertyMetadata
|
|
{
|
|
public string Name { get; set; }
|
|
public string Path { get; set; }
|
|
public int Level { get; set; }
|
|
public PropertyInfo PropertyInfo { get; set; }
|
|
public Type PropertyType { get; set; }
|
|
public PropertyMetadata Parent { get; set; }
|
|
}
|
|
}
|