Skip to content

Issue with Dynamic Sort column In KeySet for Value Type #49

@Xor-el

Description

@Xor-el

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions