65.9K
CodeProject 正在变化。 阅读更多。
Home

C# 中使用对象筛选对象的便捷方法

starIconstarIconstarIcon
emptyStarIcon
starIcon
emptyStarIcon

3.26/5 (7投票s)

2023年10月22日

MIT

4分钟阅读

viewsIcon

18078

downloadIcon

174

介绍一个免费库,以节省编写繁琐筛选逻辑的一些重复工作

引言

在实施 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 实例。将返回 NamePublishedYear 属性与 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
© . All rights reserved.