Efcore: Automatically perform required conversions when query filter is defined on interface or base type

Created on 10 Nov 2017  路  4Comments  路  Source: dotnet/efcore

Say I have the following interface that can be implemented on any entity that I want to support soft-deletion.

``` c#
public interface ISoftDeletableEntity
{
bool IsDeleted { get; set; }
}

It would be nice to be able to define a global query filter for any entity type that implements this interface like this:
``` c#
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<ISoftDeletableEntity>().HasQueryFilter(e => e.IsDeleted == false);
}

Currently this throws the following exception: System.ArgumentException: 'The entity type 'MyApp.ISoftDeletableEntity' provided for the argument 'clrType' must be a reference type.'

area-query propose-close type-enhancement

Most helpful comment

@nphmuller This is something that can be done with bulk configuration on the mapped entity types:

```C#
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
foreach (var entityType in modelBuilder.Model.GetEntityTypes()
.Where(e => typeof(ISoftDeletableEntity).IsAssignableFrom(e.ClrType)))
{
modelBuilder
.Entity(entityType.ClrType)
.HasQueryFilter(ConvertFilterExpression(e => !e.IsDeleted, entityType.ClrType));
}
}

private static LambdaExpression ConvertFilterExpression(
Expression> filterExpression,
Type entityType)
{
var newParam = Expression.Parameter(entityType);
var newBody = ReplacingExpressionVisitor.Replace(filterExpression.Parameters.Single(), newParam, filterExpression.Body);

return Expression.Lambda(newBody, newParam);

}

Making a better experience around this is covered by #9117 and #6787.

The code is more complicated in this case than in many others because it requires constructing a lambda expression where the parameter is validated to be the exact entity type--not the interface. I'm going to re-purpose this bug to automatically handle base types/interfaces in EF code. After this issue is fixed it should be possible to simplify the above to:

```C#
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    Expression<Func<ISoftDeletableEntity, bool>> expression = e => !e.IsDeleted;

    foreach (var entityType in modelBuilder.Model.GetEntityTypes()
        .Where(e => typeof(ISoftDeletableEntity).IsAssignableFrom(e.ClrType)))
    {
        modelBuilder.Entity(entityType.ClrType).HasQueryFilter(expression);
    }
}

All 4 comments

@nphmuller This is something that can be done with bulk configuration on the mapped entity types:

```C#
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
foreach (var entityType in modelBuilder.Model.GetEntityTypes()
.Where(e => typeof(ISoftDeletableEntity).IsAssignableFrom(e.ClrType)))
{
modelBuilder
.Entity(entityType.ClrType)
.HasQueryFilter(ConvertFilterExpression(e => !e.IsDeleted, entityType.ClrType));
}
}

private static LambdaExpression ConvertFilterExpression(
Expression> filterExpression,
Type entityType)
{
var newParam = Expression.Parameter(entityType);
var newBody = ReplacingExpressionVisitor.Replace(filterExpression.Parameters.Single(), newParam, filterExpression.Body);

return Expression.Lambda(newBody, newParam);

}

Making a better experience around this is covered by #9117 and #6787.

The code is more complicated in this case than in many others because it requires constructing a lambda expression where the parameter is validated to be the exact entity type--not the interface. I'm going to re-purpose this bug to automatically handle base types/interfaces in EF code. After this issue is fixed it should be possible to simplify the above to:

```C#
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    Expression<Func<ISoftDeletableEntity, bool>> expression = e => !e.IsDeleted;

    foreach (var entityType in modelBuilder.Model.GetEntityTypes()
        .Where(e => typeof(ISoftDeletableEntity).IsAssignableFrom(e.ClrType)))
    {
        modelBuilder.Entity(entityType.ClrType).HasQueryFilter(expression);
    }
}

@ajcvickers Wouldn't be more efficient to set IMutableEntityType.QueryFilter like my example at https://github.com/aspnet/EntityFrameworkCore/issues/8881#issuecomment-337743060? That way you don't look up every affected entity type twice. Please "Automatically perform required conversions when query filter is defined" in that manner, too.

@ajcvickers
Nice. Was hoping it was possible already, but got stuck rewriting the expression. Thanks for the sample. It helps out a lot!

Totally agree with the repurpose. The sample after the fix is way easier to write.

I've set up a Gist which shows my query filter use-case and the code I had to write to get it working.
https://gist.github.com/nphmuller/8891c315d79aaaf720f9164cd0f10400~~
https://gist.github.com/nphmuller/05ff66dfa67e1d02cdefcd785661a34d

Was this page helpful?
0 / 5 - 0 ratings