-
-
Notifications
You must be signed in to change notification settings - Fork 12
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Make KeysetColumn<T>
part of public API.
#58
Comments
I want to avoid exposing A workaround is to use reflection to access the internal Here is a reflection function I've put together quickly: private static List<(bool IsDecreasing, LambdaExpression LambdaExpression)> GetColumnInternals<T>(KeysetPaginationContext<T> context)
{
var list = new List<(bool, LambdaExpression)>();
var columnsProp = context.GetType()
.GetProperty("Columns", BindingFlags.Instance | BindingFlags.NonPublic)!;
var columnsObject = columnsProp.GetValue(context) as System.Collections.ICollection;
foreach (var columnObject in columnsObject)
{
var props = columnObject.GetType().GetProperties(BindingFlags.Instance | BindingFlags.Public);
var isDecreasing = (bool)props.Single(x => x.Name == "IsDescending").GetValue(columnObject);
var lambdaExpression = (LambdaExpression)props.First(x => x.Name == "LambdaExpression").GetValue(columnObject);
list.Add((isDecreasing, lambdaExpression));
}
return list;
} |
Reflection is not really a good solution, although I appreciate the suggestion. It is unfortunate, now I need to abstract it away behind my own intermediate builder + value type which is just unnecessary bloat.
Do you at least think you'd be exposing this in the future in some way? |
I'm aware that direction and the lambda expression are the properties that are needed, but when I say refactoring I'm talking in particular about these properties. Could you show me an example code and its unit test that you would like to write here? It feels a bit needless to me to have unit tests for the direction/column, since you hard code that in the request. I'm trying to understand your needs for this. Is it because you have dynamic direction/columns depending on request args, and you want to ensure a request arg is translating to the right direction/column?
The next major version can expose this since I'll be done with a few breaking changes then. Unfortunately I can't tell you when that will happen. |
Exactly this. I have MediatR requests where ordering could either be configured via a protected member, or in other use cases a caller (eg. an API consumer) could specify ordering. So for example when I want to unit test an API controller, I don't actually reach the handler implementation. In this specific case, all I need is to assert that request sent with expected (keyset) arguments.
This is based on the assumption that we actually execute the query against say an in-mem DB or real DB and verify the results are in expected ordering? Additionally, I actually have a generic implementation where request and query are decoupled, see below. Just for sake of example, just whipped up.
Hopefully this makes it a bit more clear. using System.Linq.Expressions;
using System.Reflection;
using System.Runtime.CompilerServices;
using MediatR;
using Microsoft.EntityFrameworkCore;
using MR.EntityFrameworkCore.KeysetPagination;
using NSubstitute;
using Xunit;
namespace KeysetExample;
public class MyService
{
private readonly ISender _sender;
public MyService(ISender sender)
{
_sender = sender;
}
public async Task ProcessAllAsync()
{
IAsyncEnumerable<MyEntity> items = _sender.CreateStream(new BulkGet<MyEntity>
{
Keyset = b => b
.Ascending(e => e.Name)
.Ascending(e => e.Id)
});
await foreach (MyEntity item in items)
{
// Do something.
}
}
}
public record MyEntity
{
public int Id { get; init; }
public string Name { get; init; }
}
public record BulkGet<T> : IStreamRequest<T>
{
public Action<KeysetPaginationBuilder<T>> Keyset { get; init; }
}
public class BulkGetHandler<T> : IStreamRequestHandler<BulkGet<T>, T>
where T : class
{
private readonly IDbContextFactory<DbContext> _dbContextFactory;
public BulkGetHandler(IDbContextFactory<DbContext> dbContextFactory)
{
_dbContextFactory = dbContextFactory;
}
public async IAsyncEnumerable<T> Handle(BulkGet<T> request, [EnumeratorCancellation] CancellationToken cancellationToken)
{
await using DbContext dbContext = await _dbContextFactory.CreateDbContextAsync(cancellationToken);
object? reference = null;
do
{
IQueryable<T> q = dbContext.Set<T>()
.AsNoTracking()
.KeysetPaginateQuery(request.Keyset, reference: reference)
.Take(100);
reference = null;
await foreach (T? item in q.AsAsyncEnumerable().WithCancellation(cancellationToken))
{
yield return item;
reference = item;
}
} while (reference is not null);
}
}
public class MyServiceTests
{
private readonly ISender _senderMock;
private readonly MyService _sut;
public MyServiceTests()
{
_senderMock = Substitute.For<ISender>();
_sut = new MyService(_senderMock);
}
[Fact]
public async Task Test_Should_SendQuery()
{
MyEntity[] data =
[
new()
{
Name = "A",
Id = 1
},
new()
{
Name = "A",
Id = 2
}
];
_senderMock
.CreateStream(Arg.Any<BulkGet<MyEntity>>(), Arg.Any<CancellationToken>())
.Returns(data.ToAsyncEnumerable());
// Act
await _sut.ProcessAllAsync();
// Assert
Func<BulkGet<MyEntity>, bool> assertRequest = req =>
{
var b = new KeysetPaginationBuilder<MyEntity>();
req.Keyset.Invoke(b);
// Here I would like to do something like:
KeysetColumn<MyEntity> col0 = b.Columns[0];
PropertyInfo p0 = MemberVisitor.GetProperty(col0.LambdaExpression);
return p0.Name == nameof(MyEntity.Name)
&& p0.DeclaringType == typeof(MyEntity)
&& !col0.IsDescending;
};
await _senderMock
.Received(1)
.CreateStream(Arg.Is<BulkGet<MyEntity>>(req => assertRequest(req)), Arg.Any<CancellationToken>())
.ToListAsync();
}
}
internal class MemberVisitor : ExpressionVisitor
{
private PropertyInfo? _property;
private MemberVisitor()
{
}
public static PropertyInfo GetProperty(LambdaExpression expr)
{
var visitor = new MemberVisitor();
visitor.Visit(expr);
return visitor._property ?? throw new InvalidOperationException("Not a property.");
}
protected override Expression VisitMember(MemberExpression node)
{
if (node.Member is PropertyInfo pi)
{
_property = pi;
}
return base.VisitMember(node);
}
} |
I would like to be able to assert in unit tests which columns (eg. the expression(s) and its sort direction(s)) are configured on the
KeysetPaginationBuilder<T>
. But currently the builder itsColumns
property is internal everywhere (also on theKeysetPaginationContext
, as is theKeysetColumn<T>
type.Can we selectively make some of this public? I'd understand you want to avoid exposing too much to keep the API surface small, but tests require too much setup because I have to run the complete query logic, and am effectively writing integration tests. For small unit tests, I do not care and just want to verify that the columns used is correct. I do not really see any downsides of exposing the column expression and direction?
I could also extract it with an expression visitor but this is way too cumbersome and I'd still have to build the entire query (which in my use case is not relevant). Or do you have another recommendation?
The text was updated successfully, but these errors were encountered: