-
-
Notifications
You must be signed in to change notification settings - Fork 13
Description
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.