XIKEW.COM - 实用教程 - C# 8.0&.NET Core 3.0 LINQ的使用 - 实用教程,C# 8.0, .NET Core 3.0,Modern Cross-Platform Development, Chapter 12 , Querying and Manipulating Data Using LINQ, LINQ - 关于《C# 8.0 And .NET Core 3.0 Modern Cross-Platform Development》第十二章单元 使用LINQ查询和操作数据

C# 8.0&.NET Core 3.0 LINQ的使用
NETCORE 10/21/2020 9:48:55 PM 阅读:78

img

关于《C# 8.0 And .NET Core 3.0 Modern Cross-Platform Development》第十二章单元 使用LINQ查询和操作数据 关键字:C# 8.0, .NET Core 3.0,Modern Cross-Platform Development, Chapter 12 , Querying and Manipulating Data Using LINQ, LINQ

编写LINQ查询

LINQ具有以下几个部分,有些是必需的有些是可选的

扩展方法(必需的)

这些方法包括Where、OrderBy和Select等示例。它们提供了LINQ的功能。

LINQ提供程序(必需的)

这些包括LINQ to Objects, LINQ to Entities, LINQ to XML, LINQ to OData, and LINQ to Amazon。它们可以将标准LINQ操作转换为针对不同类型数据的特定命令。

Lambda表达式(可选)

这些可以用来代替命名方法来简化LINQ扩展方法调用。

LINQ查询理解语法(可选)

这些包括from,in,where,orderby,降序和select。 这些是C#关键字,它们是某些LINQ扩展方法的别名,它们的使用可以简化您编写的查询,尤其是如果您已经对其他查询语言(例如结构化查询语言SQL)有经验的话。 接触LINQ的程序员通常会认为LINQ查询理解语法就是LINQ的编写方式,但出乎意外的是这只是LINQ的可选部分之一

使用可枚举类扩展序列

LINQ扩展方法,如Where和Select,由可枚举静态类附加到任何类型(称为序列),该序列实现IEnumerable<T>。 例如,任何类型的数组都实现了IEnumerable<T>类,其中T是数组中项的类型,因此所有数组都支持LINQ查询和操作它们。 所有的泛型集合,如List<T>Dictionary<TKey, TValue>Stack<T>,和Queue<T>,都实现了IEnumerable<T>,这样它们就可以被LINQ查询和操作。

Enumerable定义了45个以上的扩展方法,如下表所示

| 方法 | 说明 | | --- | --- | | First , FirstOrDefault , Last , LastOrDefault | 获取序列中的第一项或最后一项,或返回类型的默认值,例如,如果没有第一项或最后一项,则为int返回0,为引用类型返回null | | Where | 返回与指定筛选器匹配的项序列 | | Single , SingleOrDefault | 返回与特定筛选器匹配的项,或抛出异常,或返回该类型的默认值(如果没有确切的匹配项) | | ElementAt , ElementAtOrDefault | 返回位于指定索引位置的项,或抛出异常,或返回该类型的默认值(如果在该位置没有项)| | Select , SelectMany | 将序列的每个元素投影到 IEnumerable<TResult> 并将结果序列合并为一个序列 | | OrderBy , OrderByDescending , ThenBy , ThenByDescending | 按指定属性对项排序 | | Reverse | 顺序反转 | | GroupBy , GroupJoin , Join | 对序列进行分组和连接 | | Skip , SkipWhile | 跳过多个项目或在表达式为真时跳过 | | Take , TakeWhile | 接受多个项目或在表达式为真时进行 | | Aggregate , Average , Count , LongCount , Max , Min , Sum | 计算聚合值 | | All , Any , Contains | 如果所有或任何项与筛选器匹配,或者序列包含指定项,则返回true | | Cast | 将项转换为指定类型 | | OfType | 删除与指定类型不匹配的项 | | Except , Intersect , Union | 执行返回集合的操作。集合不能有重复的项。尽管这些方法的输入可以是任意序列,因此可以有重复,但结果总是一个集合 | | Append , Concat , Prepend | 执行合并操作序列 | | Zip | 根据项的位置执行匹配操作 | | Distinct | 从序列中删除重复的项 | | ToArray , ToList , ToDictionary , ToLookup | 将序列转换为数组或集合 |

使用Where过滤实体

使用LINQ的最常见原因是使用Where扩展方法来过滤序列中的项目。 让我们通过定义名称序列然后在其上应用LINQ操作来探索过滤.

  • 在Code文件夹中,创建一个名为Chapter12的文件夹,其中包含一个名为LinqWithObjects的子文件夹。
  • 在Visual Studio Code中,将工作区另存为Chapter12文件夹中的Chapter12.code-workspace
  • 将名为LinqWithObjects的文件夹添加到工作区中
  • 导航至 Terminal | New Terminal
  • 在终端中,输入以下命令
dotnet new console
  • 在Program.cs中,添加一个LinqWithArrayOfStrings方法,它定义了一个字符串值数组,然后尝试调用其中的Where extension方法,如下面的代码所示
static void LinqWithArrayOfStrings()
{
    var names = new string[] { "Michael", "Pam", "Jim", "Dwight", "Angela", "Kevin", "Toby", "Creed" };
    var query = names.
}
  • 在键入Where方法时,请注意,字符串数组成员的IntelliSense列表中没有它,如下面的屏幕截图所示 img 这是因为Where是一个扩展方法。它不存在于数组类型上。要使Where extension方法可用,我们必须导入系统。Linq命名空间。

  • 将下面的语句添加到Program.cs文件的顶部

using System.Linq;
  • 重新键入Where方法,注意IntelliSense列表显示了更多的方法,包括由Enumerable类添加的扩展方法,如下面的屏幕截图所示 img

  • 当你为Where方法输入圆括号时,IntelliSense告诉我们要调用Where,我们必须传入一个Func<string, bool>委托的实例,如下面的屏幕截图所示 img

  • 输入一个表达式来创建Func<string、bool>委托的新实例,现在请注意,我们还没有提供方法名,因为我们将在下一步中定义它,如下面的代码所示

var query = names.Where(new Func<string, bool>());

Func<string, bool>委托告诉我们,对于传递给该方法的每个字符串变量,该方法必须返回一个bool值。如果该方法返回true,则表示应该将该字符串包含在结果中,如果该方法返回false,则表示应该将其排除。

针对命名方法

让我们定义一个只包含超过四个字符的名称的方法。

  • 向程序添加一个方法,如下面的代码所示
static bool NameLongerThanFour(string name)
{
    return name.Length > 4;
}
  • 回到LinqWithArrayOfStrings方法中,将方法名称传递到Func<string, bool>委托中,然后循环查询项,如下面的代码所示
var query = names.Where(new Func<string, bool>(NameLongerThanFour));

foreach (string item in query)
{
    WriteLine(item);
}
  • 在Main中,调用LinqWithArrayOfStrings方法,运行控制台应用程序,并查看结果,注意只列出长度超过四个字母的名称,如下面的输出所示
Michael
Dwight
Angela
Kevin
Creed

通过删除显式委托实例化简化代码

我们可以通过删除Func<string, bool>委托的显式实例化来简化代码,因为c#编译器可以为我们实例化该委托 要通过查看逐步改进的代码来帮助您学习,请复制和粘贴查询。

  • 注释掉第一个示例,如下面的代码所示
// var query = names.Where(new Func<string, bool>(NameLongerThanFour));
  • 修改该副本以删除委托的显式实例化,如下面的代码所示
var query = names.Where(NameLongerThanFour);
  • 重新运行应用程序并注意它具有相同的行为

针对lambda表达式

我们可以使用lambda表达式来代替命名方法来进一步简化代码。 尽管lambda表达式一开始看起来很复杂,但它只是一个无名的函数。它使用 => (读作“goes to”) 符号来表示返回值。

  • 复制和粘贴查询,注释第二个示例,并修改查询,如下面的代码所示
var query = names.Where(name => name.Length > 4);

请注意,lambda表达式重要部分的语法包括 name => name.Length > 4,但仅此而已。lambda表达式只需要定义以下内容

  1. 输入参数的名称。
  2. 一个返回值表达式
  3. 输入参数的名称类型是从以下事实推断出来的:该序列包含字符串值,并且返回类型必须是 Where要求的布尔值,因此=>符号后面的表达式必须返回布尔值

编译器为我们做了大部分工作,所以我们的代码可以尽可能简洁

  • 重新运行应用程序并注意它具有相同的行为

实体排序

其他常用的扩展方法是OrderBy和ThenBy,它们用于排序序列。 扩展方法支持链式操作,只要前面的方法返回的是另一个序列,即实现IEnumerable<T>接口的类型。

使用OrderBy按单个属性排序

让我们继续使用之前的项目来探索排序。

  • 将对OrderBy的调用附加到现有查询的末尾,如下面的代码所示
var query = names
        .Where(name => name.Length > 4)
        .OrderBy(name => name.Length);

格式化LINQ语句,以便每个扩展方法调用都发生在它自己的行上,从而使它们更容易阅读。

  • 重新运行应用程序并注意,名称现在按最短优先排序,如下面的输出所示
Kevin
Creed
Dwight
Angela
Michael

想要将最长的名称放在前面,可以使用 OrderByDescending

使用ThenBy做二次排序

我们可能希望按多个属性排序,例如,按照字母顺序排序相同长度的名称。

  • 在现有查询的末尾添加对ThenBy方法的调用,如以下代码中突出显示的那样:
var query = names
    .Where(name => name.Length > 4)
    .OrderBy(name => name.Length)
    .ThenBy(name => name);

重新运行应用程序并注意以下排序顺序中的细微差别。在一组相同长度的名称中,名称是按字符串的全部值按字母顺序排序的,因此Creed在Kevin之前,Angela在Dwight之前,如下面的输出所示

Creed
Kevin
Angela
Dwight
Michael

按类型过滤

Where 非常适合按值过滤,比如文本和数字。但是,如果序列包含多个类型,并且您希望按特定类型进行筛选并尊重任何继承层次结构,该怎么办呢?假设您有一组 Exception 序列,该序列具有复杂的层次结构,如下图所示: img

让我们研究按类型筛选

  • 在程序中,添加LinqWithArrayOfExceptions方法,该方法定义一个异常派生对象数组,如下面的代码所示
static void LinqWithArrayOfExceptions()
{
    var errors = new Exception[]
    {
        new ArgumentException(),
        new SystemException(),
        new IndexOutOfRangeException(),
        new InvalidOperationException(),
        new NullReferenceException(),
        new InvalidCastException(),
        new OverflowException(),
        new DivideByZeroException(),
        new ApplicationException()
    };
}
  • 使用 OfType<T> 扩展方法编写语句来过滤非算术异常,并将它们写入控制台,如下面的代码所示
var numberErrors = errors.OfType<ArithmeticException>();
foreach (var error in numberErrors)
{
    WriteLine(error);
}
  • 在主方法中,注释掉对LinqWithArrayOfStrings方法的调用,并添加对LinqWithArrayOfExceptions方法的调用。
System.OverflowException: Arithmetic operation resulted in an overflow.
System.DivideByZeroException: Attempted to divide by zero.

使用LINQ处理集合和包

集合是数学中最基本的概念之一。集合是一个或多个唯一对象的集合。多集或包是一个或多个具有重复的对象的集合。你可能还记得在学校学过维恩图。常见的集合操作包括集合之间的intersect或union。

让我们创建一个控制台应用程序,它将为学徒组定义三个字符串值数组,然后对它们执行一些常见的集合和多集合操作。

  • 创建一个名为LinqWithSets的新控制台应用程序项目,将其添加到本章的工作区中,并选择该项目作为OmniSharp的活动项目。
  • 导入以下附加命名空间:
using System.Collections.Generic; // for IEnumerable<T>
using System.Linq; // for LINQ extension methods

在Program中,在Main方法之前,添加以下方法,将任何字符串变量序列作为逗号分隔的单个字符串输出到控制台输出,并附带一个可选的描述

static void Output(IEnumerable<string> cohort,string description = "")
{
    if (!string.IsNullOrEmpty(description))
    {
        WriteLine(description);
    }
    Write(" ");
    WriteLine(string.Join(", ", cohort.ToArray()));
}
  • 在Main中,添加语句来定义三个名称数组,输出它们,然后对它们执行各种设置操作,如下面的代码所示
var cohort1 = new string[]{ "Rachel", "Gareth", "Jonathan", "George" };
var cohort2 = new string[]{ "Jack", "Stephen", "Daniel", "Jack", "Jared" };
var cohort3 = new string[]{ "Declan", "Jack", "Jack", "Jasmine", "Conor" };
Output(cohort1, "Cohort 1");
Output(cohort2, "Cohort 2");
Output(cohort3, "Cohort 3");
WriteLine();
Output(cohort2.Distinct(), "cohort2.Distinct():");
WriteLine();
Output(cohort2.Union(cohort3), "cohort2.Union(cohort3):");
WriteLine();
Output(cohort2.Concat(cohort3), "cohort2.Concat(cohort3):");
WriteLine();
Output(cohort2.Intersect(cohort3), "cohort2.Intersect(cohort3):");
WriteLine();
Output(cohort2.Except(cohort3), "cohort2.Except(cohort3):");
WriteLine();
Output(cohort1.Zip(cohort2,(c1, c2) => $"{c1} matched with {c2}"),"cohort1.Zip(cohort2):");
  • 运行控制台应用程序并查看结果,如下面的输出所示
Cohort 1
Rachel, Gareth, Jonathan, George
Cohort 2
Jack, Stephen, Daniel, Jack, Jared
Cohort 3
Declan, Jack, Jack, Jasmine, Conor
cohort2.Distinct():
Jack, Stephen, Daniel, Jared
cohort2.Union(cohort3):
Jack, Stephen, Daniel, Jared, Declan, Jasmine, Conor
cohort2.Concat(cohort3):
Jack, Stephen, Daniel, Jack, Jared, Declan, Jack, Jack, Jasmine, Conor
cohort2.Intersect(cohort3):
Jack
cohort2.Except(cohort3):
Stephen, Daniel, Jared
cohort1.Zip(cohort2):
Rachel matched with Jack, Gareth matched with Stephen, Jonathan matched with Daniel, George matched with Jack

使用Zip,如果两个序列中的项数不相等,则某些项将没有匹配的伙伴。没有合作伙伴的将不包括在结果中。

使用LINQ EF Core

为了学习投影,最好使用一些更复杂的序列,因此在下一个项目中,我们将使用Northwind样本数据库。

  • 创建一个名为LinqWithEFCore的新控制台应用程序项目,将其添加到本章的工作区中,并选择该项目作为OmniSharp的活动项目。
  • 修改LinqWithEFCore.csproj文件,如下面的标记中突出显示的那样
<Project Sdk="Microsoft.NET.Sdk">
    <PropertyGroup>
        <OutputType>Exe</OutputType>
        <TargetFramework>netcoreapp3.0</TargetFramework>
    </PropertyGroup>

    <ItemGroup>
        <PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="3.0.0" />
    </ItemGroup>
</Project>
  • 在Terminal中,下载引用的包并编译当前项目,如下面的命令所示
sqlite3 Northwind.db < Northwind.sql

如何创建Northwind数据库的详细说明在第11章,使用实体框架核心与数据库一起工作。

  • 让我们定义一个Entity Framework Core模型来表示我们将使用的数据库和表。 DbContext派生的类必须具有一个名为OnConfiguring的重写方法,并在该方法中设置数据库连接字符串。
  • 将三个类文件添加到名为Northwind.cs、Category.cs和Product.cs的项目中。
  • 修改名为Northwind.cs的类文件,如下面的代码所示
using Microsoft.EntityFrameworkCore;

namespace Packt.Shared
{
    // this manages the connection to the database
    public class Northwind : DbContext
    {
        // these properties map to tables in the database
        public DbSet<Category> Categories { get; set; }
        public DbSet<Product> Products { get; set; }
        protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
        {
            string path = System.IO.Path.Combine(System.Environment.CurrentDirectory, "Northwind.db");
            optionsBuilder.UseSqlite($"Filename={path}");
        }
    }
}
  • 修改名为Category.cs的类文件,如下面的代码所示
using System.ComponentModel.DataAnnotations;

namespace Packt.Shared
{
    public class Category
    {
        public int CategoryID { get; set; }
        [Required]
        [StringLength(15)]
        public string CategoryName { get; set; }
        public string Description { get; set; }
    }
}
  • 修改名为Product.cs的类文件,如下面的代码所示
using System.ComponentModel.DataAnnotations;

namespace Packt.Shared
{
    public class Product
    {
        public int ProductID { get; set; }
        
        [Required]
        [StringLength(40)]
        public string ProductName { get; set; }
        
        public int? SupplierID { get; set; }
        
        public int? CategoryID { get; set; }
        
        [StringLength(20)]
        public string QuantityPerUnit { get; set; }
        
        public decimal? UnitPrice { get; set; }
        
        public short? UnitsInStock { get; set; }
        
        public short? UnitsOnOrder { get; set; }
        
        public short? ReorderLevel { get; set; }
        
        public bool Discontinued { get; set; }
    }
}

我们还没有定义这两个实体类之间的关系。这是故意的。稍后,您将使用LINQ来连接这两个实体集。

筛选和排序序列

现在让我们编写语句来过滤和排序表中的行序列。

  • 打开Program.cs文件并导入以下类型和名称空间
using static System.Console;
using Packt.Shared;
using Microsoft.EntityFrameworkCore;
using System.Linq;
  • 创建筛选和排序产品的方法,如下面的代码所示
static void FilterAndSort()
{
    using (var db = new Northwind())
    {
        var query = db.Products
        .Where(product => product.UnitPrice < 10M)
        // IQueryable<Product>
        .OrderByDescending(product => product.UnitPrice);
        
        WriteLine("Products that cost less than $10:");
        
        foreach (var item in query)
        {
            WriteLine("{0}: {1} costs {2:$#,##0.00}", item.ProductID, item.ProductName, item.UnitPrice);
        }
        
        WriteLine();
    }
}

DbSet<T>实现接口IQueryable<T>,LINQ实现接口IEnumerable<T>,所以LINQ可以用来查询和操作为EF Core构建的模型中的实体集合。 您可能还注意到,序列实现了IQueryable<T>(或IOrderedQueryable<T>在对一个LINQ排序方法的调用后),而不是IEnumerable<T>IOrderedEnumerable<T>。 这表明我们正在使用LINQ提供程序,它使用表达式树在内存中构建查询。它们以树形数据结构表示代码,并允许创建动态查询,这对于为SQLite等外部数据提供者构建LINQ查询非常有用。

你可以在下面的链接中阅读更多关于表达式树的内容:https://docs.microsoft.com/en-us/dotnet/csharp/programming-guide/concepts/expression-trees/

LINQ查询将被转换为另一种查询语言,比如SQL。使用foreach枚举查询或调用ToArray等方法将强制执行查询。 在Main中,调用FilterAndSort方法。 运行控制台应用程序并查看结果,如下面的输出所示

Products that cost less than $10:

41: Jack's New England Clam Chowder costs $9.65
45: Rogede sild costs $9.50
47: Zaanse koeken costs $9.50
19: Teatime Chocolate Biscuits costs $9.20
23: Tunnbröd costs $9.00
75: Rhönbräu Klosterbier costs $7.75
54: Tourtière costs $7.45
52: Filo Mix costs $7.00
13: Konbu costs $6.00
24: Guaraná Fantástica costs $4.50
33: Geitost costs $2.50

尽管该查询输出了我们想要的信息,但效率很低,因为它从Products表中获取了所有列,而不仅仅是我们需要的三列,这相当于下面的SQL语句

SELECT * FROM Products;

在第11章,使用Entity Framework Core处理数据库中,您学习了如何记录对SQLite执行的SQL命令,以便自己查看。

序列投影到新的类型

在讨论投影之前,我们需要回顾一下对象初始化语法。如果您定义了一个类,那么您可以使用new、类名和花括号实例化一个对象,以设置字段和属性的初始值,如下面的代码所示

var alice = new Person
{
    Name = "Alice Jones",
    DateOfBirth = new DateTime(1998, 3, 7)
};

c# 3.0及以后版本允许实例化匿名类型的实例,如下面的代码所示

var anonymouslyTypedObject = new
{
    Name = "Alice Jones",
    DateOfBirth = new DateTime(1998, 3, 7)
};

虽然我们没有指定类型名,但是编译器可以从两个属性name和DateOfBirth的设置中推断出匿名类型。当编写LINQ查询来将现有类型投影到新类型中而无需显式定义新类型时,这种功能特别有用。由于该类型是匿名的,所以这只能用于var声明的局部变量。 让我们向Select方法添加一个调用,通过将Product类的实例投影到只有三个属性的新匿名类型的实例中,从而提高对数据库表执行SQL命令的效率。 在Main中,修改LINQ查询以使用Select方法只返回我们需要的三个属性(即表列),如下面的代码中突出显示的那样

var query = db.Products
                .Where(product => product.UnitPrice < 10M) // IQueryable<Product>
                .OrderByDescending(product => product.UnitPrice)
                // IOrderedQueryable<Product>
                .Select(product => new // anonymous type
                {
                    product.ProductID,
                    product.ProductName,
                    product.UnitPrice
                });

运行控制台应用程序并确认输出与前面相同。

连接和分组序列

有两种用于连接和分组的扩展方法

Join:此方法有四个参数:inner 要连接的序列、outerKeySelector 左侧序列上要匹配的属性或特性、innerKeySelector 右侧序列上要匹配的属性和特性,还有resultSelector 结果选择器 。 GroupJoin:此方法具有相同的参数,但它将匹配合并到一个group对象中,其中一个键属性用于匹配值,一个IEnumerable<T>类型用于多个匹配。 让我们在使用两个表时研究这些方法:categories和products。

  • 创建一个方法来选择类别和产品,联接它们并输出它们,如下面的代码所示
