Skip to content

Latest commit

 

History

History
144 lines (109 loc) · 9.75 KB

README.md

File metadata and controls

144 lines (109 loc) · 9.75 KB

Custom Query Filters Test Project

A test project devoted to replacing Entity Framework Core's global query filters with a more flexible and controllable solution. This is built on top of ABP Framework and runs on .NET 5. P.S. It's a mess because it's a test 😛

A list of current issues can be found below. If you feel like helping out, please create a PR

The problem

Filters such as soft deletion can be configured on entites by using global query filters e.g. ModelBuilder.Entity<Post>.HasQueryFilter(p => !p.IsDeleted) which will ensure that only non-deleted posts are returned by appending the filter to every query.

There are times when you need to return deleted entities, and only way to achieve this (with the built-in query filter functionality) is to add a property to your DbContext which is evaluated at runtime. This causes databases providers to not index the query whilst also increasing the query complexity which can hurt performance.

A common example of when you might not want to have and entitiy automatically filtered is when it is a child of another entity.

class Blog
{
    public ICollection<Post> Posts { get; set; }
}

class Post
{
    public Blog Blog { get; set; }
}

Using the example above, if you had a Blog entity which contains a collection of Posts (ICollection<Post> Posts) and some of those posts are deleted, the Blog.Posts property will only contain the non-deleted Post entities due to gloabl query filters being applied to the Posts.

Similarly, if you want to return ALL Posts and you have the Blog as a property of the Post i.e. Post.Blog, only posts belonging to non-deleted blogs will be returned even if they themselves are not deleted!

Microsoft say this is to ensure referential integrity, but this is only applicable at a database level and will be enforced at that level anyway. It leads to unexpected results (which are hard to spot) and there are also bugs with the implementation (try running Count() on the IQueryable before returning the list - you'll get less results than count says you should have!). In my opinion, query filters are business rules, and sometimes you need to ignore those rules when manipulating data. Microsoft's current implementation is also too restrictive for any real-world application.

The goal is simple: Ignore the global query filters for specific entities by using ABP's DataFilters i.e. DataFilter.Disable<ISoftDelete<Blog>>() unless the IQueryable extension method IQueryable.IgnoreAbpQueryFilters() has been used, which stops the filters being applied on a per-query basis.

This solution should fix various issues which have been raised in the past - see linked issues. It could even be extended to address current query filter limitations, create dynamic filters and allow ABP to leverage EF6 compiled models.

I am also planning on integrating these changes to ABP's DataFilters to facilitate this project.

The method

  1. Disable the use of global query filters (the ABP ModelBuilder.OnModelCreating() query filter generation code is bypassed, so no calls to Entity.UseQueryFilter())
  2. Intercept the query compilation and append the appropriate data filters

Usage example

using (DataFilter.Enable<ISoftDelete())
using (DataFilter.Disable<ISoftDelete<Blog>>())
{
    // This should return a list of non-deleted posts with the 'Blog' populated
    // even if the Blog is marked as deleted.
    var postWithDeletedBlog = PostsRepository
        .Include(x => x.Blog)
        .ToListAsync();
}

Current issues

Replacing query filters sounds simpler than it actually is and there are many hurdles to overcome.

The following known issues (non-exhaustive) are present in the solution:

  • ❌ Collection filtering doesn't work for nested includes. It will also return unexpected results on filtered collections when using the same DbContext due to navigation fixup when tracking entities.
  • ❌ Lazy/Eager/Implicit loading isn't considered nor is IgnoreAutoIncludes, entity tracking and skip navigations etc.
  • ❌ Filters shouldn't be applied if the navigation items are not going to be loaded. This relates the the point above.
  • ❌ Different DB providers implement things differently - Only Relational EF provider is currently implemented.
  • ✔️ Queries are cached so if filters change at runtime, they don't take effect.

Running the project

  1. Ensure that MySQL is installed and that your appsettings.json is correct
  2. Run the DbMigrations project to create the database and seed the blog/post data
  3. Run the Web project to see the sample data. You can modify Pages/index.js to change which queries are run.

Execution flow:

  • CustomAbpDbContext is an extension to AbpDbContext which overrides some functionality, adds the AbpGlobalFiltersOptionsExtension and replaces the IQueryTranslationPreprocessorFactory
    • CustomQueryTranslationPreprocessorFactory creates an instance of CustomQueryTranslationPreprocessor
      • An instance of AbpFilterAppendingExpressionVisitor is created by CustomQueryTranslationPreprocessor which then modifies the query in the Process() method before allowing the base provider to proccess the query
        1. This uses the AbpGlobalFiltersOptionsExtension to gain access to important contextual information such as the IDataFilter and CurrentTenant which are required to modify the query
        2. The CompiledQueryWithAbpFiltersCacheKey decides if the query needs to be processed again or a cached copy of the results can be returned. This looks to see if the DataFilters have changed etc. and if they have then the AbpFilterAppendingExpressionVisitor is executed to regenerate the query.

For more info about the ABP project, you can visit docs.abp.io.

Main project files

  • Application
    • BlogAppService.cs
    • PostAppService.cs
  • Domain
    • Data/AppDataSeedContributor.cs
    • Extensions/AbpQueryableExtensions.cs
    • AbpQueryFilterDemoConsts.cs <-- Change core configuration here
  • Domain.Shared
    • IMultiTenantExtension.cs
    • ISoftDeleteExtension.cs
  • EntityframeworkCore
    • Extensions/
      • AbpFilterAppendingExpressionVisitor.cs
      • AbpGlobalFiltersOptionsExtension.cs
      • CompiledQueryWithAbpFiltersCacheKeyGenerator.cs
      • CustomQueryTranslationPreprocessor.cs
      • Extensions.cs
    • CustomAbpDbContext.cs
    • Repositories/PostRepository.cs
  • Web
    • Pages/Index.cshtml
    • Pages/Index.js
    • Logs/log.txt <-- View the compiled SQL queries here

Most of the logic is in AbpFilterAppendingExpressionVisitor so check that out first if you want to get stuck in.

Useful links

Docs

Source code

Linked issues