-
Notifications
You must be signed in to change notification settings - Fork 3.2k
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
Support multiple HasQueryFilter calls on same entity type #10275
Comments
I've set up a Gist which shows my query filter use-case and the code I had to write to get it working. |
This is currently by-design as it is usually pretty easy to combine filters with '&&'. Filters can also be unset by passing null and so it is not just a matter of allowing |
@anpete - Sure, than consider this a feature request. ;) When the filters can't be combined easily with However, say I want to combine multiple filters based on different base/interface types. Like a type that implements both ISoftDeletableEntity and ITenantEntity. I don't think it's possible to wire both of these up in a single |
I would second a vote for this feature as I just hit it myself with the exact two scenarios mentioned; soft deletes and multi-tenancy. I was able to combine filters for my current use-case; but there can certainly be issues where the code to calc filters based on model inheritance could get awful "gummy" in a single filter declaration. |
Here's a gist of the workaround we have implemented: Edit: Woops, looks like I've already posted a Gist a year ago. The code in this one is a bit more cleaned up though. :) |
Interestingly the "Global Query Filters" documentation written 9 days before this bug was opened suggests calling
|
@FelixKing - Those |
My workaround with extension methods: internal static void AddQueryFilter<T>(this EntityTypeBuilder entityTypeBuilder, Expression<Func<T, bool>> expression)
{
var parameterType = Expression.Parameter(entityTypeBuilder.Metadata.ClrType);
var expressionFilter = ReplacingExpressionVisitor.Replace(
expression.Parameters.Single(), parameterType, expression.Body);
var internalEntityTypeBuilder = entityTypeBuilder.GetInternalEntityTypeBuilder();
if (internalEntityTypeBuilder.Metadata.QueryFilter != null)
{
var currentQueryFilter = internalEntityTypeBuilder.Metadata.QueryFilter;
var currentExpressionFilter = ReplacingExpressionVisitor.Replace(
currentQueryFilter.Parameters.Single(), parameterType, currentQueryFilter.Body);
expressionFilter = Expression.AndAlso(currentExpressionFilter, expressionFilter);
}
var lambdaExpression = Expression.Lambda(expressionFilter, parameterType);
entityTypeBuilder.HasQueryFilter(lambdaExpression);
}
internal static InternalEntityTypeBuilder GetInternalEntityTypeBuilder(this EntityTypeBuilder entityTypeBuilder)
{
var internalEntityTypeBuilder = typeof(EntityTypeBuilder)
.GetProperty("Builder", BindingFlags.NonPublic | BindingFlags.Instance)?
.GetValue(entityTypeBuilder) as InternalEntityTypeBuilder;
return internalEntityTypeBuilder;
} Usage: if (typeof(ITrackSoftDelete).IsAssignableFrom(entityType.ClrType))
modelBuilder.Entity(entityType.ClrType).AddQueryFilter<ITrackSoftDelete>(e => IsSoftDeleteFilterEnabled == false || e.IsDeleted == false);
if (typeof(ITrackTenant).IsAssignableFrom(entityType.ClrType))
modelBuilder.Entity(entityType.ClrType).AddQueryFilter<ITrackTenant>(e => e.TenantId == MyTenantId); |
Managing global filters can certainly become unwieldy. I have filters based on user roles and using Combining filters seems to be a common use case including this SO question but the solution can be difficult for beginners to follow. Can extension methods like these be added into EF Core, or would a separate nuget package be more expedient? |
When doing multiple includes and using HasQueryFilter on say, objects with child objects which all have a IsDeleted flag, the execute goes up to 14,000 MS. Is there a solution to this? I would rather not include deleted records in the context vs, the business or presentation layer. In my case the soft delete is a DateTime? and I only include where IS NULL, could that be the issue? IS a bool faster than a IS NULL check? |
@cgountanis Please file a new issue and include a small, runnable project solution or complete code listing that demonstrates the behavior you are seeing. |
@cgountanis Just saw #15996. Thanks! |
Here's a scenario where support for multiple In short, I wrote a method that lets you do this: modelBuilder.SetQueryFilterOnAllEntities<ITenantEntity>(e => e.TenantId == tenantId);
modelBuilder.SetQueryFilterOnAllEntities<ISoftDeletable>(e => !e.IsDeleted); What that code does is is find all entities that implement the interface and adds the specified query filter to the entity (some expression tree rewriting is involved). However, this doesn't work in the case where an entity implements both interfaces because the last query filter overwrites the previous one. |
I want to note that I can update my own method now that I know about this behavior, but it was surprising. It lead to entities from one tenant bleeding into another until i figured out what was going on. So in short, I think it's surprising that the last |
Actually, as I think about it, throwing an exception could be problematic if you were appending to an existing query filter by overwriting it. Perhaps adding an |
@haacked Thanks for the feedback. We agree that there is a usability issue here. We can't do anything here for 3.0, but for a future release we would like to make it possible to:
Also, we plan to implement filtered Include (#1833) for more localized ad-hoc filtering. |
Those all sound great! Thanks for following up. |
@YZahringer Any workaround for .net 3.0 |
@pantonis Updated to EF Core 3.1: internal static void AddQueryFilter<T>(this EntityTypeBuilder entityTypeBuilder, Expression<Func<T, bool>> expression)
{
var parameterType = Expression.Parameter(entityTypeBuilder.Metadata.ClrType);
var expressionFilter = ReplacingExpressionVisitor.Replace(
expression.Parameters.Single(), parameterType, expression.Body);
var currentQueryFilter = entityTypeBuilder.Metadata.GetQueryFilter();
if (currentQueryFilter != null)
{
var currentExpressionFilter = ReplacingExpressionVisitor.Replace(
currentQueryFilter.Parameters.Single(), parameterType, currentQueryFilter.Body);
expressionFilter = Expression.AndAlso(currentExpressionFilter, expressionFilter);
}
var lambdaExpression = Expression.Lambda(expressionFilter, parameterType);
entityTypeBuilder.HasQueryFilter(lambdaExpression);
} Usage: if (typeof(ITrackSoftDelete).IsAssignableFrom(entityType.ClrType))
modelBuilder.Entity(entityType.ClrType).AddQueryFilter<ITrackSoftDelete>(e => IsSoftDeleteFilterEnabled == false || e.IsDeleted == false);
if (typeof(ITrackTenant).IsAssignableFrom(entityType.ClrType))
modelBuilder.Entity(entityType.ClrType).AddQueryFilter<ITrackTenant>(e => e.TenantId == MyTenantId); |
Hey @YZahringer, thanks for the updated workaround script. Do you have any example about how to disable a specific filter for a given entity in Linq, using that approach? Thanks! |
@mhosman You can define a simple bool property in your DbContext like To be more dynamic/focussed, I suppose you can use an array of strings to enable named filters and use contains in your filter. |
We could combine multiple query filter calls. And add API HasNoQueryFilter to remove the filter. |
Has anyone used DynamicFilters package? On the surface it seems to provide functionality that some are requesting here. The ability enable a filter under specific condition (i.e. from HttpContext) seems very powerful. Hopefully some ideas can be incorporated into EF Core when the design stage for this issue is worked on. |
Use
|
If you ran the method twice for different interfaces, and an entity implemented more than one interface, only the last query filter would be applied. I have fixed this issue. Sources: dotnet/efcore#10275 https://gist.github.com/haacked/febe9e88354fb2f4a4eb11ba88d64c24
@YZahringer thanks for your answer. I modified your code a little bit.
and usage
|
If all you need is to append a new query filter you can use (EF Core 5.0.2)
it does not use internal EF api as previous answers so it should be quite stable :) |
@magiak thank you for this! I finally got around to updating my website to EF Core 5 and tested this out and it worked like a charm. I also updated my blog post to mention this code and credit you. |
- Configure multiple query filters on a entity referenced by name. - Ignore individual filters by name. The current implmentation of query filter is kept but when ignoring the filters using the current extension method will ignore all filters (so also the named). Fixes dotnet#8576 dotnet#10275 dotnet#21459
@magiak what |
Recently, I tried to work around the problem of being able to only use the last query filter. I have created an extension that allows the use of multiple filters and it is also possible to control which query filter is active with services injected into DbContext. Example:
|
Feel free to add this to the extensions list in docs. |
Since |
Sure, the only thing you need to do, is inject the |
Also consider #26146 |
I think the workaround for this problem, other than the one @nphmuller put together, is to abstract away I've long wondered why there isn't a marker interface for these sorts of things. Every project I've worked on in the last 10 years pretty much has an I think this would also (long-term) allow some standards for ORMs to specify metadata, similar to Microsoft.Extensions.DependencyInjection's 50 rules for IoC conformance. Ideally, that metadata layer is separate from any particular ORM. After all, we pretty much had these design patterns defined ~20 years ago by Fowler:
|
@Krzysztofz01 Does this package allow you to append to existing? Currently extending off of a base DbContext which applies one that we're trying to preserve while appending more |
Currently, the EFCore.QueryFilterBuilder is working more like a LINQ Expressions compiler wrapper with an EntityFramework-like API. The current implementation is not capable of editing query filters which were added using the default API. I'm doing some research to make this tool more versatile, by experimenting and overriding some DbContext default behaviour. |
My solution:
|
Based on @nphmuller's gist, I created a nuget package. |
Hi guys (and @nphmuller of course ;) So i came up with the following improvements. DbContext:
Extension methods:
Test Methods
Hope this will help you out in case of improvements needed. |
As of 2.0 multiple
HasQueryFilter()
calls onEntityTypeBuilder
result in only the latest one being used. It would be nice if multiple query filters could be defined this way.Example:
Current workaround is to define all filters in single expression, or rewrite the expression to concat multiple filters.
The text was updated successfully, but these errors were encountered: