Global Query Filters in Entity Framework Core

Global Query Filters in Entity Framework Core

In this article, I will discuss Global Query Filters in Entity Framework Core (EF Core) with Examples. Please read our previous article discussing Shadow Properties in Entity Framework Core with Examples.

What are Global Query Filters in Entity Framework Core?

Global Query Filters in Entity Framework Core allow developers to define filters at the model level that are automatically applied to all queries involving a specific entity type. These filters are defined once in the OnModelCreating method of our DbContext class, ensuring consistency and reducing repetitive code across our application. Once defined, these filters are automatically applied to all queries targeting those entities unless explicitly overridden.

They help implement features such as soft deletes, multi-tenancy, data partitioning by user roles, and other scenarios where certain conditions should consistently apply across all queries involving specific entities.

Key Features Global Query Filters in EF Core:
  • Automatic Filtering: You don’t need to apply the filter manually every time you query the database; it is automatically included in every query unless explicitly bypassed using LINQ’s IgnoreQueryFilters() method.
  • Reusable: Once defined, the filter applies globally to the entity.
  • Global Scope: Filters apply to all queries, including Select, Include, Count, Sum, etc.
  • Custom Logic: Global Query Filter allows us to define custom filtering logic, including conditions based on specific columns or global application settings.
  • Optional or Conditional: Filters can be added or removed at runtime, depending on the needs of your application.
When to Use Global Query Filters

This is particularly useful for scenarios such as:

  • Soft Deletes: When an entity is marked as deleted but should still exist in the database for historical purposes, a global query filter can exclude those entities from queries (e.g., skip all rows where IsDeleted = true).
  • Multi-Tenancy: For applications serving multiple tenants, a filter can ensure that each tenant only sees their own data (e.g., filter by TenantId to retrieve data belonging to the current tenant).
  • Permissions/Access Control: Filter out data that the current user should not have access to based on their roles or permissions. That means restricting data access based on user roles or permissions.
Soft Delete Functionality in EF Core Using Global Query Filters

In many Real-time applications, we implement soft deletes, where records are marked as deleted rather than physically removed from the database. This allows for data recovery and auditing while keeping the data out of regular application workflows.

Define the Entity (Order) with Soft Delete Property

We will implement a “SOFT DELETE” mechanism where entities marked as “deleted” are not physically removed from the database but instead excluded from query results.

In this example, assume you have an Order entity with an IsDeleted flag. Instead of manually filtering out deleted orders every time, we can set up a global query filter that automatically excludes orders marked as deleted. So, create a class file named Order.cs within the Entities folder and copy and paste the following code.

namespace EFCoreCodeFirstDemo.Entities
{
    public class Order
    {
        public int OrderId { get; set; }          // Primary Key
        public string ProductName { get; set; }   // Name of the product
        public int Quantity { get; set; }         // Quantity ordered
        public bool IsDeleted { get; set; }       // Soft delete flag
        public DateTime OrderDate { get; set; }   // Date of the order
    }
}
Configure Global Query Filter in DbContext

To implement global query filters, we need to override the OnModelCreating method of our DbContext class and use the HasQueryFilter method on the entity configurations. We will define a global query filter to exclude orders marked as deleted. So, modify the EFCoreDbContext class as follows:

using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
namespace EFCoreCodeFirstDemo.Entities
{
    public class EFCoreDbContext : DbContext
    {
        // Configuring the database connection and logging
        protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
        {
            // Display the generated SQL queries in the console
            optionsBuilder.LogTo(Console.WriteLine, LogLevel.Information);

            // Configuring the database connection
            optionsBuilder.UseSqlServer(@"Server=LAPTOP-6P5NK25R\SQLSERVER2022DEV;Database=EFCoreDB;Trusted_Connection=True;TrustServerCertificate=True;");
        }

        // Defining the global query filter in OnModelCreating
        protected override void OnModelCreating(ModelBuilder modelBuilder)
        {
            // Global Query Filter to exclude soft-deleted orders
            modelBuilder.Entity<Order>().HasQueryFilter(o => !o.IsDeleted);
        }

        // DbSet representing Orders table
        public DbSet<Order> Orders { get; set; }
    }
}
Apply Migrations and Update the Database

Open the Package Manager Console and execute the following command:

  • Add-Migration AddSoftDeleteToOrder
  • Update-Database

Once you execute the above commands, it should create the database with the required Orders table as shown in the below image:

Global Query Filters in Entity Framework Core

Insert Sample Data

Please execute the following SQL commands to insert sample data into the Orders table. Ensure that some records are marked as deleted.

INSERT INTO Orders (ProductName, Quantity, IsDeleted, OrderDate) VALUES ('Laptop', 2, 0, GETDATE()); -- Not Deleted
INSERT INTO Orders (ProductName, Quantity, IsDeleted, OrderDate) VALUES ('Smartphone', 5, 1, GETDATE()); -- Deleted
INSERT INTO Orders (ProductName, Quantity, IsDeleted, OrderDate) VALUES ('Headphones', 10, 0, GETDATE()); -- Not Deleted
INSERT INTO Orders (ProductName, Quantity, IsDeleted, OrderDate) VALUES ('Monitor', 3, 1, GETDATE()); -- Deleted
Querying Entities with Global Query Filter Applied

Now, whenever you query the Orders DbSet, only orders not marked as deleted will be returned, and we don’t need to add the condition in our queries explicitly. For a better understanding, please modify the Program class as follows:

using EFCoreCodeFirstDemo.Entities;
namespace EFCoreCodeFirstDemo
{
    public class Program
    {
        public static void Main(string[] args)
        {
            try
            {
                using var context = new EFCoreDbContext();

                // Retrieve all active (non-deleted) orders
                var activeOrders = context.Orders.ToList();

                Console.WriteLine("\nActive Orders:");
                foreach (var order in activeOrders)
                {
                    Console.WriteLine($"\tOrder ID: {order.OrderId}, Product: {order.ProductName}, Quantity: {order.Quantity}, Order Date: {order.OrderDate}");
                }
            }
            catch (Exception ex)
            {
                Console.WriteLine($"An error occurred while fetching active orders: {ex.Message}");
            }
        }
    }
}
Output:

Querying Entities with Global Query Filter Applied

You can see it only displays orders where IsDeleted is false, and the same is applied in the WHERE clause of the SELECT query by EF Core.

Bypassing the Global Query Filter in Entity Framework Core

There are scenarios where you might need to ignore global query filters (e.g., an admin page that displays all records, regardless of IsDeleted). To disable the filters, we can use the IgnoreQueryFilters() extension method on our queries.

For example, we want to access all orders, including those marked as deleted (e.g., for administrative purposes). In that case, we can bypass the global query filter using the IgnoreQueryFilters method. For a better understanding, please modify the Program class as follows:

using EFCoreCodeFirstDemo.Entities;
using Microsoft.EntityFrameworkCore;
namespace EFCoreCodeFirstDemo
{
    public class Program
    {
        public static void Main(string[] args)
        {
            try
            {
                using var context = new EFCoreDbContext();

                // Retrieve all orders, including deleted ones
                var allOrders = context.Orders.IgnoreQueryFilters().ToList();

                Console.WriteLine("\nAll Orders (Including Deleted):");
                foreach (var order in allOrders)
                {
                    Console.WriteLine($"\tOrder ID: {order.OrderId}, Product: {order.ProductName}, Quantity: {order.Quantity}, Order Date: {order.OrderDate}");
                }
            }
            catch (Exception ex)
            {
                Console.WriteLine($"An error occurred while fetching active orders: {ex.Message}");
            }
        }
    }
}
Output:

As you can see in the above output, all orders are displayed regardless of the IsDeleted flag.

Performance Consideration when working with Global Query Filters in EF Core

For optimal performance, especially with large datasets, consider adding an index to the IsDeleted column. This can significantly speed up query execution by allowing the database to locate records based on the soft delete flag quickly.

CREATE INDEX IX_Orders_IsDeleted ON Orders(IsDeleted);

Parameterized Global Filter in Entity Framework Core

EF Core supports parameterized filters, allowing us to pass runtime parameters into global query filters. This is useful for scenarios like multi-tenancy, where the tenant ID (Customer ID) is determined at runtime. Multi-tenancy refers to a software architecture where a single instance of an application (including its infrastructure, such as databases and servers) serves multiple customers, known as tenants.

Let’s consider an example of a multi-tenant system where each customer can only view their own orders. This is done by filtering data based on the current CustomerId. We will use Customer and Order entities, and the Order entity will include a CustomerId property to identify the customer to which the order belongs. So, let us first create the entities:

Customer Entity:

So, create a class file named Customer.cs within the Entities folder and then copy and paste the following code.

namespace EFCoreCodeFirstDemo.Entities
{
    public class Customer
    {
        public int CustomerId { get; set; } // Primary Key
        public string Name { get; set; } // Customer name
        public string Email { get; set; } // Customer email

        // One-to-many relationship: A customer can have multiple orders
        public List<Order> Orders { get; set; }
    }
}
Order Entity:

So, create a class file named Order.cs within the Entities folder and then copy and paste the following code.

namespace EFCoreCodeFirstDemo.Entities
{
    public class Order
    {
        public int OrderId { get; set; }          // Primary Key
        public string ProductName { get; set; }   // Name of the product
        public int Quantity { get; set; }         // Quantity ordered
        public bool IsDeleted { get; set; }       // Soft delete flag
        public DateTime OrderDate { get; set; }   // Date of the order
        public int CustomerId { get; set; } // Foreign key for the customer
        public Customer Customer { get; set; } //Navigation Property
    }
}
Configure the Global Query Filter in DbContext

We will define a global query filter that restricts data access based on the current Customer ID. We will inject the current customer ID into the context object for demonstration purposes to filter data accordingly. So, modify the EFCoreDbContext class as follows:

using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
namespace EFCoreCodeFirstDemo.Entities
{
    public class EFCoreDbContext : DbContext
    {
        // Simulated Customer ID (In real applications, retrieve this from the authenticated user's context)
        private readonly int _currentCustomerId;

        public EFCoreDbContext(int currentCustomerId)
        {
            _currentCustomerId = currentCustomerId;
        }

        public EFCoreDbContext()
        {
        }

        // Configuring the database connection and logging
        protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
        {
            // Display the generated SQL queries in the console
            // optionsBuilder.LogTo(Console.WriteLine, LogLevel.Information);

            // Configuring the database connection
            optionsBuilder.UseSqlServer(@"Server=LAPTOP-6P5NK25R\SQLSERVER2022DEV;Database=EFCoreDB;Trusted_Connection=True;TrustServerCertificate=True;");
        }

        // Defining the global query filter in OnModelCreating
        protected override void OnModelCreating(ModelBuilder modelBuilder)
        {
            // Global Query Filter to enforce cutsomer-based data isolation
            modelBuilder.Entity<Order>().HasQueryFilter(c => c.CustomerId == _currentCustomerId);
        }

        // DbSet representing Customers and Orders tables
        public DbSet<Order> Orders { get; set; }
        public DbSet<Customer> Customers { get; set; }
    }
}
Apply Migrations and Update the Database

Before proceeding further, please delete the existing EFCoreDB database and the Migration folder from the Projects. Then, open the Package Manager Console and execute the following code:

  • Add-Migration AddTenantIdToCustomer
  • Update-Database
Insert Sample Data

Execute the following SQL commands to insert sample data into the Customers and Orders table. Ensure that orders belong to different customers.

USE EFCoreDB
GO

-- Insert into Customers table
INSERT INTO Customers (Name, Email) VALUES ('Alice Johnson', 'alice@example.com');
INSERT INTO Customers (Name, Email) VALUES ('Bob Smith', 'bob@example.com');
INSERT INTO Customers (Name, Email) VALUES ('Charlie Brown', 'charlie@example.com');
GO

-- Insert into Orders table
INSERT INTO Orders (ProductName, Quantity, IsDeleted, OrderDate, CustomerId) VALUES 
('Laptop', 2, 0, GETDATE(), 1),
('Smartphone', 5, 1, GETDATE(), 2),
('Headphones', 10, 0, GETDATE(), 1), 
('Monitor', 3, 1, GETDATE(), 2), 
('Desktop', 2, 0, GETDATE(), 1),
('Tablet', 5, 1, GETDATE(), 3), 
('Keyboard', 10, 1, GETDATE(), 1), 
('Earpad', 3, 1, GETDATE(), 2); 
GO
Querying Entities with Global Query Filter Applied

When querying Orders, the global query filter automatically restricts the results to those belonging to the current customer (CustomerId = 1).

using EFCoreCodeFirstDemo.Entities;
using Microsoft.EntityFrameworkCore;
namespace EFCoreCodeFirstDemo
{
    public class Program
    {
        public static void Main(string[] args)
        {
            try
            {
                // Simulating CustomerId from authentication
                int customerId = 1; // Example CustomerId

                using var context = new EFCoreDbContext(customerId);

                // Retrieve Orders for the current tenant
                var customer = context.Customers
                            .Include(ord => ord.Orders)
                            .FirstOrDefault();

                if (customer != null)
                {
                    Console.WriteLine($"Customer ID: {customerId}, Name: {customer.Name}, Email: {customer.Email}");
                    Console.WriteLine($"Customer Orders");
                    foreach (var order in customer.Orders)
                    {
                        Console.WriteLine($"\tOrder ID: {order.OrderId}, Product Name: {order.ProductName}, Order Date: {order.OrderDate.ToShortDateString()}");
                    }
                }
                else
                {
                    Console.WriteLine($"Customer ID: {customerId} Not Found");
                }
            }
            catch (DbUpdateException ex)
            {
                Console.WriteLine($"Database error: {ex.InnerException?.Message ?? ex.Message}");
            }
            catch (Exception ex)
            {
                Console.WriteLine($"An error occurred while fetching cutsomer-specific orders: {ex.Message}");
            }
        }
    }
}
Output:

Parameterized Global Filter in Entity Framework Core

Combining Multiple Global Query Filters in EF Core:

We can combine multiple global query filters using logical operators in the HasQueryFilter() method to meet complex business logic. For a better understanding, modify the EFCoreDbContext class as follows. Here, we are applying SOFT DELETE and CUSTOMER ID using logical AND Operators:

using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
namespace EFCoreCodeFirstDemo.Entities
{
    public class EFCoreDbContext : DbContext
    {
        // Simulated Customer ID (In real applications, retrieve this from the authenticated user's context)
        private readonly int _currentCustomerId;

        public EFCoreDbContext(int currentCustomerId)
        {
            _currentCustomerId = currentCustomerId;
        }

        public EFCoreDbContext()
        {
        }

        // Configuring the database connection and logging
        protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
        {
            // Display the generated SQL queries in the console
            // optionsBuilder.LogTo(Console.WriteLine, LogLevel.Information);

            // Configuring the database connection
            optionsBuilder.UseSqlServer(@"Server=LAPTOP-6P5NK25R\SQLSERVER2022DEV;Database=EFCoreDB;Trusted_Connection=True;TrustServerCertificate=True;");
        }

        // Defining the global query filter in OnModelCreating
        protected override void OnModelCreating(ModelBuilder modelBuilder)
        {
            // Global Query Filter to enforce cutsomer-based data isolation
            
            //Combining using AND (&&) or OR (||)
            modelBuilder.Entity<Order>().HasQueryFilter(o => !o.IsDeleted && o.CustomerId == _currentCustomerId);
        }

        // DbSet representing Customers and Orders tables
        public DbSet<Order> Orders { get; set; }
        public DbSet<Customer> Customers { get; set; }
    }
}
Output:

Combining Multiple Global Query Filters in EF Core

Note: Global Query Filters are not applied to raw SQL queries executed via methods like FromSqlRaw.

Limitations and Considerations Global Query Filters in Entity Framework Core
  • Filters Don’t Apply to FromSqlRaw/FromSqlInterpolated: Global query filters do not apply automatically when we use raw SQL queries via FromSqlRaw or FromSqlInterpolated. If you rely heavily on raw SQL, you may have to replicate some filtering logic directly in your SQL statements.
  • Query Performance: Each global filter adds a WHERE clause to your queries for the specified entity. If you have many filters or complex filters, it might impact performance. Proper indexing (e.g., indexing on the IsDeleted and TenantId columns) can help maintain good performance.
  • Navigation Properties and Include: Global filters also apply to navigation properties when you use .Include(…). If you load an entity with includes, the child entities will be filtered unless we disable or override the filter.

Global Query Filters in Entity Framework Core help us implement consistent filtering logic across all queries without the need for repetitive code. They are particularly useful for scenarios like soft deletes, multi-tenancy, and access control. However, it’s essential to keep performance and maintainability in mind. They can help keep our application code cleaner, more maintainable, and consistent when used effectively.

In the next article, I will discuss the Entity Framework Core Database First Approach with Examples. In this article, I try to explain Global Query Filters in Entity Framework Core (EF Core) with Examples. I hope you enjoy this article on Global Query Filters in Entity Framework Core.

1 thought on “Global Query Filters in Entity Framework Core”

Leave a Reply

Your email address will not be published. Required fields are marked *