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 ApplyAdvancedFilter(this IQueryable 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(); 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>(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 ScanEntity(Type type, int level = 1, string path = "", PropertyMetadata parent = null) { var metaList = new List(); 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 DiscoverBranchFilters( PropertyMetadata branchRoot, List allMeta, Dictionary filterValues, HashSet appliedKeys) { var results = new Dictionary(); // 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 ApplyBranchFilter( IQueryable query, ParameterExpression rootParam, PropertyMetadata branchRoot, Dictionary 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>(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; } } }