static void JoinCategoriesAndProducts()
{
    using (var db = new Northwind())
    {
        // join every product to its category to return 77 matches
        var queryJoin = db.Categories.Join(
                inner: db.Products,
                outerKeySelector: category => category.CategoryID,
                innerKeySelector: product => product.CategoryID,
                resultSelector: (c, p) =>
                new { c.CategoryName, p.ProductName, p.ProductID });
                
        foreach (var item in queryJoin)
        {
            WriteLine("{0}: {1} is in {2}.",
            arg0: item.ProductID,
            arg1: item.ProductName,
            arg2: item.CategoryName);
        }
    }
}

在连接中有两个序列,外部序列和内部序列。在前面的示例中,categories是外部序列,products是内部序列。

  • 在Main中,注释掉对FilterAndJoin的调用,并调用JoinCategoriesAndProducts。

  • 运行控制台应用程序并查看结果。注意,77种产品中的每一种都有一行输出,结果首先显示饮料类中的所有产品,然后是调味品类,依此类推,如下面的输出所示

1: Chai is in Beverages.
2: Chang is in Beverages.
24: Guaraná Fantástica is in Beverages.
34: Sasquatch Ale is in Beverages.
35: Steeleye Stout is in Beverages.
38: Côte de Blaye is in Beverages.
39: Chartreuse verte is in Beverages.
43: Ipoh Coffee is in Beverages.
67: Laughing Lumberjack Lager is in Beverages.
70: Outback Lager is in Beverages.
75: Rhönbräu Klosterbier is in Beverages.
76: Lakkalikööri is in Beverages.
3: Aniseed Syrup is in Condiments.
4: Chef Anton's Cajun Seasoning is in Condiments.
  • 在现有查询的末尾,调用OrderBy方法按ProductID进行排序,如下面的代码所示
.OrderBy(cp => cp.ProductID);
  • 重新运行应用程序并查看结果,如下面的输出所示(编辑为只包含前10项)
1: Chai is in Beverages.
2: Chang is in Beverages.
3: Aniseed Syrup is in Condiments.
4: Chef Anton's Cajun Seasoning is in Condiments.
5: Chef Anton's Gumbo Mix is in Condiments.
6: Grandma's Boysenberry Spread is in Condiments.
7: Uncle Bob's Organic Dried Pears is in Produce.
8: Northwoods Cranberry Sauce is in Condiments.
9: Mishi Kobe Niku is in Meat/Poultry.
10: Ikura is in Seafood.
  • 创建一个方法来分组和联接,显示组名,然后显示每个组中的所有项,如下面的代码所示
static void GroupJoinCategoriesAndProducts()
{
    using (var db = new Northwind())
    {
        // group all products by their category to return 8 matches
        var queryGroup = db.Categories.AsEnumerable().GroupJoin(
                    inner: db.Products,
                    outerKeySelector: category => category.CategoryID,
                    innerKeySelector: product => product.CategoryID,
                    resultSelector: (c, matchingProducts) => new {
                        c.CategoryName,
                        Products = matchingProducts.OrderBy(p => p.ProductName)
                    });
        foreach (var item in queryGroup)
        {
            WriteLine("{0} has {1} products.", arg0: item.CategoryName, arg1: item.Products.Count());
            foreach (var product in item.Products)
            {
                WriteLine($" {product.ProductName}");
            }
        }
    }
}

如果我们没有调用AsEnumerable方法,则会抛出一个运行时异常,如下面的输出所示

Unhandled exception. System.NotImplementedException: The method or operation is not implemented.
at Microsoft.EntityFrameworkCore.Relational.Query.Pipeline.RelationalQueryableMethodTranslatingExpressionVisitor.TranslateGroupJoin(ShapedQueryExpression outer, ShapedQueryExpression inner, LambdaExpression outerKeySelector, LambdaExpression innerKeySelector, LambdaExpression resultSelector)

这是因为并不是所有的LINQ扩展方法都可以从表达式树转换为其他查询语法,比如SQL。在这些情况下,我们可以通过调用AsEnumerable方法将IQueryable<T>转换为IEnumerable<T>,该方法强制查询处理使用LINQ to EF Core来将数据带入应用程序,然后使用LINQ to Object 来执行更复杂的内存处理。但是,这通常是低效的。

  • 在Main中,注释前面的方法调用并调用GroupJoinCategoriesAndProducts。

  • 重新运行控制台应用程序,查看结果,并注意每个类别中的产品已按名称排序,如查询中定义的那样,并显示在下面的部分输出中

Beverages has 12 products.
Chai
Chang
Chartreuse verte
Côte de Blaye
Guaraná Fantástica
Ipoh Coffee
Lakkalikööri
Laughing Lumberjack Lager
Outback Lager
Rhönbräu Klosterbier
Sasquatch Ale
Steeleye Stout
Condiments has 12 products.
Aniseed Syrup
Chef Anton's Cajun Seasoning
Chef Anton's Gumbo Mix

聚合序列

有LINQ扩展方法来执行聚合函数,比如平均和和。让我们编写一些代码,看看这些方法如何从Products表聚合信息。

创建一个方法来显示聚合扩展方法的使用,如下面的代码所示

