namespace BZPT.Repositories { using System; using System.Collections; using System.Collections.Generic; using System.Linq; using System.Linq.Expressions; using System.Reflection; using System.Threading.Tasks; using DevExtreme.AspNet.Data.ResponseModel; using DevExtreme.AspNet.Data; using SqlSugar; using NPlatform.Result; public static class SqlSugarDataSourceLoader { // Main method to load data public static async Task> LoadAsync(this ISugarQueryable source, DataSourceLoadOptions options) where T : class, new() { // 1. Apply Filtering var filteredQuery = ApplyFilter(source, options.Filter as IList); // 2. Calculate Total Summaries (before paging, after filtering) object[]? totalSummaries = null; if (options.TotalSummary != null && options.TotalSummary.Any()) { totalSummaries = await CalculateTotalSummariesAsync(filteredQuery, options.TotalSummary); } // 3. Calculate Total Count (before paging, after filtering) long totalCount = -1; // Use long for potentially large counts if (options.RequireTotalCount) { // Use CountAsync for efficiency totalCount = await filteredQuery.CountAsync(); } // 4. Apply Sorting var sortedQuery = ApplySorting(filteredQuery, options.Sort); // 5. Apply Paging var pagedQuery = ApplyPaging(sortedQuery, options.Skip, options.Take); // --- Grouping Check --- // Grouping is complex to implement correctly and efficiently with SqlSugar // matching DevExtreme's expected output structure. We explicitly don't support it here. if (options.Group != null && options.Group.Any()) { // Option 1: Throw an exception throw new NotImplementedException("Grouping is not supported by this SqlSugarDataSourceLoader implementation due to its complexity. Implement custom grouping logic if required."); // Option 2: Return an empty grouped result (less disruptive but might hide issues) // return new LoadResult { // groupCount = 0, // totalCount = totalCount, // Might still be relevant // summary = totalSummaries, // data = new List() // Return empty group structure // }; } // 6. Execute Query to Get Data List data = await pagedQuery.ToListAsync(); // 7. Assemble Result return new ListResult(data, totalCount, totalSummaries, 200); } // --- Private Helper Methods --- #region Filtering private static ISugarQueryable ApplyFilter(ISugarQueryable query, IList? filter) where T : class, new() { if (filter == null || filter.Count == 0) return query; try { var parameter = Expression.Parameter(typeof(T), "x"); var filterExpression = ParseFilter(filter, parameter); if (filterExpression != null) { var lambda = Expression.Lambda>(filterExpression, parameter); return query.Where(lambda); } } catch (Exception ex) { // Log the error details Console.WriteLine($"Error parsing DevExtreme filter: {ex}"); // Decide handling: throw, return original query, or return empty set // Throwing is often better to signal a malformed request throw new ArgumentException("Failed to parse the provided filter criteria.", nameof(filter), ex); } return query; } private static Expression? ParseFilter(IList filter, ParameterExpression parameter) { // Format: [ "field", "operator", value ] or [ filter1, "and/or", filter2, ... ] or [ "!", filter ] if (filter.Count > 0 && filter[0] is IList nestedFilter) // Nested filters or logical operator { var expressions = new List(); string? logicalOperator = null; foreach (var item in filter) { if (item is IList subFilter) { var parsed = ParseFilter(subFilter, parameter); if (parsed != null) expressions.Add(parsed); } else if (item is string op && (op.ToLowerInvariant() == "and" || op.ToLowerInvariant() == "or")) { if (logicalOperator != null && logicalOperator != op.ToLowerInvariant()) throw new ArgumentException("Mixing 'and' and 'or' at the same level is not supported without explicit nesting."); logicalOperator = op.ToLowerInvariant(); } } if (!expressions.Any()) return null; logicalOperator ??= "and"; // Default to AND if not specified Expression? combined = expressions[0]; for (int i = 1; i < expressions.Count; i++) { combined = logicalOperator == "or" ? Expression.OrElse(combined, expressions[i]) : Expression.AndAlso(combined, expressions[i]); } return combined; } else if (filter.Count == 2 && filter[0] is string unaryOp && unaryOp == "!") // Unary 'not' operator { if (filter[1] is IList subFilter) { var parsed = ParseFilter(subFilter, parameter); return parsed != null ? Expression.Not(parsed) : null; } throw new ArgumentException("Invalid '!' operator usage in filter."); } else if (filter.Count == 3 && filter[0] is string fieldName && filter[1] is string operation) // Simple condition { object? value = filter[2]; MemberExpression member = Expression.PropertyOrField(parameter, fieldName); Expression valueExpression = CreateConstantExpression(value, member.Type); return BuildComparisonExpression(member, operation, valueExpression, parameter); } else { throw new ArgumentException($"Invalid filter format: {string.Join(",", filter)}"); } } private static Expression CreateConstantExpression(object? value, Type targetType) { // Handle potential type mismatches, especially with nulls and enums if (value == null) { return Expression.Constant(null, targetType); } Type underlyingTargetType = Nullable.GetUnderlyingType(targetType) ?? targetType; object? convertedValue; try { if (underlyingTargetType.IsEnum) { convertedValue = Enum.Parse(underlyingTargetType, value.ToString()!, true); } else if (value is IConvertible) { // Handle DateTimeOffset specifically if necessary, depending on how DevExtreme sends it if (underlyingTargetType == typeof(DateTimeOffset) && value is string s && DateTimeOffset.TryParse(s, out var dto)) { convertedValue = dto; } else if (underlyingTargetType == typeof(DateTime) && value is string strDt && DateTime.TryParse(strDt, out var dt)) { // Add specific DateTime parsing if needed (e.g., handle specific formats/kinds) convertedValue = dt; } else if (underlyingTargetType == typeof(Guid) && value is string strGuid && Guid.TryParse(strGuid, out var guid)) { convertedValue = guid; } else { convertedValue = Convert.ChangeType(value, underlyingTargetType, System.Globalization.CultureInfo.InvariantCulture); } } else if (underlyingTargetType == value.GetType()) // 如果类型已完全匹配 { convertedValue = value; } // 如果目标类型不是集合,但传入的值是集合 (可能用于 'anyof') // 或者类型不匹配且值不是 IConvertible,我们直接使用原始值。 // BuildComparisonExpression 中的 'anyof' 分支会检查它是否真的是一个集合。 // 其他标量操作符如果收到集合,会在尝试比较时因类型不匹配而自然失败(或需要更复杂的处理)。 // 这种方式是妥协,避免 CreateConstantExpression 需要知道操作符。 else if (value is IEnumerable && !(value is string)) // 如果是集合 (且不是字符串,因为字符串也是IEnumerable) { // 对于 'anyof' 操作符,我们希望传递原始集合 // 对于其他操作符,如果到这里,说明类型不匹配且不是IConvertible, // 传递原始值,后续比较可能会失败,这是预期的。 convertedValue = value; // 直接使用原始集合对象 // 注意:此时的 targetType 可能与 value.GetType() 不同。 // Expression.Constant(convertedValue, value.GetType()) 可能更安全, // 但为了与现有逻辑兼容(期望返回 Expression.Constant(..., targetType)),我们暂时这样。 // 如果后续标量比较需要,可能需要调整。 // 更稳妥的做法是,如果CreateConstantExpression感知到是为anyof准备的,则直接返回Expression.Constant(value, value.GetType()) // 但这需要修改 ParseFilter 给 CreateConstantExpression 传递更多信息。 // 目前,我们让它通过,并寄希望于 targetType 兼容或 BuildComparisonExpression 正确处理。 return Expression.Constant(convertedValue, value.GetType()); // ***** 使用值的实际类型创建常量 ***** } else // 其他情况(非IConvertible,类型不匹配,也不是集合) { throw new InvalidCastException($"值 '{value}' (类型: {value.GetType().Name}) 不是IConvertible且与目标类型 '{underlyingTargetType.Name}' (字段: 不匹配,也不是一个可直接用于集合操作的 IEnumerable。"); } } catch (Exception ex) { throw new InvalidCastException($"Cannot convert filter value '{value}' (Type: {value.GetType().Name}) to target property type '{targetType.Name}'. Field: [Implicit FieldName]", ex); } return Expression.Constant(convertedValue, targetType); // Use the original targetType (which might be Nullable) } private static Expression BuildComparisonExpression(MemberExpression member, string operation, Expression valueExpression, ParameterExpression parameter) { var memberType = member.Type; var valueType = valueExpression.Type; // Ensure types are compatible for comparison, potentially adjusting for nullables Expression left = member; Expression right = valueExpression; // If one side is nullable and the other isn't, we might need a conversion // Example: Comparing Nullable property with int constant if (memberType != valueType) { if (Nullable.GetUnderlyingType(memberType) == valueType && valueType.IsValueType) // Property is Nullable, Value is T { // Promote value to Nullable right = Expression.Convert(valueExpression, memberType); } else if (memberType == Nullable.GetUnderlyingType(valueType) && memberType.IsValueType) // Property is T, Value is Nullable { // This scenario is less common with constants, but possible if the filter value was somehow nullable // We might need to access the .Value property of the nullable constant, but need null check right = Expression.Property(valueExpression, "Value"); // Potential NullReferenceException if value is null // Safer: Introduce null check? Or assume filter ensures non-null for comparison? // For simplicity here, assume non-null if comparing with non-nullable property. } // Add more sophisticated type matching/conversion if needed } switch (operation.ToLowerInvariant()) { case "=": return Expression.Equal(left, right); case "<>": return Expression.NotEqual(left, right); case ">": return Expression.GreaterThan(left, right); case ">=": return Expression.GreaterThanOrEqual(left, right); case "<": return Expression.LessThan(left, right); case "<=": return Expression.LessThanOrEqual(left, right); case "contains": // String.Contains, or check if collection contains value if (left.Type == typeof(string)) { MethodInfo containsMethod = typeof(string).GetMethod("Contains", new[] { typeof(string) }) ?? throw new InvalidOperationException("String.Contains method not found."); // Ensure the value is a string constant for String.Contains Expression stringValue = right.NodeType == ExpressionType.Constant && ((ConstantExpression)right).Value is string ? right : Expression.Call(right, typeof(object).GetMethod("ToString", Type.EmptyTypes)!); // Or handle error if not convertible return Expression.Call(left, containsMethod, stringValue); } else if (typeof(IEnumerable).IsAssignableFrom(left.Type) && left.Type.IsGenericType) // e.g. List.Contains(value) { // Assuming 'right' is the element type var elementType = left.Type.GetGenericArguments()[0]; MethodInfo containsMethod = typeof(Enumerable).GetMethods() .Single(m => m.Name == "Contains" && m.GetParameters().Length == 2) .MakeGenericMethod(elementType); return Expression.Call(containsMethod, left, right); // Might need type adjustment for 'right' } throw new NotSupportedException($"'contains' operator is not supported for type {left.Type}."); case "notcontains": if (left.Type == typeof(string)) { MethodInfo containsMethod = typeof(string).GetMethod("Contains", new[] { typeof(string) })!; Expression stringValue = right.NodeType == ExpressionType.Constant && ((ConstantExpression)right).Value is string ? right : Expression.Call(right, typeof(object).GetMethod("ToString", Type.EmptyTypes)!); return Expression.Not(Expression.Call(left, containsMethod, stringValue)); } // Add Enumerable logic similar to 'contains' if needed throw new NotSupportedException($"'notcontains' operator is not supported for type {left.Type}."); case "startswith": if (left.Type == typeof(string)) { MethodInfo startsWithMethod = typeof(string).GetMethod("StartsWith", new[] { typeof(string) })!; Expression stringValue = right.NodeType == ExpressionType.Constant && ((ConstantExpression)right).Value is string ? right : Expression.Call(right, typeof(object).GetMethod("ToString", Type.EmptyTypes)!); return Expression.Call(left, startsWithMethod, stringValue); } throw new NotSupportedException($"'startswith' operator is not supported for type {left.Type}."); case "endswith": if (left.Type == typeof(string)) { MethodInfo endsWithMethod = typeof(string).GetMethod("EndsWith", new[] { typeof(string) })!; Expression stringValue = right.NodeType == ExpressionType.Constant && ((ConstantExpression)right).Value is string ? right : Expression.Call(right, typeof(object).GetMethod("ToString", Type.EmptyTypes)!); return Expression.Call(left, endsWithMethod, stringValue); } throw new NotSupportedException($"'endswith' operator is not supported for type {left.Type}."); case "anyof": case "in": // 此时,valueExpression 应该是一个 Expression.Constant,其 .Value 是一个 IEnumerable 集合。 // 这是因为 CreateConstantExpression 在遇到 IEnumerable 时,返回了 Expression.Constant(value, value.GetType())。 if (!(valueExpression is ConstantExpression constantCollectionExpr)) { throw new ArgumentException( $"对于 '{operation}' 操作符,其值表达式必须是一个包含集合的 ConstantExpression。" + $"实际接收到的表达式类型: {valueExpression.GetType().FullName}。这通常是 CreateConstantExpression 未能正确处理集合作为常量。"); } object? collectionObject = constantCollectionExpr.Value; if (collectionObject == null) throw new ArgumentException($"字段 '{member.Member.Name}' 的 '{operation}' 操作符所对应的集合值不能为空。"); if (!(collectionObject is IEnumerable valueCollection)) { throw new ArgumentException( $"字段 '{member.Member.Name}' 的 '{operation}' 操作符所对应的值必须是一个 IEnumerable 集合。" + $"常量表达式中实际值的类型: {collectionObject.GetType().FullName}"); } List itemsList; try { itemsList = valueCollection.Cast().ToList(); } catch (Exception ex) { throw new InvalidOperationException($"为字段 '{member.Member.Name}' 的 '{operation}' 操作符转换集合项到 List 失败。", ex); } if (!itemsList.Any()) { return Expression.Constant(false); // 字段 IN (空列表) 结果为 false } // 使用 Enumerable.Contains(IEnumerable source, object value) MethodInfo containsMethodin = typeof(Enumerable).GetMethods(BindingFlags.Static | BindingFlags.Public) .Single(m => m.Name == "Contains" && m.GetParameters().Length == 2) .MakeGenericMethod(typeof(object)); Expression memberAsObject = Expression.Convert(member, typeof(object)); // 将属性成员转为 object // constantCollectionExpr (即 Expression.Constant(集合)) 作为源 // memberAsObject 作为要查找的值 return Expression.Call(null, containsMethodin, constantCollectionExpr, memberAsObject); default: throw new NotSupportedException($"Filter operator '{operation}' is not supported."); } } #endregion #region Sorting private static ISugarQueryable ApplySorting(ISugarQueryable query, SortingInfo[]? sortOptions) where T : class, new() { if (sortOptions == null || sortOptions.Length == 0) return query; var orderByList = new List(); foreach (var sortInfo in sortOptions) { if (string.IsNullOrWhiteSpace(sortInfo.Selector)) continue; orderByList.Add(new OrderByModel { FieldName = sortInfo.Selector, OrderByType = sortInfo.Desc ? OrderByType.Desc : OrderByType.Asc }); } if (orderByList.Any()) { // SqlSugar's OrderBy accepts a list for multi-column sorting return query.OrderBy(orderByList); } return query; } #endregion #region Paging private static ISugarQueryable ApplyPaging(ISugarQueryable query, int skip, int take) where T : class, new() { var result = query; // Start with the input query if (skip > 0) { result = result.Skip(skip); } if (take > 0) { // Important: SqlSugar's Skip/Take translate to LIMIT/OFFSET or ROWNUMBER based on DB. // Apply Take *after* Skip. result = result.Take(take); } return result; } #endregion #region Summaries private static async Task CalculateTotalSummariesAsync(ISugarQueryable query, SummaryInfo[] summaries) where T : class, new() { if (summaries == null || summaries.Length == 0) return null; var results = new object?[summaries.Length]; var parameter = Expression.Parameter(typeof(T), "s"); for (int i = 0; i < summaries.Length; i++) { var summaryInfo = summaries[i]; if (string.IsNullOrEmpty(summaryInfo.Selector)) { results[i] = null; // Cannot calculate summary without a selector continue; } try { MemberExpression member; try { member = Expression.PropertyOrField(parameter, summaryInfo.Selector); } catch (ArgumentException ex) // Handle invalid field name early { Console.WriteLine($"Error creating summary expression for selector '{summaryInfo.Selector}': {ex.Message}"); results[i] = null; // Or some error indicator continue; } // SqlSugar needs the Expression for aggregates var selectorLambda = Expression.Lambda(member, parameter); // e.g., s => s.Price // SqlSugar aggregate functions often need Expression> // We need to dynamically create the correct generic type for the lambda switch (summaryInfo.SummaryType.ToLowerInvariant()) { case "sum": // Need Expression> where TResult is the member type or decimal/double var sumLambda = Expression.Lambda>(Expression.Convert(member, typeof(object)), parameter); // Convert to object for generic SumAsync results[i] = await query.SumAsync(sumLambda); // SqlSugar might handle numeric conversion // Or try to be more specific if SumAsync overload exists: // if (member.Type == typeof(decimal)) results[i] = await query.SumAsync((Expression>)selectorLambda); // else if (...) // other numeric types break; case "avg": var avgLambda = Expression.Lambda>(Expression.Convert(member, typeof(object)), parameter); results[i] = await query.AvgAsync(avgLambda); break; case "min": var minLambda = Expression.Lambda>(Expression.Convert(member, typeof(object)), parameter); results[i] = await query.MinAsync(minLambda); break; case "max": var maxLambda = Expression.Lambda>(Expression.Convert(member, typeof(object)), parameter); results[i] = await query.MaxAsync(maxLambda); break; case "count": // Count doesn't usually need a selector in SQL (COUNT(*)), // but if DevExtreme asks for count based on a field, it implies counting non-null values of that field. // SqlSugar's CountAsync() counts rows. Use Where().CountAsync() for conditional count. // For simplicity, we'll just return the total count (already calculated if RequireTotalCount was true). // Or perform a separate count: results[i] = await query.CountAsync(); // This counts rows matching the filter break; default: Console.WriteLine($"Unsupported summary type: {summaryInfo.SummaryType}"); results[i] = null; // Unsupported summary type break; } } catch (Exception ex) { // Log error specific to this summary item Console.WriteLine($"Error calculating summary '{summaryInfo.SummaryType}' for selector '{summaryInfo.Selector}': {ex}"); results[i] = null; // Indicate error for this summary } } return results; } #endregion } }