mrahhal / MR.EntityFrameworkCore.KeysetPagination

Keyset/Seek/Cursor pagination for Entity Framework Core.
MIT License
218 stars 11 forks source link

Custom value objects #46

Closed IRlyDontKnow closed 5 months ago

IRlyDontKnow commented 1 year ago

Hello, i'm trying to use this library with custom value objects but it seems like it does not work as expected.

For example:

public class UserId
{
    public UserId(string value) => Value = value;
    public string Value { get; set; }
}

public class User
{
    public UserId Id { get; set; }
    public string Email { get; set; }
    public DateTime RegisteredAt { get; set; }
}

public class UserEntityTypeConfiguration : IEntityTypeConfiguration<User>
{
    public void Configure(EntityTypeBuilder<User> builder)
    {
        builder.HasKey(x => x.Id);
        builder.Property(x => x.Id)
            .ValueGeneratedNever()
            .HasConversion(i => i.Value, i => new UserId(i));
        builder.Property(x => x.Email);
        builder.Property(x => x.RegisteredAt);
    }
}

var keysetBuilderAction = (KeysetPaginationBuilder<User> b) =>
{
    b.Ascending(x => x.CreatedAt).Ascending(x => x.Id);
};

var queryable = context.Users.AsQueryable();

var reference = await context.Users.FirstOrDefaultAsync(x => x.Id == new UserId(query.After));
var keysetContext = queryable.KeysetPaginate(keysetBuilderAction, KeysetPaginationDirection.Backward, reference);

It throws an exception: The binary operator GreaterThan is not defined for the types 'App.ValueObjects.UserId'

Is there anyway to use UserId.Value for comparison?

mrahhal commented 1 year ago

Hello. With the way you're defining the keyset above, it would be the same as trying to write .Where(x => x.Id > ...) yourself, which of course is not defined for your custom UserId type. So it makes sense that this is not supported.

However, you can try just adding the .Value in the keyset definition itself, just as you would have written .Where(x => x.Id.Value > ...):

b.Ascending(x => x.CreatedAt).Ascending(x => x.Id.Value);
IRlyDontKnow commented 1 year ago

I've tried this one before also. It throws different error:

The LINQ expression 'DbSet<User>()
    .OrderByDescending(p => p.RegisteredAt)
    .ThenByDescending(p => p.Id.Value)' could not be translated. Either rewrite the query in a form that can be translated, or switch to client evaluation explicitly by inserting a call to 'AsEnumerable', 'AsAsyncEnumerable', 'ToList', or 'ToListAsync'. See https://go.microsoft.com/fwlink/?linkid=2101038 for more information.
mrahhal commented 1 year ago

The generated expression looks fine. This is EFCore itself having trouble translating this. I don't usually use custom types for ids like this so I can't tell you why. This problem is not from this library, you'll have to figure out why EFCore is failing to translate Id.Value.

To confirm, try writing a normal offset query yourself using this same ordering (so without using this library), and see if that gives you the same error or not.

IRlyDontKnow commented 1 year ago

So, i've written two queries without using this library and here are the results.

This one results in the LINQ expression exception. The same one as in my previous comment.

var x = await dbContext.Users
        .OrderByDescending(p => p.RegisteredAt)
        .ThenByDescending(p => p.Id.Value)
        .ToListAsync();

This one works as expected and properly applies sorting.

var x = await dbContext.Users
        .OrderByDescending(p => p.RegisteredAt)
        .ThenByDescending(p => p.Id)
        .ToListAsync();

So it seems like EFCore properly utilizes value converters and converts value object into string.

mrahhal commented 1 year ago

I see. The thing is, a keyset query will include both an ordering expression as above, but will also include generated conditional expressions (mainly comparison and equality), like p.Id.Value > "the reference value". Even if I allowed the ordering to work, this second conditional expression will give you a compilation error if you write it by hand (as again, this library only generates these which you could have written by hand).

First step here is to find a way to make the hand written manual expression work in your case (in particular, the comparisons on the prop), and then I could think if it can be supported in this library. It might for example invovle implementing the comparison and equality operators on your custom type. Even if you do this though, I can't promise I can support the use case in this library, will have to see how that will look like.

mrahhal commented 5 months ago

I'm closing this for now as I can't think of a way this can work in a keyset query as mentioned above. If you come up with a hand written keyset query that works with your case feel free to open this issue again and I'll have another look.