static void AggregateProducts()
{
    using (var db = new Northwind())
    {
        WriteLine("{0,-25} {1,10}", arg0: "Product count:", arg1: db.Products.Count());        
        WriteLine("{0,-25} {1,10:$#,##0.00}", arg0: "Highest product price:", arg1: db.Products.Max(p => p.UnitPrice));
        WriteLine("{0,-25} {1,10:N0}", arg0: "Sum of units in stock:", arg1: db.Products.Sum(p => p.UnitsInStock));
        WriteLine("{0,-25} {1,10:N0}", arg0: "Sum of units on order:", arg1: db.Products.Sum(p => p.UnitsOnOrder));
        WriteLine("{0,-25} {1,10:$#,##0.00}", arg0: "Average unit price:", arg1: db.Products.Average(p => p.UnitPrice));
        WriteLine("{0,-25} {1,10:$#,##0.00}", arg0: "Value of units in stock:", arg1: db.Products.AsEnumerable().Sum(p => p.UnitPrice * p.UnitsInStock));
    }
}
  • 在Main中,注释前面的方法并调用AggregateProducts。

  • 运行控制台应用程序并查看结果,如下面的输出所示

Product count: 77
Highest product price: $263.50
Sum of units in stock: 3,119
Sum of units on order: 780
Average unit price: $28.87
Value of units in stock: $74,050.85

在Entity Framework Core 3.0及以后版本中,无法转换为SQL的LINQ操作不再在客户端自动计算,因此必须显式调用一个enumerable来强制客户端对查询进行进一步处理。

更多信息:您可以在下面的链接中了解更多关于这个破坏性更改的信息 https://docs.microsoft.com/en-us/ef/core/what-is-new/ef-core-3.0/breaking-changes-linq-queries-are-no-longer-evaluated-on-the-client

用语法糖来美化LINQ语法

c# 3.0在2008年引入了一些新的语言关键字,以便有SQL经验的程序员更容易编写LINQ查询。这种语法糖有时被称为LINQ查询理解语法。

更多信息:LINQ查询理解语法在功能上是有限的。它只提供了最常用的LINQ特性的c#关键字。您必须使用扩展方法来访问LINQ的所有特性。你可以在下面的链接中阅读更多关于它为什么被称为理解语法的内容 https://stackoverflow.com/questions/6229187/linq-why-is-it-called-comprehension-syntax

考虑以下字符串值数组

var names = new string[] { "Michael", "Pam", "Jim", "Dwight", "Angela", "Kevin", "Toby", "Creed" };

要过滤和排序名称,可以使用扩展方法和lambda表达式,如下面的代码所示

var query = names
    .Where(name => name.Length > 4)
    .OrderBy(name => name.Length)
    .ThenBy(name => name);

也可以使用查询理解语法获得相同的结果,如下面的代码所示

var query = from name in names where name.Length > 4 orderby name.Length, name select name;

编译器将查询理解语法更改为等效的扩展方法和lambda表达式。

对于LINQ查询理解语法,select关键字始终是必需的。在使用扩展方法和lambda表达式时,选择扩展方法是可选的,因为整个项是隐式选择的。

并不是所有的扩展方法都有等价的c#关键字,例如Skip和Take扩展方法,它们通常用于实现对大量数据的分页。

不能仅使用查询理解语法编写跳过和接受的查询,因此我们可以使用所有扩展方法编写查询,如下面的代码所示

var query = names
    .Where(name => name.Length > 4)
    .Skip(80)
    .Take(10);

或者,您可以将查询理解语法包装在括号中,然后切换到使用扩展方法,如下面的代码所示

var query = (from name in names where name.Length > 4 select name)
    .Skip(80)
    .Take(10);

学习使用lambda表达式的扩展方法和编写LINQ查询的查询理解语法方法,因为您可能需要维护同时使用这两种方法的代码。

使用并行LINQ的多个线程

默认情况下,只有一个线程用于执行LINQ查询。Parallel LINQ (PLINQ )是一种允许多个线程执行LINQ查询的简单方法。 不要假设使用并行线程会提高应用程序的性能。始终度量实际时间和资源使用情况。

创建一个受益于多线程的应用程序

为了看到它的实际应用,我们将从一些代码开始,这些代码只使用一个线程来对2亿个整数进行平方。我们将使用秒表类型来测量性能的变化。

我们将使用操作系统工具来监视CPU和CPU核心的使用情况。如果您没有多个cpu,或者至少没有多个核心,那么这个练习不会显示太多信息

  • 创建一个名为LinqInParallel的新控制台应用程序项目,将其添加到本章的工作区中,并选择该项目作为OmniSharp的活动项目。

  • 导入需要的命名空间 System.Diagnostics,以便我们可以使用 StopWatch 计时类型 System.Collections.Generic,以便我们可以使用IEnumerable<T>类型 System.Linq 我们可以使用LINQ; System.Console 控制台类型。

  • 向Main添加语句以创建秒表来记录计时,在启动计时器前等待按键,创建2亿个整数,对每个整数平方,停止计时器,并显示经过的毫秒,如下面的代码所示

var watch = Stopwatch.StartNew();
Write("Press ENTER to start: ");
ReadLine();
watch.Start();
IEnumerable<int> numbers = Enumerable.Range(1, 200_000_000);
var squares = numbers.Select(number => number * number).ToArray();
watch.Stop();
WriteLine("{0:#,##0} elapsed milliseconds.",watch.ElapsedMilliseconds);
  • 运行控制台应用程序,但还不要按Enter键启动。

Windows 10

如果您使用的是Windows 10,然后右键单击Windows开始按钮或按Ctrl + Alt + Delete,然后单击任务管理器。 在任务管理器窗口的底部,单击“详细信息”按钮。在任务管理器窗口的顶部,单击性能选项卡。 右键单击“ CPU利用率”图,选择“将图更改为”,然后选择“逻辑处理器”。

