C# 中使用对象筛选对象的便捷方法
介绍一个免费库,以节省编写繁琐筛选逻辑的一些重复工作
引言
在实施 Web 应用程序期间,根据特定条件搜索数据是一个常见需求,开发人员需要提供一个搜索面板,让业务用户提供一些输入值,然后将输入值发送到服务器端,并使用这些输入值过滤数据库中的数据,最后将过滤后的数据发送回 Web 应用程序,作为搜索请求的响应。
本文介绍一个免费的 C# 库,它可以节省开发人员实现此类过滤逻辑的精力。
背景
对于简单的搜索用例,搜索逻辑总是简单且相似的,然而对于每次搜索,开发人员都必须重复繁琐且毫无意义的编码来填充它。
以一个最简化的书籍搜索示例为例:每本 Book
都有一个名称属性和一个出版年份属性。
public class Book
{
public string Name { get; set; } = null!;
public int PublishedYear { get; set; }
}
public class SearchForBookDto
{
public string? Name { get; set; }
public int? PublishedYear { get; set; }
}
为了根据数据库中的任一或两个属性搜索书籍,代码可能如下所示
// 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();
很容易在用例中看到,对于每个搜索属性,逻辑都是相同的:如果属性值不为 null
,则将其应用于可查询对象。如果需要填充的搜索字段很多,或者需要实现的搜索功能很多,这将非常繁琐。
基本用例
Oasis.DynamicFilter
可以帮助简化代码到以下
var expressionMaker = new FilterBuilder().Register<Book, SearchForBookDto>().Build();
await databaseContext.Set<Book>()
.Where(expressionMaker.GetExpression<Book, SearchForBookDto>(searchForBookDto))
.ToListAsync();
在两个语句中
第一个语句是配置过程,用于初始化过滤器构建器,然后使用 SearchForBookDto
注册对 Book
的过滤,然后将 FilterBuilder
实例构建为 IFilter
接口的实例,开发人员可以使用它来生成用于过滤的表达式或函数。
FilterBuilder
是一个集中式类,供开发人员注册所有过滤对。开发人员需要使用同一个实例注册所有过滤用例,并将它构建的 IFilter
接口实例分发到所有需要该功能的代码中。
在第二个语句中,调用 expressionMaker.GetExpression<Book, SearchForBookDto>(searchForBookDto)
会根据 searchForBookDto
的值生成一个 Linq Expression
,用于过滤 Book
实例。将返回 Name
和 PublishedYear
属性与 searchForBookDto
的同名属性相等的 Book
实例。
支持自定义过滤配置
为了向开发人员提供更大的灵活性,Oasis.DynamicFilter
支持使用更复杂的表达式过滤实体,而不仅仅是让它们访问直接属性。请查看以下示例
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; }
}
这次 Book
有一个名为 Author
的导航属性,为了演示该功能,书籍搜索用例变为:搜索所有作者名称包含字符串 "John
" 并且在作者 40
岁之前出版的书籍。
以下代码为此目的生成表达式
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);
.Filter
方法有两个输入参数。第一个参数是根据该参数过滤 Book
的过滤方法;第二个参数是应用过滤方法的条件。在上面的示例中,如果 filter.AuthorName
是一个空字符串,则将不应用书籍作者姓名必须包含作者姓名值的过滤方法;如果 filter.Age
为 null,则将不应用书籍必须在作者达到一定年龄之前出版的过滤方法。如果未传递第二个参数,则为 null,在这种情况下,将应用过滤方法,无论 filter 包含什么值。
允许自定义过滤配置为 Oasis.DynamicFilter
增加了许多可用性。结合基本的过滤条件自动生成功能,该库成为一个强大的集中式过滤条件注册中心,允许任何过滤条件,并帮助开发人员节省编写琐碎过滤规则的开发工作。
摘要
为了快速演示所提及的用例,请下载并查看附带的示例代码。Oasis.DynamicFilter
在 .NET standard 2.1 中实现,可下载的示例代码是 .NET 6 中的 Xunit 测试库。它在其中使用了 Sqlite,以证明它与 Linq to SQL 配合良好。
要查找该库的更多详细信息,请访问其 GitHub 存储库。
如有任何疑问或建议,请在此处发表评论或在存储库下提交错误。
历史
- 2023 年 10 月 22 日:初始提交
- 2023 年 10 月 28 日:包版本更新为 0.2.1
- 2024 年 7 月 14 日:包版本更新为 0.3.0
- 2024 年 7 月 27 日:包版本更新为 0.3.1