dynamic filtering in C# using Asp.NET Core

In our previous tutorial, we discussed the key points of expression trees, their use cases and limitations. Any topic without a practical example, especially if it is related to programming, is of little use. In this article, we'll look at the second part of expression trees in C# and show the real power of using them in practice.

What are we going to build?

Our main goal is to create a web API in Asp.NET Core with dynamic filtering functionality built using Minimal API, EF Core and of course expression trees.

We plan to build filtering for a product database and use expression trees to show one of the real capabilities of expression trees in building complex and dynamic queries. Here is the final example with several dynamic filtering arguments:

expression trees in C#

expression trees in C#

For more complete examples, refer to the repository at GitHub.

Getting started

First, open Visual Studio and select the Asp.NET Core Web API template with the following configuration:

asp.net core web api termplate

asp.net core web api termplate

We use .NET 8.0, but the theme itself is independent of the .NET version. You can even use the classic .NET Framework to work with expression trees. The name of the project is “ExpressionTreesInPractice

Here is the generated template from Visual Studio:

project initial state

project initial state

To have simple storage we will use InMemory Ef Core. You can use any other EF Core substore.

Now go to Tool->Nuget Package Manager->Package Manager Console and enter the following command:

install-package microsoft.entityframeworkcore.inmemory

Now let's create our implementation DbContext. Create a folder called 'Database' and add the class to it ProductDbContext with the following content:

using ExpressionTreesInPractice.Models;
using Microsoft.EntityFrameworkCore;

namespace ExpressionTreesInPractice.Database
{
    public class ProductDbContext : DbContext
    {
        public DbSet<Product> Products { get; set; }
        public ProductDbContext(DbContextOptions<ProductDbContext> options) : base(options) { }
        protected override void OnModelCreating(ModelBuilder modelBuilder)
        {
            modelBuilder.Entity<Product>().HasData(new List<Product>
            {
                new Product(){ Id = 1, Category = "TV", IsActive = true, Name = "LG", Price = 500},
                new Product(){ Id = 2, Category = "Mobile", IsActive = false, Name = "Iphone", Price = 4500},
                new Product(){ Id = 3, Category = "TV", IsActive = true, Name = "Samsung", Price = 2500}
            });
            base.OnModelCreating(modelBuilder);
        }
    }
}

We just added some basic data to be initialized when the app starts and that's what we need to override OnModelCreating from DbContext. A great example of using the Template Method pattern, isn't it?

We need our entity model called Productyou can create a folder 'Models' and add the class there Product with the following content:

namespace ExpressionTreesInPractice.Models
{
    public class Product
    {
        public int Id { get; set; }
        public string Category { get; set; }
        public decimal Price { get; set; }
        public bool IsActive { get; set; }
        public string Name { get; set; }
    }
}

Now it's time to register our implementation DbContext in the file Program.cs:

builder.Services.AddDbContext<ProductDbContext>(x => x.UseInMemoryDatabase("ProductDb"));

By the way, there are a lot of unnecessary code fragments in Program.cs that need to be removed. After all the cleanup, our code should look like this:

using ExpressionTreesInPractice.Database;
using Microsoft.EntityFrameworkCore;

var builder = WebApplication.CreateBuilder(args);

// Добавляем сервисы в контейнер.
builder.Services.AddDbContext<ProductDbContext>(x => x.UseInMemoryDatabase("ProductDb"));

var app = builder.Build();

// Настраиваем конвейер HTTP-запросов.
app.UseHttpsRedirection();

app.Run();

We don't want to use controllers as they are heavy and cause additional problems. Therefore, we choose the minimal API. If you are not familiar with the minimal APIs, please take a look our video tutorialto find out more.

Once you've figured it out, open Program.cs and add the following code:

app.MapGet("/products", async ([FromBody] ProductSearchCriteria productSearch, ProductDbContext dbContext) =>
{ }

The above code defines a route in the ASP.NET Core Minimal API and creates an endpoint for HTTP-request GET on the way /products. The method uses asynchronous programming to handle potentially long operations without blocking the application's main thread.

ProductSearchCriteria is a parameter passed to the method that contains criteria for filtering products. It is marked with the attribute [FromBody]which means the request body will be bound to this parameter. Usually GET-requests do not use a request body, but in this case it is allowed if a complex object needs to be passed.

ProductDbContext is a database context that represents a session with the database. It is embedded in a method, allowing the application to perform operations such as querying products based on search criteria.

Reason for use ProductSearchCriteria instead of Product is that the request must be dynamic. In this case, the user can provide some of the product attributes, but not all. Since properties Product do not allow values nullthe user would be forced to specify all properties, even if he does not want to filter by all.

Usage ProductSearchCriteria gives more flexibility. This is a container for optional and dynamic parameters. The user can specify only those attributes they want to search on, making it more suitable for scenarios where not all product properties are needed in the query.

This is what our class looks like ProductSearchCriteria in the folder 'Models'.

namespace ExpressionTreesInPractice.Models
{
    public record PriceRange(decimal? Min, decimal? Max);
    public record Category(string Name);
    public record ProductName(string Name);
    public class ProductSearchCriteria
    {
        public bool? IsActive { get; set; }
        public PriceRange? Price { get; set; }
        public Category[]? Categories { get; set; }
        public ProductName[]? Names { get; set; }
    }
}

Now let's focus on implementing the minimal API. Please note that the purpose of this tutorial is not to show best practices or write clean code. The goal is to demonstrate expression trees in practice, and after mastering the material, you can easily refactor the code.

Here is the first code snippet inside the function MapGet:

await dbContext.Database.EnsureCreatedAsync();
 ParameterExpression parameterExp = Expression.Parameter(typeof(Product), "x");
 Expression predicate = Expression.Constant(true);//x=>True && x.IsActive=true/false

 if (productSearch.IsActive.HasValue)
 {
     MemberExpression memberExp = Expression.Property(parameterExp, nameof(Product.IsActive));

     ConstantExpression constantExp = Expression.Constant(productSearch.IsActive.Value);

     BinaryExpression binaryExp = Expression.Equal(memberExp, constantExp);

     predicate = Expression.AndAlso(predicate, binaryExp);
 }

var lambdaExp = Expression.Lambda<Func<Product, bool>>(predicate, parameterExp);
var data = await dbContext.Products.Where(lambdaExp).ToListAsync();
return Results.Ok(data);

This code uses C# expression classes to dynamically build a predicate for a database query. Let's take it step by step.

await dbContext.Database.EnsureCreatedAsync();

This statement asynchronously checks whether the database has been created. If it does not exist, it will be created. This is typically used in development or testing environments to ensure that the database schema is in place.

ParameterExpression parameterExp = Expression.Parameter(typeof(Product), "x");

This creates an expression parameter that represents an instance of the class Product. This will act as an input parameter (x) in the expression tree, similar to how you define a lambda expression like x => ....

Expression predicate = Expression.Constant(true);

Initially, the predicate is created as a constant Boolean expression with the value true. This is useful for building a dynamic predicate step by step, since you can use it as a base for adding other conditions (e.g. true AND other conditions). This serves as a starting point for combining additional expressions.

if (productSearch.IsActive.HasValue)

This block if checks that the property IsActive V productSearch not equal nullwhich means the user has set a filter for product activity.

Inside the block if:

MemberExpression memberExp = Expression.Property(parameterExp, nameof(Product.IsActive));

This creates MemberExpressionwhich accesses the property IsActive copy Productpresented parameterExp (x.IsActive). Essentially this represents the expression x => x.IsActive.

ConstantExpression constantExp = Expression.Constant(productSearch.IsActive.Value);

Created ConstantExpression with meaning productSearch.IsActive. This is the value with which the comparison will be made (true or false).

BinaryExpression binaryExp = Expression.Equal(memberExp, constantExp);

Created BinaryExpressionwhich compares the property IsActive with a given value. This represents the expression x.IsActive == productSearch.IsActive.

predicate = Expression.AndAlso(predicate, binaryExp);

The current predicate (which started with true) is combined with a new condition (x.IsActive == productSearch.IsActive) using a logical operation AND. This results in an expression that can be used to filter products by their activity status.

Overall, the above code dynamically builds a tree of expressions that will ultimately be used to filter products based on whether they are active or not. Primordial predicate (true) allows you to easily add additional conditions without special processing for the first condition. If productSearch.IsActive specified, a condition is added to check if the property matches IsActive product to the specified value (true or false).

Then the variable lambdaExp assigned a lambda expression that represents the filtering function for the entities Product. This lambda expression is created from a predicate built earlier, which can contain conditions such as checking whether the product is active (IsActive). Call Expression.Lambda<Func<Product, bool>> generates Func<Product, bool>that is, a function that takes a product as an input parameter and returns a Boolean value that determines whether the product meets the filtering criteria.

This lambda expression is then passed to the method Where DbSet Products V dbContext. Method Where applies this filter to product records in the database. It creates a query that retrieves only those products that match the conditions defined in the lambda expression.

Finally, the method ToListAsync() Asynchronously executes a query and retrieves matching products as a list. This list is then returned as a 200 OK HTTP response using Results.Ok(data). The result is a filtered list of products sent back as an API response.

To test, simply run the application and submit the following GET-request with body via Postman:

expression tree with isActive in C#

expression tree with isActive in C#

This approach is useful when building queries dynamically because it allows you to add conditions based on the filters provided.

This is what your expression will look like after compiling the expression tree:

{x => (True AndAlso (x.IsActive == True))}

So far we have implemented the simplest property, which has two values: true or false. But what about other properties like categories, names, price etc? Users can select a product not only by activity, but, for example, by its category. We allow users to specify multiple categories at once, so we implemented this as an array in our class ProductSearchCategory.

csharpCopy codeif (productSearch.Categories is not null && productSearch.Categories.Any())
{
    //x.Category
    MemberExpression memberExp = Expression.Property(parameterExp, nameof(Product.Category));
    Expression orExpression = Expression.Constant(false);
    foreach (var category in productSearch.Categories)
    {
        var constExp = Expression.Constant(category.Name);
        BinaryExpression binaryExp = Expression.Equal(memberExp, constExp);
        orExpression = Expression.OrElse(orExpression, binaryExp);
    }
    predicate = Expression.AndAlso(predicate, orExpression);
}

The code adds dynamic filtering by product categories. First it checks to see if they are equal Categories in the object productSearch null and whether they contain elements. If yes, then a dynamic expression is built to filter products by category.

Starts with property access Category class Product through expression. This expression represents x => x.CategoryWhere x – this is an instance Product.

Original expression orExpression installed in false. This will be the basis for dynamic comparison of categories. Uses a loop to iterate through each category in productSearch.Categories. For each category, a constant expression is created with the name of the category and a binary expression that tests whether the product category is equal to the specified category.

The binary expressions are then combined using OrElsewhich means that if the product matches any of the provided categories, the condition becomes true. After processing all categories, the combined expression orExpression is added to the main predicate with AndAlso. This means that the main predicate will now check both the previous conditions and whether the product category matches one of the categories in the search criteria.

This approach allows you to dynamically filter products across multiple categories and integrates category filtering into an existing predicate.

At the end of the above code, you will get a LINQ expression, which is a lambda function used to filter products based on dynamic conditions. This expression can be converted into a predicate for use in a LINQ query that can be applied to your ProductDbContext or anyone IQueryable<Product>.

The LINQ expression in this case will be a combination of logical operations (AND and OR) that filter products. In pseudocode it would look like this:

products.Where(x => (x.Category == "Category1" || x.Category == "Category2" || ...) && другие условия)

If the user specifies and isActiveand categories, then we get the following lambda expression:

csharpCopy code{x => ((True AndAlso (x.IsActive == True)) AndAlso (((False OrElse (x.Category == "TV"))))}

To test, simply run the application and submit the following GET-request with body via Postman:

expression trees in C# for categories

expression trees in C# for categories

We use the same approach for the field Names. Here's our code snippet:

if (productSearch.Names is not null && productSearch.Names.Any())
{
    //x.Name
    MemberExpression memberExp = Expression.Property(parameterExp, nameof(Product.Name));
    Expression orExpression = Expression.Constant(false);
    foreach (var productName in productSearch.Names)
    {
        var constExp = Expression.Constant(productName.Name);
        BinaryExpression binaryExp = Expression.Equal(memberExp, constExp);
        orExpression = Expression.OrElse(orExpression, binaryExp);
    }
    predicate = Expression.AndAlso(predicate, orExpression);
}

This code snippet dynamically creates a filter condition for product names using expression trees. First it checks that the property productSearch.Names not equal null and contains elements. If the names of the products to be filtered are present, then the construction of the expression for comparing the property continues Name essence Product.

Expression memberExp refers to a property Name product (similar to x.Name in lambda expression). The expression is initially created orExpressionwhich is set as false. This expression will be updated in a loop to accumulate comparisons for each name in the collection productSearch.Names.

Inside a loop for each name in the collection productSearch.Names a constant expression is created with the product name. A binary expression is then generated that checks whether the product name matches the current name from the search. A loop accumulates a series of conditions ORusing Expression.OrElsewhich creates a logical OR operation between the current orExpression and a new comparison.

After the loop completes, the resulting expression is orExpression is a chain of OR conditions where the product name must match one of the names in the collection productSearch.Names. This expression is combined with an existing predicate using Expression.AndAlsoensuring that the filter by name is applied along with any other conditions previously defined in the predicate.

Simply put, our code block dynamically builds a query filter that matches products by their name, allowing multiple possible names from the collection productSearch.Names.

If the user only specifies names(names) in the body of the request, we will get something like the following lambda expression:

{x => (True AndAlso (((False OrElse (x.Name == "LG")) OrElse (x.Name == "LG2")) OrElse (x.Name == "Samsung")))}

If we get all the filtering options like isActivecategories(categories) and names(names) from the request body, we will end up with the following lambda expression:

{x => (((True AndAlso (x.IsActive == True)) AndAlso (((False OrElse (x.Category == "TV")) OrElse (x.Category == "Some Other")) OrElse (x.Category == "Mobile"))) AndAlso (((False OrElse (x.Name == "LG")) OrElse (x.Name == "LG2")) OrElse (x.Name == "Samsung")))}

This is what it will look like when you run the application and submit the request:

expression trees in C# with names

expression trees in C# with names

The last argument for our dynamic filtering is price(price). This is a complex object consisting of minimum(min) And maximum(max) values. The user should be able to specify either, both, or neither. That's why we made these parameters nullable.

This is what our code implementation looks like:

if (productSearch.Price is not null)
{
    //x.Price 400
    MemberExpression memberExp = Expression.Property(parameterExp, nameof(Product.Price));
    //x.Price >= min
    if (productSearch.Price.Min is not null)
    {
        var constExp = Expression.Constant(productSearch.Price.Min);
        var binaryExp = Expression.GreaterThanOrEqual(memberExp, constExp);
        predicate = Expression.AndAlso(predicate, binaryExp);
    }
    //(x.Price >= min && x.Price <= max)
    if (productSearch.Price.Max is not null)
    {
        var constExp = Expression.Constant(productSearch.Price.Max);
        var binaryExp = Expression.LessThanOrEqual(memberExp, constExp);
        predicate = Expression.AndAlso(predicate, binaryExp);
    }
}

This code dynamically creates a predicate to filter products by price range using expression trees. It starts by checking that the object productSearch.Price not equal nullwhich indicates that a price filter has been applied.

Expression memberExp created to represent a property Price product (x.Price). This expression is used to compare the price of a product with the minimum and maximum values ​​specified in the object productSearch.Price.

If the minimum price is specified (productSearch.Price.Min not equal null), an expression is created that checks whether the product's price is greater than or equal to the minimum value. This condition is added to the general predicate using Expression.AndAlsowhich means the product must satisfy this condition to be included in the results.

Similarly, if the maximum price is specified (productSearch.Price.Max not equal null), another expression is created that checks whether the price of the product is less than or equal to the maximum value. This condition is also added to an existing predicate using Expression.AndAlsoensuring that both minimum and maximum price conditions apply.

In short, the code builds a predicate that filters products by a specified price range, ensuring that products have a price greater than or equal to the minimum (if specified) and less than or equal to the maximum (if specified).

If the user only specifies the price in the body of the request, we will get a lambda expression similar to the following:

{x => ((True AndAlso (x.Price >= 400)) AndAlso (x.Price <= 5000))}

If we get all the filtering options like isActive, categories, names And price from the request body, we will end up with the following lambda expression:

{x => (((((True AndAlso (x.IsActive == True)) AndAlso (((False OrElse (x.Category == "TV")) OrElse (x.Category == "Some Other")) OrElse (x.Category == "Mobile"))) AndAlso (((False OrElse (x.Name == "LG")) OrElse (x.Name == "LG2")) OrElse (x.Name == "Samsung"))) AndAlso (x.Price >= 400)) AndAlso (x.Price <= 5000))}

This is what it will look like when you run the application and submit the request:

expression trees in C# with price

expression trees in C# with price

By the way, do you want to see everything in practice with a detailed video? Then here is my video where I create everything from scratch and give a simple and clear explanation of each step

The same content where I explain everything in English. By the way, these videos are not translations. Everything is recorded from scratch for each video.

Graceful completion

This article serves as a hands-on continuation of the previous tutorial on expression trees in C#, with a focus on how they can actually be used within Web APIs on ASP.NET Core. She explores building dynamic filtering functionality using the Minimal API, Entity Framework Core (EF Core), and expression trees.

The project includes the creation of a product database with the ability to dynamically filter by product attributes such as IsActive, Category, Name And Price. The article highlights the use of expression trees to build flexible and dynamic queries without hard-coding specific filters.

Setup starts by using the ASP.NET Core Web API with an in-memory database for storage, although you can use other databases supported by EF Core. The article emphasizes using a minimal API instead of traditional controllers for simplicity and performance, and provides instructions for performing the necessary steps, including setting up a database context (DbContext) and data initialization.

One of the main features demonstrated in the article is how expression trees are used to build predicates dynamically. For example, when filtering by property IsActive the system checks whether the user has specified this filter and then dynamically creates a condition that compares the product's activity status to the value provided. The process is extended to include dynamic filtering on other properties such as Category, Name And Priceeach of which allows you to flexibly configure criteria for queries.

Using expression trees, the article shows how you can build complex and flexible queries without having to write a lot of hard-coded query methods. Example of filtering products by Name And Category demonstrates how logical conditions OR can be dynamically combined based on user input, resulting in concise and reusable query logic.

Additionally, price filtering is handled by checking both the minimum and maximum values ​​and dynamically adjusting the predicate to only include those products that are within the specified price range.

In conclusion, this article demonstrates the power of expression trees in creating dynamic and flexible queries in C# applications. It provides practical code examples that use expression trees to build web API queries on ASP.NET Core, offering a practical way to manage complex real-world scenarios such as filtering product databases based on various user inputs.

Want to go deeper?

I regularly share my experience at the senior level on my YouTube channels TuralSuleymaniTech in English and TuralSuleymaniTechRu in Russian, where I break down complex topics such as .NET, microservices, Apache Kafka, Javascript, software design, Node.js and much more, making them easy to understand. Join us and improve your skills!

Similar Posts

Leave a Reply

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