TemplatePro/BZPT.SqlSugarRepository/SqlSugarDataSourceLoader.cs

536 lines
28 KiB
C#
Raw Normal View History

2025-07-17 22:41:38 +08:00
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<ListResult<T>> LoadAsync<T>(this ISugarQueryable<T> source, DataSourceLoadOptions options) where T : class, new()
{
// 1. Apply Filtering
var filteredQuery = ApplyFilter(source, options.Filter as IList<object>);
// 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<Group>() // Return empty group structure
// };
}
// 6. Execute Query to Get Data
List<T> data = await pagedQuery.ToListAsync();
// 7. Assemble Result
return new ListResult<T>(data, totalCount, totalSummaries, 200);
}
// --- Private Helper Methods ---
#region Filtering
private static ISugarQueryable<T> ApplyFilter<T>(ISugarQueryable<T> query, IList<object>? 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<Func<T, bool>>(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<object> filter, ParameterExpression parameter)
{
// Format: [ "field", "operator", value ] or [ filter1, "and/or", filter2, ... ] or [ "!", filter ]
if (filter.Count > 0 && filter[0] is IList<object> nestedFilter) // Nested filters or logical operator
{
var expressions = new List<Expression>();
string? logicalOperator = null;
foreach (var item in filter)
{
if (item is IList<object> 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<object> 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<T>)
}
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<int> property with int constant
if (memberType != valueType)
{
if (Nullable.GetUnderlyingType(memberType) == valueType && valueType.IsValueType) // Property is Nullable<T>, Value is T
{
// Promote value to Nullable<T>
right = Expression.Convert(valueExpression, memberType);
}
else if (memberType == Nullable.GetUnderlyingType(valueType) && memberType.IsValueType) // Property is T, Value is Nullable<T>
{
// 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<int>.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<object> itemsList;
try
{
itemsList = valueCollection.Cast<object>().ToList();
}
catch (Exception ex)
{
throw new InvalidOperationException($"为字段 '{member.Member.Name}' 的 '{operation}' 操作符转换集合项到 List<object> 失败。", ex);
}
if (!itemsList.Any())
{
return Expression.Constant(false); // 字段 IN (空列表) 结果为 false
}
// 使用 Enumerable.Contains<object>(IEnumerable<object> 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<T> ApplySorting<T>(ISugarQueryable<T> query, SortingInfo[]? sortOptions) where T : class, new()
{
if (sortOptions == null || sortOptions.Length == 0)
return query;
var orderByList = new List<OrderByModel>();
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<T> ApplyPaging<T>(ISugarQueryable<T> 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<object?[]?> CalculateTotalSummariesAsync<T>(ISugarQueryable<T> 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<Func<T, TFieldType>>
// We need to dynamically create the correct generic type for the lambda
switch (summaryInfo.SummaryType.ToLowerInvariant())
{
case "sum":
// Need Expression<Func<T, TResult>> where TResult is the member type or decimal/double
var sumLambda = Expression.Lambda<Func<T, object>>(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<Func<T, decimal>>)selectorLambda);
// else if (...) // other numeric types
break;
case "avg":
var avgLambda = Expression.Lambda<Func<T, object>>(Expression.Convert(member, typeof(object)), parameter);
results[i] = await query.AvgAsync(avgLambda);
break;
case "min":
var minLambda = Expression.Lambda<Func<T, object>>(Expression.Convert(member, typeof(object)), parameter);
results[i] = await query.MinAsync(minLambda);
break;
case "max":
var maxLambda = Expression.Lambda<Func<T, object>>(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
}
}