Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / Languages / C#

A Convenient Way of Filtering Objects with Objects in C#

3.26/5 (7 votes)
26 Jul 2024MIT4 min read 17.6K   146  
To introduce a free library to save some tedious work for writing tedious filtering logic
This article introduces a free library that helps .NET developers to easily filter C# objects with object instances, to save them the effort of writing tedious filtering code.

Introduction

During implementation of a web application, searching for data according to certain criteria is a common requirement, that developers provide a searching panel to let business users provide some input values, then the input value gets sent to the server side, and the input values will be used to filter data in the database, at last the filter data will be sent back to web application and response of the search request.

This article introduces a free C# library that may save developers efforts of implementing such filtering logics.

Background

For simple searching use cases, the search logic is always simple and similar, yet for every search, developers have to repeat the tedious and brainless coding to fill it in.

Take a most simplified example of searching for books: Each Book has a name property and a published year property.

C#
public class Book
{
    public string Name { get; set; } = null!;
    public int PublishedYear { get; set; }
}
C#
public class SearchForBookDto
{
    public string? Name { get; set; }
    public int? PublishedYear { get; set; }
}

To search for the book by either or both properties in the database, the code may be like the following:

C#
// searching logic is like this
IQueryable queryable = databaseContext.Set<Book>();
if (searchForBookDto.Name != null)
{
    queryable = queryable.Where(book => book.Name == searchForBookDto.Name);
}

if (searchForBookDto.PublishedYear != null)
{
    queryable = queryable.Where(book => book.PublishedYear == searchForBookDto.PublishedYear);
}

await queryable.ToListAsync();

It's easy to see in the use case that for each searching property, the logic is the same: if the property value is not null, apply it to the queryable. This is quite tedious if there are a lot of such search fields to fill in for, or there are a lot of such searching features to implement.

Basic Use Case

Oasis.DynamicFilter can help to simply the code to below:

C#
var expressionMaker = new FilterBuilder().Register<Book, SearchForBookDto>().Build();
await databaseContext.Set<Book>()
    .Where(expressionMaker.GetExpression<Book, SearchForBookDto>(searchForBookDto))
    .ToListAsync();

In the two statements:

The first statement is the configuring process, to initialize a filter builder, then register the filtering on Book with SearchForBookDto, then it builds the FilterBuilder instance into an instance of IFilter interface, which developers can use to generate expressions or functions for filtering.

FilterBuilder is a centralised class for developers to register all filtering pairs. Developers are expected to register all filtering cases with the same instance, and distribute the IFilter interface instance it builds into all code that need the feature.

In the second statement, calling of expressionMaker.GetExpression<Book, SearchForBookDto>(searchForBookDto) makes a Linq Expression according to value of searchForBookDto that filters Book instances. Book instances with Name and PublishedYear properties equal to properties of the same names from searchForBookDto will be returned.

Support for Custom Filtering Configuration

To provide more flexibility to developers, Oasis.DynamicFilter supports filtering entities with more complicated expressions, rather than only letting them visit direct properties. Check the following example:

C#
public sealed class Book
{
    public int PublishedYear { get; set; }

    public string Name { get; set; } = null!;

    public Author Author { get; set; } = null!;
}

public sealed class Author
{
    public int BirthYear { get; set; }

    public string Name { get; set; } = null!;
}

public sealed class AuthorFilter
{
    public string? AuthorName { get; set; }

    public int? Age { get; set; }
}

This time Book has an navigation property named Author, and to demonstrate the feature, the book searching use case becomes: search for all books that are written by author whose name contains string "John", and are published before the author went 40 years old.

The following code generates the expression for this purpose:

C#
var expressionMaker = new FilterBuilder()
    .Configure<Book, AuthorFilter>()
        .Filter(filter => book => book.Author.Name.Contains(filter.AuthorName!),
                filter => !string.IsNullOrEmpty(filter.AuthorName))
        .Filter(filter => book => book.PublishedYear - book.Author.BirthYear < filter.Age,
                filter => filter.Age.HasValue)
        .Finish()
.Build();
var filter = new AuthorFilter { Age = 40, AuthorName = "John" };
var exp = expressionMaker.GetExpression<Book, AuthorFilter>(filter);

The .Filter method has 2 input parameters. The first parameter is the filtering method according to which Book will be filtered; the second parameter is the condition to apply the filtering method. In the example above, if filter.AuthorName is an empty string, the filtering method that author name of the book must contain the author name value will not be applied; if filter.Age is null, the filtering method that the book must be pushlished before the author reaches a certain age won't be applied. The second parameter is null if not passed, in this case the filtering method will be applied no matter what value filter contains.

Allowing custom filtering configuration adds a lot usability to Oasis.DynamicFilter. Combined with the basic filtering condition auto-generation feature, this library becomes a powerful centralized filtering condition registry which allows any filtering condition, and helps developers to save development efforts in coding trivial filtering rules.

Summary

For a quick demonstration of the use cases mentioned, please download and check the sample code attached. Oasis.DynamicFilter is implemented in .NET standard 2.1, and the downloadable sample code is a Xunit test library in .NET 6. Sqlite is used in it to prove that it works well with Linq to SQL.

To find more details of the library, please visit its GitHub repository.

Should there be any inquiry or suggestion, please leave a comment here or submit a bug under the repository.

History

  • 22nd October, 2023: Initial submission
  • 28th October, 2023: Package version updated to 0.2.1
  • 14th July, 2024: Package version updated to 0.3.0
  • 27th July 2024: Pakcage version updated to 0.3.1

License

This article, along with any associated source code and files, is licensed under The MIT License