macOS

如果您正在使用macOS,那么启动活动监视器。 导航到查看| 更新频率| 经常(1秒)。 要查看CPU图表,请导航到窗口| CPU历史记录。

所有操作系统

重新排列任务管理器或CPU历史记录,或您的Linux工具和Visual Studio代码,使它们并列在一起。 等待cpu安定下来,然后按Enter启动秒表并运行查询。 结果应该是经过的毫秒数,如下面的输出和屏幕截图所示

Press ENTER to start.
173,689 elapsed milliseconds.

img 任务管理器或CPU历史窗口应该显示使用最多的一个或两个CPU。其他cpu可能同时执行后台任务,比如垃圾收集器,因此其他cpu或核心不会是完全扁平的,但工作肯定不会均匀地分布在所有可能的cpu或核心中。

在Main中,修改查询以调用AsParallel extension方法,如下面的代码所示

var squares = numbers.AsParallel().Select(number => number * number).ToArray();

再次运行应用程序。 等待任务管理器或CPU历史记录窗口安定下来,然后按Enter启动秒表并运行查询。这一次,应用程序完成的时间应该更短(尽管可能没有您希望的那么短,因为管理这些多个线程需要额外的努力!)

Press ENTER to start.
145,904 elapsed milliseconds.

任务管理器或CPU历史窗口应该显示所有CPU都被平等地用于执行LINQ查询,如下面的屏幕截图所示

img 您将在第13章“使用多任务处理提高性能和可伸缩性”中了解更多关于管理多线程的内容。

创建自己的LINQ扩展方法

在第6章“实现接口和继承类”中,您学习了如何创建自己的扩展方法。要创建LINQ扩展方法,您所要做的就是扩展IEnumerable<T>类型。 将自己的扩展方法放在单独的类库中,这样就可以轻松地将它们作为自己的程序集或NuGet包部署。

我们将以平均扩展法为例。任何一个学校的孩子都会告诉你,平均可能意味着三件事之一 平均值:把所有的数字加起来,然后除以总数。 模式:最常见的数量。 中位数:排序后在数字中间的数字。

微软实现的平均扩展方法计算平均值。我们可能想为模式和中值定义我们自己的扩展方法。

在LinqWithEFCore项目中,添加一个名为MyLinqExtensions.cs的新类文件。

修改类,如下面的代码所示

using System.Collections.Generic;
namespace System.Linq
{
    public static class MyLinqExtensions
    {
        // this is a chainable LINQ extension method
        public static IEnumerable<T> ProcessSequence<T>(this IEnumerable<T> sequence)
        {
            // you could do some processing here
            return sequence;
        }
        // these are scalar LINQ extension methods
        public static int? Median(this IEnumerable<int?> sequence)
        {
            var ordered = sequence.OrderBy(item => item);
            int middlePosition = ordered.Count() / 2;
            return ordered.ElementAt(middlePosition);
        }
        public static int? Median<T>(this IEnumerable<T> sequence, Func<T, int?> selector)
        {
            return sequence.Select(selector).Median();
        }
        public static decimal? Median(this IEnumerable<decimal?> sequence)
        {
            var ordered = sequence.OrderBy(item => item);
            int middlePosition = ordered.Count() / 2;
            return ordered.ElementAt(middlePosition);
        }
        public static decimal? Median<T>(this IEnumerable<T> sequence, Func<T, decimal?> selector)
        {
            return sequence.Select(selector).Median();
        }
        public static int? Mode(this IEnumerable<int?> sequence)
        {
            var grouped = sequence.GroupBy(item => item);
            var orderedGroups = grouped.OrderBy(group => group.Count());
            return orderedGroups.FirstOrDefault().Key;
        }
        public static int? Mode<T>(this IEnumerable<T> sequence, Func<T, int?> selector)
        {
            return sequence.Select(selector).Mode();
        }
        public static decimal? Mode(this IEnumerable<decimal?> sequence)
        {
            var grouped = sequence.GroupBy(item => item);
            var orderedGroups = grouped.OrderBy(group => group.Count());
            return orderedGroups.FirstOrDefault().Key;
        }
        public static decimal? Mode<T>(this IEnumerable<T> sequence, Func<T, decimal?> selector)
        {
            return sequence.Select(selector).Mode();
        }
    }
}

如果想了解扩展方法可以参考:https://www.cnblogs.com/landeanfen/p/4632467.html

如果这个类在一个单独的类库中,要使用LINQ扩展方法,您只需要引用类库程序集,因为 System.Linq 名称空间通常已经导入。 在Program.cs中,在FilterAndSort方法中,修改产品的LINQ查询调用自定义可链扩展方法,如下代码所示:

var query = db.Products
        .ProcessSequence()
        .Where(product => product.UnitPrice < 10M)
        .OrderByDescending(product => product.UnitPrice)
        .Select(product => new
        {
            product.ProductID,
            product.ProductName,
            product.UnitPrice
        });

在主方法中,取消对FilterAndSort方法的注释,并注释掉对其他方法的调用。

运行控制台应用程序并注意,您将看到与前面相同的输出,因为您的方法没有修改序列。但是您现在知道了如何用自己的功能扩展LINQ。

使用自定义扩展方法和内置的平均扩展方法,创建一个方法来输出产品的UnitsInStock和UnitPrice的平均值、中位数和模式,如下面的代码所示

