理解 C# 中的 IQueryable<T>
你好!👋🏻
你可能用过,如果你之前用过 C# 写过代码,IEnumerable<T>
那肯定也用过。这两个函数非常流行,而且在你学习这门语言的初期就会接触到。但是你听说过吗?这个函数稍微高级一些,但它的功能非常强大,你应该了解一下,这样才能充分利用它。 女士们先生们,我们将踏上一段揭秘界面的旅程,所以……来杯咖啡,我们开始吧!☕️List<T>
IQueryable<T>
IQueryable
什么IQuyerable<T>
?🤔
该IQueryable
接口是 LINQ 的基石,而 LINQ 是 C# 最强大的功能之一。该接口专为从实现 的各种数据源IQueryable<T>
(例如 SQL 数据库或类似 的内存集合)查询数据而设计List<T>
。IQueryable
其独特之处在于其强大的功能,使其在数据查询方面功能多样且高效。
首先让我们来重点介绍一下这个界面的特性,正是这些特性赋予了它特殊性和独特性。
1:延迟执行🦥
延迟执行恰好是 的一个特性IEnumerable
,也被 继承IQueryable
。简单来说,就是将查询的执行延迟到真正需要数据的时候。还没明白?继续阅读。
让我们看一下这个基本的代码片段,以更实际的方式了解延迟执行的含义:
List<FamousPerson> famousPeople =
[
new FamousPerson(1, "Sandy Cheeks", false),
new FamousPerson(2, "Tony Stark", true),
new FamousPerson(3, "Captain Marvel", true),
new FamousPerson(4, "Captain America", true),
new FamousPerson(5, "SpongeBob SquarePants", false),
new FamousPerson(6, "Hulk", false)
];
IQueryable<FamousPerson> famousAndCanFly = famousPeople
.AsQueryable()
.Where(x => x.CanFly);
foreach (var fp in famousAndCanFly)
{
Console.WriteLine($"{fp.Name} can FLY!");
}
record FamousPerson(int Id, string Name, bool CanFly);
我希望你复制代码,并使用调试器查看 的值的famousAndCanFly
计算结果。你可能认为它会是一个包含 3 个人的集合,但实际上并非如此。你会看到该值不包含任何数据,但一旦你进入循环foreach
,结果就会被执行。这就是延迟执行的含义,执行被延迟到实际需要数据时(即在循环内枚举查询结果foreach
)。
2:表达式树🌳
我已经将一些额外的查询过滤器链接到前一个查询,所以现在它看起来像这样:
IQueryable<FamousPerson> famousAndCanFly = famousPeople
.AsQueryable()
.Where(x => x.CanFly);
famousAndCanFly = famousAndCanFly
.Where(x => x.Id < 3);
famousAndCanFly = famousAndCanFly.
Where(x => x.Name.Contains("s", StringComparison.OrdinalIgnoreCase));
Console.WriteLine(famousAndCanFly.Expression);
这里只是基本的 LINQ 扩展方法调用,但需要注意的是,最后一行的控制台日志。Expression
的属性是什么IQueryable
?它本质上是一个表达式树,IQueryable
在你编写查询时会组合在一起。它允许数据源提供程序抓取查询表达式树,并将其转换为可用于该数据源的内容。因此,数据源提供程序必须提供 的实现IQueryable
。
如果您运行前面的代码,它会打印如下内容:System.Collections.Generic.List1[FamousPerson].Where(x => x.CanFly).Where(x => (x.Id < 3)).Where(x => x.Name.Contains("s", OrdinalIgnoreCase))
你看,它只是一堆链式查询表达式和 where 语句,在我们的例子中,数据源恰好是内存中的集合,并且由于它实现了IQueryable
,你可以打赌它知道如何正确地将其转换为内存集合可以理解的内容。
IQueryable
功能示例
我之前说过,这IQueryable
是命名空间的一部分System.Linq
,所有你能用 LINQ 处理其他集合的操作,都可以在这个接口上完成。通过使用Where
、OrderBy
以及Select
其他任何操作,你可以不断地链接方法调用,从而编写出你能想象到的最复杂的查询。你实际上只是受限于你对 LINQ 的了解程度。
var filtered = query.Where(x => x.Age > 30);
var orderedDesc = query.OrderByDescending(x => x.Name);
var projected = query.Select(x => new { x.Name, x.Age });
var firstOrDefault = query.FirstOrDefault();
var lastOrDefault = query.LastOrDefault();
var single = query.Single();
3:如此多的优化⚙️
由于查询被构造为表达式树,因此提供程序(例如Entity Framework Core)可以采用该表达式树并将其转换为适合数据存储的查询语言,例如SQL Server或PostgreSQL的SQL,或内存数据存储的LINQ 。
由于转换由提供程序处理,因此它可以优化查询以获得更好的性能和效率。这种优化可能包括将查询转换为更高效的 SQL 语句、应用索引或其他特定于数据库的优化。这使您可以高效地查询数据存储,而无需手动优化每个查询。
扩展 IQueryable
我正在为一个博客项目编写一个存储库,我想在方法中添加排序、分页和按标题筛选的功能GetLatestPosts
。虽然可以将它们塞进同一个方法中,但如果能将这些方法集中到一起,然后链式调用它们就能组成完美的查询,那就更好了。这就是扩展方法!
⚠️ 扩展方法并不是 IQueryable 独有的,它们可以用于扩展任何类型。
在向您展示扩展方法代码之前,我想向您展示重构的代码,以及改进版本的实际样子:
public async Task<PaginatedReadOnlyCollection<Post>>
GetLatestPostsAsync(int pageNumber, int pageSize, string? title, PostSortOption sortOption, CancellationToken cancellationToken)
{
try
{
var query = db.Posts.AsQueryable();
var filteredQuery = query.ApplyFilter(title);
var sortedQuery = filteredQuery.ApplySorting(sortOption);
var totalCount = await filteredQuery.CountAsync(cancellationToken);
var posts = await sortedQuery
.ApplyPagination(pageNumber, pageSize)
.Execute(cancellationToken);
var paginatedPosts = new PaginatedReadOnlyCollection<Post>(
totalCount,
pageNumber,
pageSize,
posts.AsReadOnly()
);
return paginatedPosts;
}
catch (OperationCanceledException)
{
logger.LogInformation("Loading posts was cancelled");
return PaginatedReadOnlyCollection<Post>.Empty(pageNumber, pageSize);
}
}
我知道有很多代码,但这里主要关注的是 4 种非 LINQ 方法。ApplyFilter(string)
,,,和。ApplySorting(PostSortOption)
ApplyPagination(int, int)
Execute(CancellationToken)
所有这些方法都是我编写的扩展方法,用于扩展IQueryable<Post>
,这使得在数据模型上编写和组成大型查询变得更加清晰和简洁Post
。
PostsQueryExtensions.cs
下面是承载我们刚刚讨论过的扩展方法的代码:
public static class PostsQueryExtensions
{
public static IQueryable<Post> ApplyFilter(this IQueryable<Post> query, string? title)
{
if (!string.IsNullOrEmpty(title))
{
query = query.Where(post => post.Title.Contains(title, StringComparison.OrdinalIgnoreCase));
}
return query;
}
public static IOrderedQueryable<Post> ApplySorting(this IQueryable<Post> query, PostSortOption sortOption)
{
return sortOption switch
{
PostSortOption.MostComments => query.OrderByDescending(p => p.Comments.Count),
PostSortOption.MostLiked => query.OrderByDescending(p => p.LikeCount),
PostSortOption.MostViews => query.OrderByDescending(p => p.Views),
_ => query.OrderByDescending(p => p.PublishedOn)
};
}
public static IQueryable<Post> ApplyPagination(this IQueryable<Post> query, int pageNumber, int pageSize)
{
return query
.Skip((pageNumber - 1) * pageSize)
.Take(pageSize);
}
public static async Task<List<Post>> Execute(this IQueryable<Post> query, CancellationToken cancellationToken)
=> await query.ToListAsync(cancellationToken);
}
如您所见,所有这些方法都扩展了查询并返回了它的更新版本,本质上,在Execute
方法最终触发之前组成了整个查询,然后调用ToListAsync
它,获取整个查询的结果并枚举它们,以便调用代码可以读取和显示结果。
结论
在本文中,我们IQuyerable
从基础开始讲解了该接口,重点介绍了其主要功能,并展示了其使用示例代码,最后还介绍了如何针对特定数据模型扩展该接口,从而简化并减少复杂查询代码的冗余,使其更加流畅简洁。
希望本文能够很好地介绍这个强大的接口,希望它对您有所帮助,并让您有所收获!
如果您对帖子中提供的代码有任何反馈,请随时指出!