EntityFramework-Plus icon indicating copy to clipboard operation
EntityFramework-Plus copied to clipboard

Feature Request: Query Filter with DbContext as one of the arguments

Open Seramis opened this issue 9 months ago • 5 comments

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

Seramis avatar Mar 06 '25 12:03 Seramis

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

JonathanMagnan avatar Mar 06 '25 13:03 JonathanMagnan

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());	
}

Online Example

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

JonathanMagnan avatar Mar 10 '25 13:03 JonathanMagnan

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...

Seramis avatar Mar 10 '25 14:03 Seramis

Awesome,

Let me know if that work ;)

JonathanMagnan avatar Mar 10 '25 14:03 JonathanMagnan

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 above
  • var id = () => Random.Shared.Next(); <-- as a method that I can call in returned expression
  • Expression<Func<int>> id = () => Random.Shared.Next(); <-- an expression of a method I can compile and invoke in returned filter expression
  • var id = dbCtx.Filters.ChangingId; <-- even this gets baked in
  • var id = () => dbCtx.Filters.ChangingId; <-- ...and this
  • Expression<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.

Seramis avatar Mar 18 '25 09:03 Seramis