static void CustomExtensionMethods()
{
    using (var db = new Northwind())
    {
        WriteLine("Mean units in stock: {0:N0}", db.Products.Average(p => p.UnitsInStock));
        WriteLine("Mean unit price: {0:$#,##0.00}", db.Products.Average(p => p.UnitPrice));
        WriteLine("Median units in stock: {0:N0}", db.Products.Median(p => p.UnitsInStock));
        WriteLine("Median unit price: {0:$#,##0.00}", db.Products.Median(p => p.UnitPrice));
        WriteLine("Mode units in stock: {0:N0}", db.Products.Mode(p => p.UnitsInStock));
        WriteLine("Mode unit price: {0:$#,##0.00}", db.Products.Mode(p => p.UnitPrice));
    }
}

在Main中,注释前面的方法调用并调用CustomExtensionMethods。

运行控制台应用程序并查看结果,如下面的输出所示

Mean units in stock: 41
Mean unit price: $28.87
Median units in stock: 26
Median unit price: $19.50
Mode units in stock: 13
Mode unit price: $22.00

使用LINQ to XML

LINQ to XML是一个LINQ提供程序,允许您查询和操作XML。

使用LINQ生成XML

让我们创建一个方法来将Products表转换为XML。

在Program.cs中,导入System.Xml.Linq命名空间。

创建一个方法以XML格式输出产品,如下代码所示:

static void OutputProductsAsXml()
{
    using (var db = new Northwind())
    {
        var productsForXml = db.Products.ToArray();
        
        var xml = new XElement("products", from p in productsForXml select 
            new XElement("product",
            new XAttribute("id", p.ProductID),
            new XAttribute("price", p.UnitPrice),
            new XElement("name", p.ProductName)));
        
        WriteLine(xml.ToString());
    }
}

在Main中,注释前面的方法调用并调用OutputProductsAsXml。

运行控制台应用程序,查看结果,并注意生成的XML的结构与前面代码中以声明方式描述的LINQ to XML语句的元素和属性相匹配,如下面的部分输出所示

<products>
<product id="1" price="18">
<name>Chai</name>
</product>
<product id="2" price="19">
<name>Chang</name>
</product>
...

使用LINQ读取XML到XML

您可能希望使用LINQ to XML轻松查询或处理XML文件。

在LinqWithEFCore项目中,添加一个名为settings.xml的文件。

修改其内容,如下所示:

<?xml version="1.0" encoding="utf-8" ?>
<appSettings>
<add key="color" value="red" />
<add key="size" value="large" />
<add key="price" value="23.99" />
</appSettings>

创建一个方法来完成这些任务

加载XML文件。

使用LINQ to XML来搜索名为appSettings的元素及其后代名为add的元素。

枚举数组来显示结果:

static void ProcessSettings()
{
    XDocument doc = XDocument.Load("settings.xml");
    var appSettings = doc.Descendants("appSettings")
            .Descendants("add")
            .Select(node => new
            {
                Key = node.Attribute("key").Value,
                Value = node.Attribute("value").Value
            }).ToArray();
    foreach (var item in appSettings)
    {
        WriteLine($"{item.Key}: {item.Value}");
    }
}

在Main中,注释先前的方法调用并调用ProcessSettings。

运行控制台应用程序并查看结果,如下面的输出所示

color: red
size: large
price: 23.99

实践与探索

通过回答一些问题来测试你的知识和理解,获得一些动手实践,并对本章所涵盖的主题进行更深入的研究。

  1. LINQ中两个必需的部分是什么?
  2. 你会使用哪一种LINQ扩展方法来返回一个类型的属性子集?
  3. 您将使用哪种LINQ扩展方法来过滤序列?
  4. 列出五个执行聚合的LINQ扩展方法。
  5. Select和SelectMany扩展方法之间的区别是什么?
  6. IEnumerable<T>IQueryable<T>有什么区别?以及如何在它们之间转换?
  7. 泛型函数委托中的最后一个类型参数表示什么?
  8. 以OrDefault结尾的LINQ扩展方法有什么好处?
  9. 为什么查询理解语法是可选的?
  10. 如何创建自己的LINQ扩展方法?
  11. 创建一个名为Exercise02的控制台应用程序,提示用户输入一个城市,然后列出Northwind客户所在城市的公司名称,如下面的输出所示
Enter the name of a city: London
There are 6 customers in London:
Around the Horn
B's Beverages
Consolidated Holdings
Eastern Connection
North/South
Seven Seas Imports

然后,在用户输入首选城市之前,通过显示客户已经居住的所有唯一城市的列表来增强应用程序,如下面的输出所示

Aachen, Albuquerque, Anchorage, Århus, Barcelona, Barquisimeto, Bergamo, Berlin, Bern, Boise, Bräcke, Brandenburg, Bruxelles, Buenos Aires, Butte, Campinas, Caracas, Charleroi, Cork, Cowes, Cunewalde, Elgin, Eugene, Frankfurt a.M., Genève, Graz, Helsinki, I. de Margarita, Kirkland, Kobenhavn, Köln, Lander, Leipzig, Lille, Lisboa, London, Luleå, Lyon, Madrid, Mannheim, Marseille, México D.F., Montréal, München, Münster, Nantes, Oulu, Paris, Portland, Reggio Emilia, Reims, Resende, Rio de Janeiro, Salzburg, San Cristóbal, San Francisco, Sao Paulo, Seattle, Sevilla, Stavern, Strasbourg, Stuttgart, Torino, Toulouse, Tsawassen, Vancouver, Versailles, Walla Walla, Warszawa

使用以下链接阅读本章所涵盖主题的更多细节