I am using the examples from this Pull Request as a reference for nested property chains. Unfortunately something like
Sql.Property<object>(x, "SomeClass.SomeProperty")
doesn't work.
Say I have a model, SortDescriptor
(defined below), which defines how we sort items in our code dynamically. Is there a way to dynamically create an expression tree to generate nested Sql.Property
calls, like in the ToPropertyExpression
method shown below?
The following SQL should be output from the SortBy
function if given the following filter:
ORDER BY Category.EntityId, StackRank, Id
(Category
is a navigation property on CategoryProperty
)
var filter1 = new PagedFilter
{
Take = 25,
Sort = new List<SortDescriptor>
{
new SortDescriptor { Selector = "Category.EntityId" },
new SortDescriptor { Selector = "StackRank" },
new SortDescriptor { Selector = "Id" },
}
};
Below are the functions and objects I am using:
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; }
}
Whenever I try to execute something similar to what's above, I get this error message saying that it's trying to evaluate Sql.Property
expression chain on the client side:
Exception message:
LinqToDB.Linq.LinqException: 'Property' is only server-side method.Stack trace:
at LinqToDB.Sql.Property[T](Object entity, String propertyName)
at LinqToDB.Linq.QueryRunner.SetParameters(Query query, Expression expression, IDataContext parametersContext, Object[] parameters, Int32 queryNumber, SqlParameterValues parameterValues)
at LinqToDB.Linq.QueryRunnerBase.SetCommand(Boolean clearQueryHints)
at LinqToDB.Data.DataConnection.QueryRunner.<>n__0(Boolean clearQueryHints)
at LinqToDB.Data.DataConnection.QueryRunner.ExecuteReaderAsync(CancellationToken cancellationToken)
at LinqToDB.Linq.QueryRunner.AsyncEnumeratorImpl1.MoveNextAsync() at Microsoft.EntityFrameworkCore.EntityFrameworkQueryableExtensions.ToListAsync[TSource](IQueryable
1 source, CancellationToken cancellationToken)
at Microsoft.EntityFrameworkCore.EntityFrameworkQueryableExtensions.ToListAsync[TSource](IQueryable1 source, CancellationToken cancellationToken) at LinqToDB.AsyncExtensions.ToListAsync[TSource](IQueryable
1 source, CancellationToken token)
at Experlogix.Api.DesignStudio.DataAccess.CategoryPropertyRepository.GetPageIndexById(CategoryPropertyFilter filter, Guid id) in D:\Dev\repos\Experlogix.Api.DesignStudio\src\Experlogix.Api.DesignStudio.DataAccess\CategoryPropertyRepository.cs:line 116
at 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)
at Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.g__Awaited|10_0(ControllerActionInvoker invoker, Task lastTask, State next, Scope scope, Object state, Boolean isCompleted)
at Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.Rethrow(ActionExecutedContextSealed context)
at Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.Next(State& next, Scope& scope, Object& state, Boolean& isCompleted)
at Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.g__Awaited|13_0(ControllerActionInvoker invoker, Task lastTask, State next, Scope scope, Object state, Boolean isCompleted)
at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.g__Awaited|19_0(ResourceInvoker invoker, Task lastTask, State next, Scope scope, Object state, Boolean isCompleted)
at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.g__Awaited|17_0(ResourceInvoker invoker, Task task, IDisposable scope)
at Microsoft.AspNetCore.Routing.EndpointMiddleware.g__AwaitRequestTask|6_0(Endpoint endpoint, Task requestTask, ILogger logger)
at Experlogix.Api.DesignStudio.Middleware.DatabaseConnectionMiddleware.Invoke(HttpContext httpContext, IUserContext userContext, IConnectionContext connectionContext, IDatabaseClient databaseClient) in D:\Dev\repos\Experlogix.Api.DesignStudio\src\Experlogix.Api.DesignStudio\Middleware\DatabaseConnectionMiddleware.cs:line 65
at Microsoft.AspNetCore.Authorization.AuthorizationMiddleware.Invoke(HttpContext context)
at Serilog.AspNetCore.RequestLoggingMiddleware.Invoke(HttpContext httpContext)
at Swashbuckle.AspNetCore.SwaggerUI.SwaggerUIMiddleware.Invoke(HttpContext httpContext)
at Swashbuckle.AspNetCore.Swagger.SwaggerMiddleware.Invoke(HttpContext httpContext, ISwaggerProvider swaggerProvider)
at Experlogix.Core.AspNet.Middleware.ApiExceptionHandlerMiddleware.Invoke(HttpContext context)
Any guidance on how something similar to this can be achieved would be greatly appreciated!
Environment details
- linq2db version: linq2db.EntityFrameworkCore 5.1.0
- Database server: SQL Server
- Operating system: Windows 10
- .NET Framework: .Net 5.0