First of all, I would like to thank you for this very useful package that abstracts away the complexity of Keyset Pagination.
So to the issue I seem to have encountered, I have a requirement to use a dynamic sort column in keyset.
in cases where the sort column is a reference type like string everything works Ok, but when the sort column is a value type like int, DateTime, or Guid, it fails with either of the following exceptions depending on the combination used.
System.InvalidOperationException : The binary operator LessThan is not defined for the types 'System.Object' and 'System.Object'.
or
System.InvalidOperationException : The binary operator GreaterThan is not defined for the types 'System.Object' and 'System.Object'.
a simple reproduction can be done by adding the code below to KeysetPaginationTest.cs and running the newly added test methods (Ascending_HasPreviousAsync_Buggy, Descending_HasPreviousAsync_Buggy, Ascending_HasNextAsync_Buggy, Descending_HasNextAsync_Buggy)
[Theory]
[InlineData("Id")]
[InlineData("String")]
[InlineData("Guid")]
[InlineData("IsDone")]
[InlineData("Created")]
[InlineData("CreatedNullable")]
public async Task Ascending_HasPreviousAsync_Buggy(string sortColumn)
{
var keysetContext = DbContext.MainModels.KeysetPaginate(
b => b.Ascending(GetSortColumn<MainModel>(sortColumn)));
var items = await keysetContext.Query
.Take(20)
.ToListAsync();
keysetContext.EnsureCorrectOrder(items);
var dtos = items.Select(x => new
{
x.Id, x.String, x.Guid, x.IsDone, x.Created, x.CreatedNullable
}).ToList();
// exception is thrown when this line executes
await keysetContext.HasPreviousAsync(dtos);
}
[Theory]
[InlineData("Id")]
[InlineData("String")]
[InlineData("Guid")]
[InlineData("IsDone")]
[InlineData("Created")]
[InlineData("CreatedNullable")]
public async Task Descending_HasPreviousAsync_Buggy(string sortColumn)
{
var keysetContext = DbContext.MainModels.KeysetPaginate(
b => b.Descending(GetSortColumn<MainModel>(sortColumn)));
var items = await keysetContext.Query
.Take(20)
.ToListAsync();
keysetContext.EnsureCorrectOrder(items);
var dtos = items.Select(x => new
{
x.Id, x.String, x.Guid, x.IsDone, x.Created, x.CreatedNullable
}).ToList();
// exception is thrown when this line executes
await keysetContext.HasPreviousAsync(dtos);
}
[Theory]
[InlineData("Id")]
[InlineData("String")]
[InlineData("Guid")]
[InlineData("IsDone")]
[InlineData("Created")]
[InlineData("CreatedNullable")]
public async Task Ascending_HasNextAsync_Buggy(string sortColumn)
{
var keysetContext = DbContext.MainModels.KeysetPaginate(
b => b.Ascending(GetSortColumn<MainModel>(sortColumn)));
var items = await keysetContext.Query
.Take(20)
.ToListAsync();
keysetContext.EnsureCorrectOrder(items);
var dtos = items.Select(x => new
{
x.Id, x.String, x.Guid, x.IsDone, x.Created, x.CreatedNullable
}).ToList();
// exception is thrown when this line executes
await keysetContext.HasNextAsync(dtos);
}
[Theory]
[InlineData("Id")]
[InlineData("String")]
[InlineData("Guid")]
[InlineData("IsDone")]
[InlineData("Created")]
[InlineData("CreatedNullable")]
public async Task Descending_HasNextAsync_Buggy(string sortColumn)
{
var keysetContext = DbContext.MainModels.KeysetPaginate(
b => b.Descending(GetSortColumn<MainModel>(sortColumn)));
var items = await keysetContext.Query
.Take(20)
.ToListAsync();
keysetContext.EnsureCorrectOrder(items);
var dtos = items.Select(x => new
{
x.Id, x.String, x.Guid, x.IsDone, x.Created, x.CreatedNullable
}).ToList();
// exception is thrown when this line executes
await keysetContext.HasNextAsync(dtos);
}
private static Expression<Func<TEntity, object>> GetSortColumn<TEntity>(string sortColumn) where TEntity: MainModel
{
return sortColumn switch
{
_ when string.Equals(sortColumn, "Id", StringComparison.OrdinalIgnoreCase) => x => x.Id,
_ when string.Equals(sortColumn, "String", StringComparison.OrdinalIgnoreCase) => x => x.String,
_ when string.Equals(sortColumn, "Guid", StringComparison.OrdinalIgnoreCase) => x => x.Guid,
_ when string.Equals(sortColumn, "IsDone", StringComparison.OrdinalIgnoreCase) => x => x.IsDone,
_ when string.Equals(sortColumn, "Created", StringComparison.OrdinalIgnoreCase) => x => x.Created,
_ when string.Equals(sortColumn, "CreatedNullable", StringComparison.OrdinalIgnoreCase) => x => x.CreatedNullable,
_ =>
throw new NotImplementedException($ "Unsupported {nameof(sortColumn)} {sortColumn}")
};
}
from my little investigation, it seems that in order to coerce an expression returning a value type into Func<TEntity,object> the compiler needs to insert a Convert(expr, typeof(object)), a UnaryExpression.
For strings and other reference types, there is no need to box, so a "straight" member expression is returned.
First of all, I would like to thank you for this very useful package that abstracts away the complexity of Keyset Pagination.
So to the issue I seem to have encountered, I have a requirement to use a dynamic sort column in keyset.
in cases where the sort column is a
referencetype likestringeverything works Ok, but when the sort column is avaluetype likeint,DateTime, orGuid, it fails with either of the following exceptions depending on the combination used.System.InvalidOperationException : The binary operator LessThan is not defined for the types 'System.Object' and 'System.Object'.or
System.InvalidOperationException : The binary operator GreaterThan is not defined for the types 'System.Object' and 'System.Object'.a simple reproduction can be done by adding the code below to KeysetPaginationTest.cs and running the newly added test methods (
Ascending_HasPreviousAsync_Buggy,Descending_HasPreviousAsync_Buggy,Ascending_HasNextAsync_Buggy,Descending_HasNextAsync_Buggy)from my little investigation, it seems that in order to coerce an expression returning a value type into
Func<TEntity,object>the compiler needs to insert aConvert(expr, typeof(object)), aUnaryExpression.For strings and other reference types, there is no need to box, so a "straight" member expression is returned.