Linq2DB (SQL Server):动态链接 Sql.Property<T> 调用

我使用此 Pull Request 中的示例作为嵌套属性链的参考。不幸的是像

Sql.Property<object>(x,"Someclass.SomeProperty") 

不起作用。

假设我有一个模型 SortDescriptor(定义如下),它定义了我们如何动态地对代码中的项目进行排序。有没有办法动态创建表达式树来生成嵌套的 Sql.Property 调用,就像下面显示的 ToPropertyExpression 方法一样?

如果给定以下过滤器,则应从 SortBy 函数输出以下 SQL:

ORDER BY Category.EntityId,StackRank,Id

CategoryCategoryProperty 上的导航属性)

        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](IQueryable1 源,CancellationToken 取消令牌)
microsoft.EntityFrameworkCore.EntityFrameworkQueryableExtensions.ToListAsync[tsource](IQueryable1 source,CancellationToken cancellationToken) at LinqToDB.AsyncExtensions.ToListAsync[tsource](IQueryable1 源,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,Valuetask1 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)

任何有关如何实现与此类似的事情的指导将不胜感激!

环境详情

  • linq2db 版本:linq2db.EntityFrameworkCore 5.1.0
  • 数据库服务器:SQL Server
  • 操作系统:Windows 10
  • .NET 框架:.Net 5.0
nidi_3 回答:Linq2DB (SQL Server):动态链接 Sql.Property<T> 调用

通过像这样更改 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
    }
}
本文链接:https://www.f2er.com/1082966.html

大家都在问