Click here to Skip to main content
15,887,683 members
Articles / Programming Languages / C#
Tip/Trick

A Convenient Way of Filtering Objects with Objects in C#

Rate me:
Please Sign up or sign in to vote.
4.33/5 (3 votes)
27 Oct 2023MIT3 min read 9.8K   73   5   2
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 More Complicated Expressions

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>()
        .FilterByStringProperty(b => b.Author.Name, StringOperator.Contains, f => f.AuthorName)
        .FilterByProperty(b => b.PublishedYear - b.Author.BirthYear, Operator.LessThan, f => f.Age)
        .Finish()
.Build();
var filter = new AuthorFilter { Age = 40, AuthorName = "John" };
var exp = expressionMaker.GetExpression<Book, AuthorFilter>(filter);

It's apparent that expressions b => b.Author.Name and b => b.PublishedYear - b.Author.BirthYear are more expressive than simply visiting Name or PublishedYear of Book, plus in this case, the comparison operators become string contains and integer less than, instead of equal. These features greatly enrich the options for developering to filter entities.

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.

Oasis.DynamicFilter actually provides more configurable filtering options to developers so filtering with conditions like not equal, greater than, value type in collection or array, collection or array contains value type are possible. It also supports Contains, StartsWith and EndsWith for string types. To find more details, 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 update to 0.2.1

License

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


Written By
Singapore Singapore
This member has not yet provided a Biography. Assume it's interesting and varied, and probably something to do with programming.

Comments and Discussions

 
QuestionQuestionable use of null type for Name Pin
Daniele Rota Nodari22-Oct-23 23:06
Daniele Rota Nodari22-Oct-23 23:06 
AnswerRe: Questionable use of null type for Name Pin
David_Cui22-Oct-23 23:16
David_Cui22-Oct-23 23:16 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.