Feature Request: Query Filter with DbContext as one of the arguments
I would like to propose a addition to the Query Filter capability of EntityFramework-Plus library.
This post is building upon examples in the docs: https://entityframework-plus.net/ef-core-query-filter
Let's take the following example:
// using Z.EntityFramework.Plus; // Don't forget to include this.
var ctx = new EntitiesContext();
ctx.Filter<Post>(q => q.Where(x => !x.IsSoftDeleted));
// SELECT * FROM Post WHERE IsSoftDeleted = false
var list = ctx.Posts.ToList();
If we would assume that the EntitiesContext has a public bool ExcludeDeleted { get; set; } then we would be able to set it and the following example could be possible:
// using Z.EntityFramework.Plus; // Don't forget to include this.
var ctx = new EntitiesContext();
ctx.ExcludeDeleted = true;
ctx.Filter<Post>((q, dbCtx) => q.Where(x => !dbCtx.ExcludeDeleted || !x.IsSoftDeleted));
// SELECT * FROM Post WHERE IsSoftDeleted = false
var list = ctx.Posts.ToList();
A more valid example would be based on multi-tenant scenario, where the DbContext is created or hydrated with TenantId while supplied by the dependency injection before any queries would run. But for now i wanted to keep this example be based on existing documentation as much as possible.
Linq2Db supports filtering that optionally can take in DbContext along with the IQueryable: https://github.com/linq2db/linq2db/blob/f60cef6849291a4ff810adcb21175d2e2ec19f8c/Tests/Linq/Linq/QueryFilterTests.cs#L69
Thank you for your suggestion @Seramis ,
I totally understand the purpose of this request
I will look to see if that could be possible with the current code we have
Best Regards,
Jon
Hello @Seramis ,
Adding this feature by allowing 2 parameters (q, dbCtx) might be too hard for us. After looking at the code, this is not an easy implementation and requires us to change too much code.
However, you should already be able to access the context and have this behavior:
using (var context = new EntityContext())
{
context.Filter<Post>(q => q.Where(x => !x.IsSoftDeleted || context.DisableMyFilter));
context.DisableMyFilter = false;
FiddleHelper.WriteTable("After Filtering", context.Posts.ToList());
context.DisableMyFilter = true;
FiddleHelper.WriteTable("After Filtering", context.Posts.ToList());
}
So you should already be able to have exactly what you are looking for
Let me know if I'm missing something
Best Regards,
Jon
Thank you for reply @JonathanMagnan !
I have a design where filters are part of the entity definition and so the context is not registering filters directly.
But I'll try to find time to test out the following idea: Filter definition in entity is a method that returns a new, filtering method. The "outer" method takes in context and the inner lambda (which is returned) takes in the IQueryable<T> itself. It should work, I think, but I have to test it that the values inside context object don't get cached somehow and never update for queries after first usage.
Roughly the code would look something like this:
[HasFilter<MyEntity>(nameof(GetFilter))]
public partial class MyEntity
{
public static Func<IQueryable<MyEntity>, IQueryable<MyEntity>> GetFilter(DbCtx ctx)
=> (IQueryable<MyEntity> query)
=> query.Where(x => x.Id == ctx.IdFilter);
}
And in context I have a "scanner code" that just runs through all entities (based on HasFilter attribute) and calls their respective "filter factories" to register the appropriate filters.
Some time ago I tried to achieve something similar with EF Core, but I went further and supported the whole DI for the filter factory argument list. The issue was that values supplied through DI service provider got "baked in" when query was first executed and never updated later on. When I then hardcoded it to only get the context instance and did not use anything related to DI, the values were not baked in and were properly changing in resulting query. I need to play around with this again...
Awesome,
Let me know if that work ;)
I have tested it now purely on EF Core (without the EntityFramework Plus addition) and it does work, with some caveats.
Consider this filter:
private static Expression<Func<User, bool>> ChangingFilter(AppDbContext dbCtx)
{
var id = Random.Shared.Next();
return x => x.Id == id && x.Id == dbCtx.ChangingId;
}
It creates a filter using locally generated ID and an ID that is coming from DB Context. The context has a property similar to this: public int ChangingId => Random.Shared.Next();.
Now when I do this:
var query1 = context.Users.ToQueryString();
var query2 = context.Users.ToQueryString();
var query3 = context.Users.ToQueryString();
What happens is that the locally generated ID gets baked in, while the id coming from the dbCtx is different for each query through parameterization. Example query generated:
.param set @__ef_filter__ChangingId_0 554844125
SELECT "u"."Id", "u"."Name", "u"."TenantId"
FROM "Users" AS "u"
WHERE "u"."Id" = 2120865947 AND "u"."Id" = @__ef_filter__ChangingId_0
There seems to be no way to make the value inside the filter function dynamic. It will always be "static" or "baked in" in a way that the value will not change between queries. I tried:
var id = Random.Shared.Next();<-- example abovevar id = () => Random.Shared.Next();<-- as a method that I can call in returned expressionExpression<Func<int>> id = () => Random.Shared.Next();<-- an expression of a method I can compile and invoke in returned filter expressionvar id = dbCtx.Filters.ChangingId;<-- even this gets baked invar id = () => dbCtx.Filters.ChangingId;<-- ...and thisExpression<Func<int>> id = () => dbCtx.Filters.ChangingId;<-- ...and this
I hoped that with some trickery, just in case, there could be some possibility to have dynamic values in filter method itself.
This will work dynamically, if DB Context is an argument to the local lambda:
Func<AppDbContext, int> id = (ctx) => ctx.Filters.ChangingId;
... but that somewhat defeats the whole purpose of it.
If you have ideas, I would gladly try them out. :)
Assuming that the EntityFramework Plus eventually uses the EF Core's basic HasQueryFilter() capability by building on top of it, it should behave the same way.