Entity Framework Core – Include Filters

The Entity Framework Core (EF) extension method Include provides us the ability to load additional data besides the entities we are querying for. For example: loading products along with their translations.

In this article:

pg
Pawel Gerr is architect consultant at Thinktecture. He focuses on backends with .NET Core and knows Entity Framework inside out.

The Entity Framework Core (EF) extension method Include provides us the ability to load additional data besides the entities we are querying for. For example: loading products along with their translations.

				
					var products = Context.Products
                      .Include(p => p.Translations)
                      .ToList();

--------------------------------------------------------------

public class Product
{
  public Guid Id { get; set; }
  public string Name { get; set; }
  public List<ProductTranslation> Translations { get; set; }
}

public class ProductTranslation
{
   public Guid ProductId { get; set; }
   public string Locale { get; set; }
   public string Description { get; set; }
}
				
			

In some use cases we want to load all translations for the requested products and in some cases we don’t. There are plenty of times when we need translations for a specific locale (i.e. language) only but at the moment there is no built-in support for providing filters to entities being included. You may go to the corresponding github issue#1833 and vote for this feature to increase the priority of it.

Some of our options are:

Entity Framework Plus

Not much to say here. Install the nuget package and use the extension method IncludeFilter.

				
					var products = Context.Products
                     .IncludeFilter(p => p.Translations.Where(t => t.Locale == "en"))
                     .ToList();
				
			

Please note that the generated queries will be different than when using built-in features of EF! To be aware of that is helpful especially during performance optimization, like finding appropriate database indexes.

Global Query Filters

The global query filters were introduced in EF 2.0 and are primarily for realization of features like soft delete and multi-tenancy. Although the filters are coupled to the entity type(s) and not to specific queries, still, we may enable and disable them at any time. Furthermore, despite the name the filters don’t have to be “global”, i.e. we can enable them for a specific instance of a DbContext only and not for all of them.

First, we introduce a variable that will be our filter and configure the corresponding query filter.

				
					public class DemoDbContext : DbContext
{
   private string _localeFilter;

   protected override void OnModelCreating(ModelBuilder modelBuilder)
   {
      ...

      modelBuilder.Entity<ProductTranslation>()
                  .HasQueryFilter(t => _localeFilter == null 
                                       || t.Locale == _localeFilter);

   }
				
			

For setting the filter we could make the variable _localeFilter public but the API would not be that nice and robust, that’s why …

The method for enabling/setting the filter comes next.

				
					// we are still in DemoDbContext
   public IDisposable SetTranslationFilter(string locale)
   {
      if (locale == null)
         throw new ArgumentNullException(nameof(locale));
      if (_localeFilter != null)
         throw new InvalidOperationException($"Changing a filter is not allowed.");

      _localeFilter = locale;

      return new FilterReset(this);
   }
				
			

SetTranslationFilter is returning an IDisposable to not to forget to reset the filter. The FilterReset is a nested private struct inside DemoDbContext.

				
					private readonly struct FilterReset : IDisposable
   {
      private readonly DemoDbContext _ctx;

      public FilterReset(DemoDbContext ctx)
      {
         _ctx = ctx ?? throw new ArgumentNullException(nameof(ctx));
      }

      public void Dispose()
      {
         _ctx._localeFilter = null;
      }
   }
				
			

We are done with the implementation of the filter. The usage is looks as following: 

				
					using (Context.SetTranslationFilter(locale))
{
   var products = Context.Products
                         .Include(p => p.Translations)
                         .ToList();
}
				
			

When using the global query filters in that manner then it is impossible that this filter is applied to other queries by accident because (1) the filter is bound to a specific instance of a DbContext and (2) a DbContext is not thread-safe so it must not be used in multiple threads concurrently.

Free
Newsletter

Current articles, screencasts and interviews by our experts

Don’t miss any content on Angular, .NET Core, Blazor, Azure, and Kubernetes and sign up for our free monthly dev newsletter.

EN Newsletter Anmeldung (#7)
Related Articles
Angular
SL-rund
If you previously wanted to integrate view transitions into your Angular application, this was only possible in a very cumbersome way that needed a lot of detailed knowledge about Angular internals. Now, Angular 17 introduced a feature to integrate the View Transition API with the router. In this two-part series, we will look at how to leverage the feature for route transitions and how we could use it for single-page animations.
15.04.2024
.NET
KP-round
.NET 8 brings Native AOT to ASP.NET Core, but many frameworks and libraries rely on unbound reflection internally and thus cannot support this scenario yet. This is true for ORMs, too: EF Core and Dapper will only bring full support for Native AOT in later releases. In this post, we will implement a database access layer with Sessions using the Humble Object pattern to get a similar developer experience. We will use Npgsql as a plain ADO.NET provider targeting PostgreSQL.
15.11.2023
.NET
KP-round
Originally introduced in .NET 7, Native AOT can be used with ASP.NET Core in the upcoming .NET 8 release. In this post, we look at the benefits and drawbacks from a general perspective and perform measurements to quantify the improvements on different platforms.
02.11.2023