问题描述
我使用此 Pull Request 中的示例作为嵌套属性链的参考。不幸的是像
sql.Property<object>(x,"SomeClass.someProperty")
不起作用。
假设我有一个模型 SortDescriptor
(定义如下),它定义了我们如何动态地对代码中的项目进行排序。有没有办法动态创建表达式树来生成嵌套的 sql.Property
调用,就像下面显示的 ToPropertyExpression
方法一样?
如果给定以下过滤器,则应从 SortBy
函数输出以下 sql:
ORDER BY Category.EntityId,StackRank,Id
(Category
是 CategoryProperty
上的导航属性)
var filter1 = new PagedFilter
{
Take = 25,Sort = new List<SortDescriptor>
{
new SortDescriptor { Selector = "Category.EntityId" },new SortDescriptor { Selector = "StackRank" },new SortDescriptor { Selector = "Id" },}
};
以下是我使用的函数和对象:
public class CategoryPropertyRepository
{
public async Task<long> GetPageIndexById(PagedFilter filter,Guid id)
{
var entity = await DbContext.Set<Entities.CategoryProperty>()
.Select(x => new
{
x.Id,RowNumber = sql.Ext.RowNumber().Over().sortBy(x,filter.sort).Tovalue()
}).FirstOrDefaultAsyncLinqToDB(x => x.Id == id);
var rowNumber = entity.RowNumber;
return rowNumber / filter.Take.Value;
}
}
public static class IOrderExtensions
{
[sql.Extension("ORDER BY {entity}{filter}",TokenName = "order_by_clause",ServerSideOnly = true,BuilderType = typeof(SortByBuilder))]
public static AnalyticFunctions.IOrderedReadyToFunction<T> SortBy<T,TEntity>(
this AnalyticFunctions.IOverMayHavePartitionAndOrder<T> over,TEntity entity,IPagedFilter filter) => throw new InvalidOperationException("SortBy is server-side only.");
public class SortByBuilder : sql.IExtensionCallBuilder
{
public void Build(sql.ISqExtensionBuilder builder)
{
var entity = builder.Arguments[1];
var filter = builder.GetValue<IPagedFilter>("filter");
var index = 0;
var expression = $"ORDER BY {string.Join(",",filter.sort.Select(x => $"{{{index++}}}{(x.Descending ? " DESC" : string.Empty)}"))}";
List<IsqlExpression> parameters = new List<IsqlExpression>();
foreach (var sort in filter.sort)
{
var sqlExpr = builder.ConvertExpressionTosql(sort.ToPropertyExpression(entity));
parameters.Add(sqlExpr);
}
builder.ResultExpression = new sqlExpression(expression,Precedence.Primary,parameters.ToArray());
}
}
public static Expression ToPropertyExpression(this SortDescriptor sort,object entity)
{
var nameParts = sort.Selector.Split('.');
// x.someClass.someProperty should yield something like sql.Property<object>(sql.Property<object>(x,"SomeClass"),"SomeProperty);
var propertyMethod = typeof(sql).getmethod("Property",BindingFlags.Public | BindingFlags.Static);
propertyMethod = propertyMethod!.MakeGenericmethod(typeof(object));
Expression exp = null;
for (int i = nameParts.Length - 1; i >= 0; i--)
{
exp = Expression.Call(null,propertyMethod,Expression.Constant(exp ?? entity),Expression.Constant(nameParts[i]));
}
return exp;
}
}
public class PagedFilter : IPagedFilter
{
public virtual int? Skip { get; set; }
public virtual int? Take { get; set; }
public virtual IList<SortDescriptor> Sort { get; set; }
}
public class SortDescriptor
{
public string Selector { get; set; }
public bool Descending { get; set; }
}
每当我尝试执行与上述类似的操作时,我都会收到此错误消息,指出它正在尝试在客户端评估 sql.Property
表达式链:
异常信息:
LinqToDB.Linq.LinqException: 'Property' 只是服务器端方法。
堆栈跟踪:
在 LinqToDB.sql.Property[T](Object entity,String propertyName)
在 LinqToDB.Linq.QueryRunner.SetParameters(Query query,Expression expression,IDataContext parametersContext,Object[] parameters,Int32 queryNumber,sqlParameterValues parameterValues)
在 LinqToDB.Linq.QueryRunnerBase.SetCommand(Boolean clearQueryHints)
在 LinqToDB.Data.DataConnection.QueryRunner.n__0(Boolean clearQueryHints)
在 LinqToDB.Data.DataConnection.QueryRunner.ExecuteReaderAsync(CancellationToken cancellingToken)
在 LinqToDB.Linq.QueryRunner.AsyncEnumeratorImpl1.MoveNextAsync() at Microsoft.EntityFrameworkCore.EntityFrameworkQueryableExtensions.ToListAsync[TSource](IQueryable
1 源,CancellationToken 取消令牌)
在 Microsoft.EntityFrameworkCore.EntityFrameworkQueryableExtensions.ToListAsync[TSource](IQueryable1 source,CancellationToken cancellationToken) at LinqToDB.AsyncExtensions.ToListAsync[TSource](IQueryable
1 源,CancellationToken 令牌)
在 Experlogix.Api.DesignStudio.DataAccess.CategoryPropertyRepository.GetPageIndexById(CategoryPropertyFilter filter,Guid id) 在 D:\Dev\repos\Experlogix.Api.DesignStudio\src\Experlogix.Api.DesignStudio.DataAccess\CategoryPropertyRepository.16
在 Experlogix.Core.DataAccess.Service6.GetPageIndexById(TFilter filter,TId id) at Experlogix.Api.DesignStudio.Controllers.CategoryPropertyController.GetPageIndexAsync(Guid id,CategoryPropertyFilter filter) in D:\Dev\repos\Experlogix.Api.DesignStudio\src\Experlogix.Api.DesignStudio\Controllers\CategoryPropertyController.cs:line 46 at Microsoft.AspNetCore.Mvc.Infrastructure.ActionMethodExecutor.TaskOfIActionResultExecutor.Execute(IActionResultTypeMapper mapper,ObjectMethodExecutor executor,Object controller,Object[] arguments) at Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.<InvokeActionMethodAsync>g__Awaited|12_0(ControllerActionInvoker invoker,ValueTask
1 actionResultValueTask)
在 Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.g__Awaited|10_0(ControllerActionInvoker invoker,Task lastTask,State next,Scope scope,Object state,Boolean isCompleted)
在 Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.Rethrow(ActionExecutedContextSealed context)
在 Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.Next(State& next,Scope& scope,Object& state,Boolean& isCompleted)
在 Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.g__Awaited|13_0(ControllerActionInvoker invoker,Boolean isCompleted)
在 Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.g__Awaited|19_0(ResourceInvoker invoker,Boolean isCompleted)
在 Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.g__Awaited|17_0(ResourceInvoker invoker,Task task,Idisposable scope)
在 Microsoft.AspNetCore.Routing.EndpointMiddleware.g__AwaitRequestTask|6_0(Endpoint endpoint,Task requestTask,ILogger logger)
在 Experlogix.Api.DesignStudio.Middleware.DatabaseConnectionMiddleware.Invoke(HttpContext httpContext,IUserContext userContext,IConnectionContext connectionContext,IDatabaseClient databaseClient) 在 D:\Dev\repos\Experlogix.Api.DesignStudio\src\Experlogix.Api.DesignStudio\Middleware\DatabaseConnectionMiddleware .cs:第65行
在 Microsoft.AspNetCore.Authorization.AuthorizationMiddleware.Invoke(HttpContext context)
在 Serilog.AspNetCore.RequestLoggingMiddleware.Invoke(HttpContext httpContext)
在 Swashbuckle.AspNetCore.SwaggerUI.SwaggerUIMiddleware.Invoke(HttpContext httpContext)
在 Swashbuckle.AspNetCore.Swagger.SwaggerMiddleware.Invoke(HttpContext httpContext,ISwaggerProvider swaggerProvider)
在 Experlogix.Core.AspNet.Middleware.ApiExceptionHandlerMiddleware.Invoke(HttpContext context)
任何有关如何实现与此类似的事情的指导将不胜感激!
环境详情
解决方法
通过像这样更改 ToPropertyExpression,我能够使查询按需要工作:
public static Expression ToPropertyExpression(this SortDescriptor sort,Expression entity)
{
return sort.Selector.Split('.').Aggregate(entity,Expression.Property);
}
所有其他代码保持不变,并生成以下 SQL 查询:
SELECT
[f].[Id],ROW_NUMBER() OVER(ORDER BY [a_Category].[EntityId],[f].[StackRank],[f].[Id])
FROM
[ds].[CategoryProperty] [f]
INNER JOIN [ds].[Category] [a_Category] ON [f].[CategoryId] = [a_Category].[Id]
,
我提出了更通用的分页解决方案。它可以处理任何查询并构建正确的 SQL。它还可以通过单次往返数据库返回 TotalCount。
实现并非微不足道,但可以用作如何使用表达式树的参考源。
使用示例:
var pageSize = 20;
var query = table.Where(x => x.Id % 2 == 0).OrderBy(x => x.Id).ThenByDescending(x => x.Value);
var pagination1 = query.Paginate(1,pageSize);
var pagination2 = query.Paginate(2,pageSize,true); // with total count
// extensions which accepts predicate to find page. Query must be ordered.
var byKey = query.GetPageByCondition(pageSize,x => x.Id == someId);
// returns page number. Query must be ordered.
var pageNumber = query.GetPageNumberByCondition(pageSize,x => x.Id == someId);
// dynamic ordering
var query = table.Where(x => x.Id % 2 == 0)
.ApplyOrderBy(new []{Tuple.Create("Id",false),Tuple.Create("Value",true)});
实施:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;
using System.Threading;
using System.Threading.Tasks;
using LinqToDB;
using LinqToDB.Async;
using LinqToDB.Expressions;
namespace Tests.Playground
{
public static class PaginationExtensions
{
public class PaginationResult<T>
{
public PaginationResult(int totalCount,int page,int pageSize,List<T> items)
{
TotalCount = totalCount;
Page = page;
PageSize = pageSize;
Items = items;
}
public int TotalCount { get; }
public List<T> Items { get; }
public int Page { get; }
public int PageSize { get; }
}
public static PaginationResult<T> Paginate<T>(this IQueryable<T> query,bool includeTotalCount = false)
{
return ProcessPaginationResult(EnvelopeQuery(query,page,includeTotalCount),pageSize);
}
public static Task<PaginationResult<T>> PaginateAsync<T>(this IQueryable<T> query,bool includeTotalCount = false,CancellationToken cancellationToken = default)
{
return ProcessPaginationResultAsync(EnvelopeQuery(query,cancellationToken);
}
public static Expression ApplyOrderBy(Type entityType,Expression queryExpr,IEnumerable<Tuple<string,bool>> order)
{
var param = Expression.Parameter(entityType,"e");
var isFirst = true;
foreach (var tuple in order)
{
var lambda = Expression.Lambda(MakePropPath(param,tuple.Item1),param);
var methodName =
isFirst ? tuple.Item2 ? nameof(Queryable.OrderByDescending) : nameof(Queryable.OrderBy)
: tuple.Item2 ? nameof(Queryable.ThenByDescending) : nameof(Queryable.ThenBy);
queryExpr = Expression.Call(typeof(Queryable),methodName,new[] { entityType,lambda.Body.Type },queryExpr,lambda);
isFirst = false;
}
return queryExpr;
}
public static PaginationResult<T> GetPageByCondition<T>(this IQueryable<T> query,Expression<Func<T,bool>> predicate,bool includeTotal = false)
{
return ProcessPaginationResult(GetPageByConditionInternal(query,predicate,includeTotal),pageSize);
}
public static int GetPageNumberByCondition<T>(this IQueryable<T> query,bool includeTotal = false)
{
return GetPageNumberByConditionInternal(query,includeTotal).FirstOrDefault();
}
public static Task<int> GetPageNumberByConditionAsync<T>(this IQueryable<T> query,bool includeTotal = false,CancellationToken cancellationToken = default)
{
return GetPageNumberByConditionInternal(query,includeTotal).FirstOrDefaultAsync(cancellationToken);
}
public static Task<PaginationResult<T>> GetPageByConditionAsync<T>(this IQueryable<T> query,CancellationToken cancellationToken = default)
{
return ProcessPaginationResultAsync(GetPageByConditionInternal(query,cancellationToken);
}
public static IQueryable<T> ApplyOrderBy<T>(this IQueryable<T> query,bool>> order)
{
var expr = ApplyOrderBy(typeof(T),query.Expression,order);
return query.Provider.CreateQuery<T>(expr);
}
#region Helpers
static Expression? Unwrap(Expression? ex)
{
if (ex == null)
return null;
switch (ex.NodeType)
{
case ExpressionType.Quote:
case ExpressionType.ConvertChecked:
case ExpressionType.Convert:
return ((UnaryExpression)ex).Operand.Unwrap();
}
return ex;
}
static MethodInfo? FindMethodInfoInType(Type type,string methodName,int paramCount)
{
var method = type.GetRuntimeMethods()
.FirstOrDefault(m => m.Name == methodName && m.GetParameters().Length == paramCount);
return method;
}
static MethodInfo FindMethodInfo(Type type,int paramCount)
{
var method = FindMethodInfoInType(type,paramCount);
if (method != null)
return method;
method = type.GetInterfaces().Select(it => FindMethodInfoInType(it,paramCount))
.FirstOrDefault(m => m != null);
if (method == null)
throw new Exception($"Method '{methodName}' not found in type '{type.Name}'.");
return method;
}
static Expression ExtractOrderByPart(Expression query,List<Tuple<Expression,bool>> orderBy)
{
var current = query;
while (current.NodeType == ExpressionType.Call)
{
var mc = (MethodCallExpression)current;
if (typeof(Queryable) == mc.Method.DeclaringType)
{
var supported = true;
switch (mc.Method.Name)
{
case "OrderBy":
case "ThenBy":
{
orderBy.Add(Tuple.Create(mc.Arguments[1],false));
break;
}
case "OrderByDescending":
case "ThenByDescending":
{
orderBy.Add(Tuple.Create(mc.Arguments[1],true));
break;
}
default:
supported = false;
break;
}
if (!supported)
break;
current = mc.Arguments[0];
}
else
break;
}
return current;
}
static Expression FinalizeFunction(Expression functionBody)
{
var toValueMethodInfo = FindMethodInfo(functionBody.Type,"ToValue",0);
functionBody = Expression.Call(functionBody,toValueMethodInfo);
return functionBody;
}
static Expression GenerateOrderBy(Expression entity,Expression functionBody,bool>> orderBy)
{
var isFirst = true;
for (int i = orderBy.Count - 1; i >= 0; i--)
{
var order = orderBy[i];
string methodName;
if (order.Item2)
methodName = isFirst ? "OrderByDesc" : "ThenByDesc";
else
methodName = isFirst ? "OrderBy" : "ThenBy";
isFirst = false;
var currentType = functionBody.Type;
var methodInfo = FindMethodInfo(currentType,1).GetGenericMethodDefinition();
var arg = ((LambdaExpression)Unwrap(order.Item1)!).GetBody(entity);
functionBody = Expression.Call(functionBody,methodInfo.MakeGenericMethod(arg.Type),arg);
}
return functionBody;
}
static Expression GeneratePartitionBy(Expression functionBody,Expression[] partitionBy)
{
if (partitionBy.Length == 0)
return functionBody;
var method = FindMethodInfo(functionBody.Type,"PartitionBy",1);
var partitionsExpr = Expression.NewArrayInit(typeof(object),partitionBy);
var call = Expression.Call(functionBody,method,partitionsExpr);
return call;
}
static Expression MakePropPath(Expression objExpression,string path)
{
return path.Split('.').Aggregate(objExpression,Expression.PropertyOrField);
}
private class Envelope<T>
{
public int TotalCount { get; set; }
public T Data { get; set; } = default!;
public int Page { get; set; }
}
static IQueryable<Envelope<T>> EnvelopeQuery<T>(IQueryable<T> query,bool includeTotalCount)
{
var withCount = includeTotalCount
? query.Select(q =>
new Envelope<T> {TotalCount = Sql.Ext.Count().Over().ToValue(),Page = page,Data = q})
: query.Select(q => new Envelope<T> {TotalCount = -1,Data = q});
return withCount.Skip((page - 1) * pageSize).Take(pageSize);
}
static PaginationResult<T> ProcessPaginationResult<T>(IQueryable<Envelope<T>> query,int pageSize)
{
int totalRecords;
int page = 0;
using (var enumerator = query.GetEnumerator())
{
List<T> result;
if (!enumerator.MoveNext())
{
totalRecords = 0;
result = new List<T>();
}
else
{
totalRecords = enumerator.Current.TotalCount;
page = enumerator.Current.Page;
result = new List<T>(pageSize);
do
{
result.Add(enumerator.Current.Data);
} while (enumerator.MoveNext());
}
return new PaginationResult<T>(totalRecords,result);
}
}
static async Task<PaginationResult<T>> ProcessPaginationResultAsync<T>(IQueryable<Envelope<T>> query,CancellationToken cancellationToken)
{
var items = query.AsAsyncEnumerable();
int totalRecords;
int page = 0;
await using (var enumerator = items.GetAsyncEnumerator(cancellationToken))
{
List<T> result;
if (!await enumerator.MoveNextAsync())
{
totalRecords = 0;
result = new List<T>();
}
else
{
totalRecords = enumerator.Current.TotalCount;
page = enumerator.Current.Page;
result = new List<T>(pageSize);
do
{
result.Add(enumerator.Current.Data);
} while (await enumerator.MoveNextAsync());
}
return new PaginationResult<T>(totalRecords,result);
}
}
class RownNumberHolder<T>
{
public T Data = default!;
public long RowNumber;
public int TotalCount;
}
static Expression<Func<int>> _totalCountTemplate = () => Sql.Ext.Count().Over().ToValue();
static Expression _totalCountEmpty = Expression.Constant(-1);
static Expression GetRowNumberQuery<T>(Expression queryWithoutOrder,bool>> orderBy,bool includeTotal)
{
if (orderBy.Count == 0)
throw new InvalidOperationException("OrderBy for query is not specified");
Expression<Func<T,AnalyticFunctions.IOverMayHavePartitionAndOrder<long>>> overExpression =
t => Sql.Ext.RowNumber().Over();
Expression<Func<IQueryable<T>,long,int,IQueryable<RownNumberHolder<T>>>> selectExpression =
(q,rn,tc) => q.Select(x => new RownNumberHolder<T> {Data = x,RowNumber = rn,TotalCount = tc});
Expression totalCountExpr = includeTotal ? _totalCountTemplate.Body : _totalCountEmpty;
var entityParam = ((LambdaExpression)((MethodCallExpression)selectExpression.Body).Arguments[1].Unwrap())
.Parameters[0];
var windowFunctionBody = overExpression.Body;
windowFunctionBody = GenerateOrderBy(entityParam,windowFunctionBody,orderBy);
windowFunctionBody = FinalizeFunction(windowFunctionBody);
var queryExpr = selectExpression.GetBody(queryWithoutOrder,totalCountExpr);
return queryExpr;
}
static IQueryable<Envelope<T>> GetPageByConditionInternal<T>(IQueryable<T> query,bool includeTotal)
{
Expression<Func<IQueryable<RownNumberHolder<T>>,IQueryable<RownNumberHolder<T>>>> cteCall = q => q.AsCte("pagination_cte");
var queryExpr = query.Expression;
var orderBy = new List<Tuple<Expression,bool>>();
var withoutOrder = ExtractOrderByPart(queryExpr,orderBy);
var rnQueryExpr = GetRowNumberQuery<T>(withoutOrder,orderBy,includeTotal);
rnQueryExpr = cteCall.GetBody(rnQueryExpr);
Expression<Func<IQueryable<RownNumberHolder<T>>,Expression<Func<RownNumberHolder<T>,bool>>,IQueryable<Envelope<T>>>> dataTemplate =
includeTotal
? (q,f,ps) =>
q
.Where(f).Take(1).Select(x => (int)(x.RowNumber - 1) / ps + 1)
.SelectMany(page => q.Where(x => x.RowNumber.Between((page - 1) * ps + 1,page * ps))
.OrderBy(x => x.RowNumber)
.Select(x =>
new Envelope<T>
{
Data = x.Data,TotalCount = (int)x.TotalCount
}))
: (q,page * ps))
.OrderBy(x => x.RowNumber)
.Select(x =>
new Envelope<T> {Data = x.Data,TotalCount = -1}));
var param = Expression.Parameter(typeof(RownNumberHolder<T>),"h");
var newPredicate = Expression.Lambda(predicate.GetBody(Expression.PropertyOrField(param,"Data")),param);
var resultExpr = dataTemplate.GetBody(rnQueryExpr,newPredicate,Expression.Constant(pageSize));
return query.Provider.CreateQuery<Envelope<T>>(resultExpr);
}
static IQueryable<int> GetPageNumberByConditionInternal<T>(IQueryable<T> query,bool includeTotal)
{
var queryExpr = query.Expression;
var orderBy = new List<Tuple<Expression,includeTotal);
Expression<Func<IQueryable<RownNumberHolder<T>>,IQueryable<int>>> dataTemplate =
(q,ps) =>
q.AsSubQuery().Where(f).Select(x => (int)((x.RowNumber - 1) / ps + 1));
var param = Expression.Parameter(typeof(RownNumberHolder<T>),Expression.Constant(pageSize));
return query.Provider.CreateQuery<int>(resultExpr);
}
#endregion
}
}