TemplatePro/BZPT.SqlSugarRepository/SqlSugarDataSourceLoader.cs

536 lines
28 KiB
C#
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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
}
}