C#9 和 .NET5 企业级应用开发(三)

.NET平台与C#开发:从基础到企业级应用实战 现代软件开发中,.NET平台作为跨语言、跨平台的开发框架,通过CLR(公共语言运行时)和BCL(基础类库)提供强大的基础设施支持。其核心原理包括JIT编译、垃圾回收和类型安全机制,显著提升开发效率和运行时性能。在技术价值层面,.NET 6/7的统一SDK设计支持Web、微服务、云原生等多种应用场景,结合C# 10的语言特性如记录类型和最小API,能够快速构建高性能应用。特别是在企业开发中,依赖注入、配置管理和EF Core等组件,为电商、物流等系统提供稳定支持。本文通过容器化部署、性能调优等实战案例,深入解 阅读详情

原文:Enterprise Application Development with C# 9 and .NET 5

协议:CC BY-NC-SA 4.0

九、.NET 5 处理数据

任何 web 应用的基本组件之一是将数据保存到永久数据存储中的能力;在选择合适的持久存储时,一些预先考虑可以帮助系统在未来更好地扩展。

任何 web 应用中常见的操作之一是登录到系统,执行一些读取/更新,然后注销,然后稍后再回来查看更改是否被保留。数据库在保持这些操作(通常称为用户事务)方面起着重要作用。除了事务数据之外,出于监控和调试的目的,应用可能还需要存储日志数据和审计数据,例如谁修改了日期。设计任何此类应用的一个重要步骤是理解需求并相应地设计数据库。根据各种数据保留要求和任何数据保护政策选择/设计数据库也很重要,例如通用数据保护条例 ( GDPR )。

一个应用可以有多个数据提供者,如结构化查询语言 ( SQL )数据提供者、NoSQL 数据提供者、文件数据提供者等等。在本章中,我们将讨论可用于中的存储和数据处理的各种数据提供程序.NET 5。我们将涵盖以下主题:

  • 数据介绍
  • 磁盘、文件和目录
  • SQL、天青宇宙数据库和天青存储
  • 与英孚核心合作
  • 使用 Azure 宇宙数据库设计数据访问服务

技术要求

对…的基本了解.NET Core、C#、Azure 和.NET 命令行界面是必需的。

本章的代码文件可以在以下链接中找到:https://github . com/PacktPublishing/Enterprise-Application-Development-with-C-Sharp-9-and-.NET-5/树/主/第 09 章。

代码的使用说明可以在这里找到:https://github . com/PacktPublishing/Enterprise-Application-Development-with-C-Sharp-9-and-.NET-5/树/主/企业% 20 应用

数据介绍

任何网络应用,无论是内容管理系统、社交网络平台还是电子商务应用,都需要将数据保存到永久存储中,以便用户可以根据需要检索、消费和处理数据。在 第八章理解缓存中,我们讨论了使用缓存存储;但是,缓存存储是临时存储,数据仍然需要保存到永久存储中。所以,我们需要一个不只是支持不同实体上各种 CRUD (简称创建/读取/更新/删除)操作,还支持高可用性,并在宕机时恢复任何数据的存储,也就是灾难恢复。

更好的系统设计的关键标准之一是在系统的早期阶段设计一个数据模型。数据模型应该尝试定义系统运行所需的所有可能的实体,并在各种实体之间进行交互。在系统设计的早期定义数据模型有助于确定如何管理数据和可以使用什么数据存储的正确策略,并有助于决定各种复制/分区策略。

下面几节将解释两种常见的分类数据存储。

关系数据库管理系统

关系数据库将数据存储在表中。每个实体被定义为一个或多个表,并且使用多个表来定义数据库。将表分隔成多个表的过程称为规范化。各种表之间的关系由外键约束定义。实体的属性定义为列,相同类型的多个实体存储为行。一些常用的关系数据库是微软的 SQL Server、MySQL、Postgres 和 Oracle。

存储员工信息的典型关系数据库可能有一个定义员工各种属性的employee表,如姓名、员工标识等,以及以员工标识为主键的列。多个员工作为单独的行存储在该表中。员工的任何属性都可以进一步规范化到单独的表中;例如,员工的项目可以存储在一个单独的表中(因为可以有多个项目),例如employeeproject,并且可以使用员工标识链接到员工表,如下图所示:

https://github.com/OpenDocCN/freelearn-csharp-zh/raw/master/docs/enter-app-dev-cs9-dnet5/img/Figure_9.1_B15927.jpg

图 9.1–员工 ER 图

以下是关系数据库的几个关键特征:

  • 使用 SQL 查询关系数据库。
  • 表大多有定义良好的模式和约束,不太可能改变。
  • 所有事务都具有 ACID (原子性/一致性/隔离性/持久性的缩写)属性,因此保持了数据的完整性和一致性。
  • 随着数据的规范化,冗余被最小化。
  • 关系数据库通常支持垂直扩展,也就是向上扩展(它们确实支持复制,但是与 NoSQL 数据库中的复制相比,这是一个昂贵的操作)。

NoSQL

另一种数据存储是 NoSQL 数据库,它以非结构化格式存储数据,其中数据不需要有预定义的模式。最常见的情况是,数据要么存储为键值对(如在 Redis 中),要么存储为文档(如在 MongoDB 和 CouchDB 中),要么存储为使用图形结构的图形(如在 Neo4j 中)。

如果我们以同一个雇员为例,并将其保存在 NoSQL 数据库(如 MongoDB)中,我们最终会将其存储在一个类似employee的集合中,每个文档都存储该雇员的所有属性,如下所示:

{
  "employee": [
    {
      "employeeid": 1,
      "name": "Ravindra",
      "salary": 100,
      "Projects": [
        {
          "id": 1,
                  "name": "project1",
        },
        {
                     "id": 2,
                  "name": "project2",
        }
      ]
    }
  ]
}

以下是 NoSQL 数据库的几个关键特征:

  • 实体不一定需要支持固定的模式,在任何时间点,都可以添加额外的属性。
  • 它们非常适合非结构化数据,例如,将位置存储在拼车应用中。
  • 与关系数据库相比,它们可以以低得多的成本轻松支持水平扩展。
  • 数据高度冗余;然而,这极大地提高了性能,因为无需跨表执行连接就可以轻松获得数据。

Azure Cosmos DB 就是这样一个云管理的 NoSQL 数据库,我们将在我们的电子商务应用中将其用作数据存储。

任何存储平台的核心基础存储介质都是进一步存储在磁盘中的文件。让我们看看中提供的各种 API.NET 5,执行创建和修改文件和目录等操作。

磁盘、文件和目录

在计算机中,一个文件只不过是一组使用唯一名称引用的数据。比如所有员工的详细信息都可以分组存储在一个名为employees的文件中,每当我们需要查看员工数据时,我们都会打开employees文件搜索该数据。这是从一个类比中得出的,如果我们想以非数字形式保存所有员工的详细信息,我们将把它们写在纸上,把所有的文件钉在一起,并保存在一个文件中。

磁盘只不过是存储文件的存储介质。根据前面的类比,磁盘可以比作可以存储文件的机架。磁盘应该支持的一个关键点是管理文件、组织文件、定义文件的属性的方法,例如创建/修改文件的时间、在磁盘上查找可用空间的能力等等。这就是文件系统发挥作用的地方,它通常与操作系统一起出现,并包含管理磁盘上文件的这些方面。文件系统实现这一点的一种方法是使用目录。

现在让我们看看中的各种 API.NET 5,它允许我们与文件、磁盘和目录交互。

处理目录

目录或文件夹是文件分组的一种方式,是文件系统定义的一种规则组织文件和子目录。英寸 NET 5 中,我们在框架中提供了与文件系统通信和组织目录的 API。这些类主要是System.IO命名空间的一部分,有两个类主要用于对目录执行各种操作:

  • 目录 : System.IO.Directory是一个静态类,公开了各种方法允许我们创建目录,查询目录属性,移动等等。当我们需要执行简单的目录操作时,这个类很有用。
  • 目录信息 : System.IO.DirectoryInfo是另一个可以用来对目录/文件夹进行各种操作的类;然而,这不是一个静态类,因此需要这个类的一个对象来调用各种方法。更重要的是,这个类提供了一些高级方法,比如在一个工作目录中创建子目录。因此,如果我们需要递归地对单个目录执行操作,可以使用这个类。

让我们创建一个简单的控制台应用,如果目录不存在,就创建一个目录,并列出目录的各种属性。我们还将创建一些文件并(在下一节中详细讨论)枚举它们,然后最后删除目录。

我们还将使用System.Environment、System.IO.Path和System.IO.DriveInfo类来显示与环境、当前驱动器和当前文件夹相关的一些常规属性。那么,让我们来看看怎么做:

  1. 使用 Visual Studio 2019 或创建控制台应用.NET 命令行界面。

  2. Update the Main class with the following code:

    static void Main(string[] args)
            {
                string currentDriveLetter = Path.GetPath
                Root(System.Reflection.Assembly.
                GetEntryAssembly().Location);
                DriveInfo di = new DriveInfo(current
                DriveLetter);
                if (di.IsReady) // Checking if drive is 
                                // ready
                {
                    Console.WriteLine($"Available space 
                    in {currentDriveLetter} is 
                    {di.AvailableFreeSpace.ToString()}");
                }
                Console.WriteLine($"Current location 
                (using Environment class) – 
                {Environment.CurrentDirectory}");
                Console.WriteLine($"Current location 
                (using path class)- {Path.
                GetFullPath(Environment.
                CurrentDirectory)}");
                string newDirectoryName = "New Data 
                Directory";
                if (Directory.Exists(newDirectoryName))
                {
                    Console.WriteLine($"Directory with 
                    name {newDirectoryName} already 
                    exists!!");
                }
                else
                {
                    Directory.CreateDirectory(
                    newDirectoryName);
                    Console.WriteLine($"Directory 
                    {newDirectoryName} created!!");
                }
            }
    

    在前面的代码中,我们正在执行以下操作:

    • 使用Path类获取当前驱动器的路径
    • 使用DriveInfo类显示当前驱动器中的可用空间
    • 如果目录不存在,则创建目录
  3. 接下来,在目录创建代码之后,将以下代码添加到Main方法中。在这段代码中,我们正在执行以下操作:

    • 使用DirectoryInfo类创建子目录。

    • 使用System.IO.File静态类创建几个空文件,然后枚举这些文件,显示它们的一些属性。

    • 最后,删除创建的文件夹。参考以下代码:

                  DirectoryInfo dirInfo = new DirectoryInfo(
                  newDirectoryName);
                  dirInfo.CreateSubdirectory($"Sub 
                  {newDirectoryName}");
                  //Create few files and enumerate.
                  for (int i = 0; i < 5; i++)
                  {
                      FileStream fs = File.Create(Path.
                      Combine(Environment.CurrentDirectory, 
                      newDirectoryName, $"File {i}"));
                      fs.Dispose();
                  }
                  foreach (FileInfo fi in dirInfo.GetFiles())
                  {
                      Console.WriteLine($"File {fi.Name} 
                      created on - {fi.CreationTime.
                      TimeOfDay}, Size: {fi.Length}");
                  }
                  dirInfo.Delete(true);// clean up, passing 
                  //true to recursively delete contents.
                  Console.WriteLine("Directory deleted!!");
      
  4. 一旦我们运行这段代码,我们将看到以下输出:

https://github.com/OpenDocCN/freelearn-csharp-zh/raw/master/docs/enter-app-dev-cs9-dnet5/img/Figure_9.2_B15927.jpg

图 9.2–目录操作输出

如果您打开 Windows 资源管理器,在删除目录之前查看目录的中间内容,它将如下所示:

https://github.com/OpenDocCN/freelearn-csharp-zh/raw/master/docs/enter-app-dev-cs9-dnet5/img/Figure_9.3_B15927.jpg

图 9.3–目录中的文件

如你所见.NET 5 自带了很多 API,允许我们对目录执行各种操作。在下一节中,让我们浏览一下可用于对文件执行类似操作的各种 API。

用流、二进制和字符串读取/写入数据

.NET 5 自带了大量的 API 来读写文件中的数据,从静态的System.IO.File类到基于实例的类,比如System.IO.FileInfo。然而,在进入可以对文件进行操作的应用编程接口之前,我们应该了解什么是流。

英寸 NET 5,一个流只不过是一个来自存储的字节序列。底层存储/设备,也称为后备存储,可以是文件、内存或对应用编程接口(网络)的响应。流的概念主要是提供一个抽象,这样应用就不需要知道后备存储。每当应用需要读取一些数据时,它将使用可以从流中读取的 API,并且当它需要更新该数据时,它将更新该流。然后,该流将进一步将其传递给后备存储。

溪流

流在从文件中读取/写入数据时很有用。英寸 NET 5,支持从流中读取和向流中写入的类是System.IO.Stream,它是一个抽象类,有三个重要的方法:

  • Read/ReadAsync:同步和异步从流中读取数据
  • Write/WriteAsync:将数据同步和异步写入流
  • Seek:接受整数值,并将流中的当前位置移动到指定值

由于Stream类是抽象类,.NET 5 附带了一些具体的实现,如下图所示:

https://github.com/OpenDocCN/freelearn-csharp-zh/raw/master/docs/enter-app-dev-cs9-dnet5/img/Figure_9.4_B15927.jpg

图 9.4–流层次结构

让我们创建一个方法,并使用FileStream类的一个对象来读取和写入数据到一个文件。我们将使用静态File类的Create方法,然后使用OpenRead方法读取文件的内容。

创建控制台应用并添加以下方法:

        static async Task ReadStreamAsync()
        {
            byte[] writeData = new byte[5] { 80, 65, 67, 75
            , 84 };
            UTF8Encoding temp = new UTF8Encoding(true);
            string data = "PACKT";
            Encoding.ASCII.GetBytes(data);
            using (Stream fs = File.Create("WriteDataUsing
            FileStream.txt"))
            {
                await fs.WriteAsync(writeData); 
                //String PACKT in ASCII   
                byte[] readData = new byte[5];
                fs.Position = 0; // Setting the stream 
                                 //position to 0.
                int chunkRed = 0, dataRed = 0;
                while ((chunkRed = fs.Read(readData,
                dataRed, readData.Length – 
                chunkRed)) > 0)
                {
                    dataRed += chunkRed;
                }                
                for (int i = 0; i < readData.Length; i++)
                    Console.Write(readData[i]);
            }

一旦我们在控制台应用中调用这个方法,我们可以看到已经创建了一个名为WriteDataUsingFileStream.txt的文本文件,该文件的内容将是PACKT字符串。现在,流的Read方法不能保证它将读取大小计数参数的整个数据(在前面的示例中为readData.Length),因此我们需要依赖于Read方法返回的值,该值只不过是已处理的数据流的大小。然而,在前面的代码中,控制台上的输出仍然以字节为单位,即80, 65, 97, 75, 84,,这就是编码和解码的作用。

编码是将字符转换成字节的一种方式,同样,解码也是将字节转换成字符的一种方式。这里,在示例中,我们需要对字节数组进行解码,以便将内容转换为字符,并以可读的格式显示。英寸 NET 5,这可以通过使用System.Text.Encoding类来实现。在前面的代码中,按照如下方式更改Console.WriteLine并运行应用:

Console.Write($"{Encoding.ASCII.GetString(readData)}");

这次你可以看到输出是PACKT。

读者/作者

大多数现实世界的应用会处理可读性更强的字符串、XML/JSON 数据或该语言支持的任何其他数据类型,而不是字节,尽管在前面的示例中,我们可以用一行代码将字节数组转换为字符串,但随着编码类型的不同,如 ASCII、UTF-8 等,它会变得更加复杂。这就是原因.NET 5 附带了读取器和写入器类,它们可以与流对话,并根据需要转换数据。这些类主要充当适配器,抽象底层流,然后将流转换为所需的类型。一般来说,读者/作家类.NET 5 如下图所示:

https://github.com/OpenDocCN/freelearn-csharp-zh/raw/master/docs/enter-app-dev-cs9-dnet5/img/Figure_9.5_B15927.jpg

图 9.5–读取器/写入器(适配器)层次结构

让我们了解这些是什么:

  • StreamReader/StreamWriter:抽象类TextReader/TextWrite的具体实现,对流进行操作,将它们转换成字符/字符串,反之亦然
  • StringReader/StringWriter:具体实现TextReader/TextWriter抽象类对内存中的字符串进行操作,例如字符串操作或者对大字符串段落中的行数进行计数
  • BinaryReader/BinaryWriter:从流中读取和向流中写入的具体类,专门将 int 和 Boolean 等原始数据类型转换为流,反之亦然
  • XmlReader/XmlWriter:将流转换为 XML 的具体类,反之亦然

有了这个的理解,让我们创建一个简单的控制台应用,我们将使用StreamWriter将数据写入文件,然后使用StreamReader读取数据,使用StringWriter和StringReader对内容重新排序,并显示它:

  1. 创建一个控制台应用,并添加以下将字符串内容写入文件的方法:

            static async Task UsingStreamReaderWriter()
            {
           // Create a string array with the lines of text
                string[] lines = { "This is the First line
                ", "This is the Second line", 
                "This is the Third line" };
                // Write the string array to a new file 
                //named "WriteLines.txt".
                using (StreamWriter outputFile = new 
                StreamWriter("WriteLinesUsingSW.txt"))
                {
                    foreach (string line in lines)
                    {
                        await outputFile.WriteLineAsync
                        (line);
                    }
                }
                Console.WriteLine("File write completed 
                using StreamWriter\n");
            }
    
  2. 将以下代码添加到UsingStreamReaderWriter方法中,在该方法中,我们正在读取之前使用StreamReader创建的文件。此外,这里我们还利用StringWriter将其附加写入字符串。当然,这里我们也可以利用StringBuilder类;然而,如果有一个场景,我们需要将它传递给另一个接受TextReader类型实例的方法,在这种情况下,StringReader会派上用场:

                StringBuilder sb = new StringBuilder();
                StringWriter srw = new StringWriter();
                using (StreamReader sr = new StreamReader
                ("WriteLinesUsingSW.txt"))
                {
                    Console.WriteLine(sr.BaseStream.Length
                    ); // Using base stream to 
                    //retrieve length of file contents.
                    while (!sr.EndOfStream)
                    {
                        string line = sr.ReadLine();
                        srw.Write($"{line}\n"); 
    // Appending . at the end of line for easy 
    //separate later
                    }
                }
                Console.WriteLine("Reading from 
                StringWriter");
                Console.WriteLine(srw.ToString());
    
  3. 最后,添加以下代码,它与文件内容相反,并打印它。由于文件内容已经在StringWriter对象中可用,我们将把它读入一个StringReader对象,并使用StringReader的方法生成最终输出:

                using (StringReader str = new 
                StringReader(srw.ToString()))
                {
                    while (str.Peek() > -
                    1) // Peeking to check end of string
                    {
                        char currentCharacter = Convert.
                        ToChar(str.Read());
                        if (currentCharacter != '\n') 
    //Checking current character and inserting at the 
    //beginning of string builder
                           sb.Insert(0, currentCharacter);
                        else
                           sb.Insert(0, '.');
                    }
                }
                if (srw != null)
                    srw.Dispose();
                Console.WriteLine("Reading from 
                Stringbuilder aftere reversal");
                Console.WriteLine(sb.ToString());
    
  4. 然后,我们在主方法中调用这个并执行控制台应用。输出将如下图所示,同时在相同的位置创建WriteLinesUsingSW.txt文件:

https://github.com/OpenDocCN/freelearn-csharp-zh/raw/master/docs/enter-app-dev-cs9-dnet5/img/Figure_9.6_B15927.jpg

图 9.6–读取器/写入器输出

StreamWriter使用的默认编码是 UTF-8;但是,有一个参数化的构造函数允许您指定编码。在StreamReader的情况下.NET 5 在内部尝试检测流的编码并相应地使用它。然而,如果它不能检测到源的编码,它将回落到 UTF-8。

一般来说,如果给一个选择,我们应该使用一个使用更少字节来存储数据的编码,重要的是,它应该支持你数据中的所有字符。

文本阅读器/文本编写器与二进制阅读器/二进制编写器

一般来说,存储在计算机中的任何东西只不过是一系列 0 或 1 的位,因此所有东西都以二进制形式存储。然而,存储为二进制的文件和二进制或文本的文件内容是有区别的。例如,文本文件的内容可以是文本数据;但是,JPEG 文件不能表示为文本,并且大多数情况下,它将以二进制形式表示。正确的读写器库用于合适的文件是非常重要的。下表给出了各种适配器之间的一些差异:

https://github.com/OpenDocCN/freelearn-csharp-zh/raw/master/docs/enter-app-dev-cs9-dnet5/img/012.jpg

表 9.1

我们对 C#中可用的各种类的讨论到此结束,这些类可用于写入文件、从文件中读取以及处理目录。让我们简单讨论一下 C#中最常用的序列化库。

JSON。净得很

任何关于文件读/写的讨论如果不涉及 JSON.NET 库都是不完整的,该库是用于将对象序列化为 JSON 的最常见的库,反之亦然。正如我们所知,JSON 是 JavaScript 对象符号,也是交换数据的公认标准。关键思想是能够保存/传输(序列化)对象的状态,以便可以在稍后阶段以相同的形式检索(反序列化)。

使用 JSON.NET 序列化数据最简单的方法是使用JsonConvert静态类并调用方法,如以下代码片段所示:

    // Type
    public class Employee
    {
        public string Name { get; set; }
        public int Id { get; set; }
    }
    // Usage  
    Employee employee = new Employee
    {
                Name = "John",
                Id = 1
    };        
    // Serialize
    File.WriteAllText("employee.json", JsonConvert.
    SerializeObject(employee));
   // Deserialize
var output = JsonConvert.DeserializeObject<Employee>(File.ReadAllText("employee.json"));

虽然从.NET Core 3.0 之后,微软推出了一个名为System.Text.Json的新库,用于序列化和反序列化对象;正如在 第 8 章中开发的Packt.Ecommerce.Caching项目一样,理解 缓存,我们将使用System.Text.Json进行我们的电子商务应用,除非我们遇到这里提到的一些问题:https://github.com/dotnet/runtime/issues?q=system.text.json。所有可用的 api 都可以在这里找到:https://docs . Microsoft . com/en-us/dotnet/API/system . text . JSON?view=net-5.0 。

到目前为止,我们已经看到了如何在中可用的文件和目录以及各种 API 上进行操作.NET 5。然而,更多的时候,应用不是直接写入文件系统,而是写入一个抽象了文件系统复杂性的数据库系统。让我们看看其中的一些提供者以及它们与.NET 5。

SQL、Azure 宇宙数据库和 Azure 存储

之前,我们讨论了将数据存储分为关系数据库和 NoSQL 的更广泛的分类。在本节中,让我们详细了解微软生态系统中可用的一些数据提供者及其与的集成.NET 5。提供者种类繁多,包括 SQL、Azure Cosmos DB、Azure Storage 等,数据提供者的选择完全由应用需求驱动。然而,在现实生活中,应用的需求会发生很大的变化,因此关键是用业务层和用户界面来抽象数据框架的实现,这进一步有助于根据需要改进设计。接下来,让我们在下一节中看看我们的第一个数据提供者,SQL。

SQL Server

关系数据库管理系统市场中占主导地位的数据库之一是微软 SQL Server,俗称 SQL Server,它使用 SQL 与数据库进行交互。SQL Server 支持所有基于关系数据库管理系统的实体,例如表、视图、存储过程和索引等,并且主要在 Windows 环境下工作。但是,从 SQL Server 2017 开始,它同时支持 Windows 和 Linux 环境。

SQL Server 的主要组件是它的数据库引擎,它负责处理查询和管理文件中的数据。除了数据库引擎之外,SQL Server 还附带了各种数据管理工具,例如:

  • SQL Server 管理工作室 ( SSMS ):连接到 SQL Server 并执行操作,如创建数据库、监控数据库、查询数据库和备份数据库
  • SQL Server 集成服务 ( SSIS ):用于数据集成和转换
  • SQL Server 分析服务 ( SSAS ):用于数据分析
  • SQL Server 报告服务 ( SSRS ):用于报告和可视化

要在本地计算机上配置 SQL Server,我们需要安装安装数据库引擎和一个或多个前面组件的 SQL Server 版本之一。安装通常包括下载安装程序并通过图形用户界面或命令行安装。有关安装的更多详细信息,请参考 https://docs . Microsoft . com/en-us/SQL/database-engine/install-windows/install-SQL-server?view=sql-server-ver15。

虽然在内部,但是 SQL Server 已经被广泛使用;管理数据库、升级等总会有开销这也是微软推出 Azure SQL 的地方,Azure SQL 是一个完全托管的 PaaS (简称平台即服务)组件,运行在与内部 SQL Server 相同的数据库引擎上。

Azure SQL 附带以下变体:

  • Azure SQL 数据库(单个数据库):这是一个托管数据库服务器允许你用专用资源创建一个完全隔离的数据库。

  • Azure SQL Database(弹性池):弹性池允许您在单个服务器上的预定义资源池(就 CPU、内存和 I/O 而言)中运行多个单个数据库。它是拥有多个数据库的企业的理想选择混合了低使用率和高使用率。在这种情况下使用弹性池的优点是,需要更多 CPU 使用的数据库可以在高需求期间利用它,并在低需求时释放它。使用弹性池的理想情况是当有一组数据库并且它们的消耗不可预测时。每当您看到一个数据库持续消耗相同的资源集时,它就可以从弹性池中移出,进入单个数据库,反之亦然。

  • Azure SQL 托管实例:该模型提供了一种将内部 SQL 基础架构无缝迁移到 Azure SQL 的方法,而无需重新架构内部应用,并允许您利用 PaaS。这非常适合于拥有庞大的内部数据库基础架构并且需要迁移到云而没有太多运营开销的应用。

  • SQL Server on VM (Windows/Linux): SQL VMs come under the Infrastructure as a Service (IaaS) category and are very similar to on-premises SQL Server, only that VMs are on Azure instead of your local network.

    小费

    建议安装 SSMS,以便在 SQL Server(内部或云)上执行各种操作,因为它支持所有数据库操作。

从. NET 5 应用的角度来看,连接到 Azure SQL 与连接到内部 SQL Server 是一样的。可以使用 ADO.NET,我们使用System.Data.SqlClient导入,然后使用SqlConnection对象连接到 SQL,然后使用SqlCommand对象执行 SQL 查询,使用SQLReader类返回值。除此之外,我们可以使用对象关系映射器 ( ORM )如实体框架核心 ( EF 核心)来处理 Azure SQL,这将在处理 EF 核心一节中讨论。

所以,在这个部分,我们已经简单介绍了 Azure SQL。但是,我建议在这里查看 Azure SQL 的所有功能:https://docs.microsoft.com/en-us/azure/azure-sql/。

至此,让我们继续讨论 Azure Cosmos DB,这是我们的电子商务应用将用作持久存储的数据库。

蓝色宇宙 DB

Azure Cosmos DB 是一个完全托管(PaaS)的 NoSQL、全球分布式和高度可扩展的数据库。Azure Cosmos DB 的一个关键之处是它的多模型特性,这有助于使用不同的 API 模型(如 SQL、MongoDB 和 Gremlin)传递各种格式的数据,如 JSON 和 BSON。开发人员可以灵活地使用他们熟悉的应用编程接口查询数据库。例如,SQL 开发人员可以继续使用 SQL 查询语法查询数据库,MongoDB 开发人员可以继续使用 MongoDB 语法查询数据库,等等。在引擎盖下,Azure Cosmos DB 以名为Atom-Record-Sequence(ARS)的格式存储数据库,并根据数据库创建过程中选择的模式将数据作为 API 公开。

Azure Cosmos DB 的另一个重要特点是它能够自动索引所有数据,并且独立于所使用的 API 模型。所有这些都无需开发人员另外创建索引,因此能够更快地检索数据。

Azure Cosmos DB 支持以下 API 来对数据库执行操作,这是我们在创建数据库时选择的:

  • 核心(SQL) API :这是可以用来查询数据库的默认 API;查询会有一个 JSON 中输入/输出格式的 SQL 查询的语法。使用核心 SQL 应用编程接口的典型查询如下所示:SELECT * FROM product WHERE product.Name = ' Mastering enterprise application development Book'。
  • MongoDB API :这个 API 建立在 MongoDB 的有线协议上,可以与 MongoDB 客户端 SDK、驱动和工具无缝集成。该应用编程接口非常适合已经与 MongoDB 集成并迁移到 Azure Cosmos DB 的应用,或者非常适合开发人员已经习惯 MongoDB 查询语言的团队。这个 API 的最新版本支持 MongoDB 服务器 3.6 版本,Azure Cosmos DB 和 Mongo API 之间的典型查询连接将如下所示:db.product.find({"Name": ' Mastering enterprise application development Book'})。就像 MongoDB 一样,数据在 BSON 表示。
  • Gremlin(图形)API :该 API 支持使用 Gremlin 语言以图形格式查询和遍历数据。这非常适合数据可以以图形的形式表示,并且可以通过它们之间的关系进行查询的情况。一个典型的例子是推荐引擎,它可以建立两个实体之间的关系,并提出一个推荐。

除此之外,还有卡珊德拉应用编程接口,它使用卡珊德拉查询语言对数据库进行操作,然后是表应用编程接口,它可以被构建在 Azure 表存储之上的应用用作它们的数据存储。

正如你所看到的,有相当多的应用编程接口,更多的应用编程接口正在被添加。选择合适的 API 完全取决于应用需求;但是,以下几点可用于缩小选择范围:

  • 如果是一个新的应用,那么就用核心应用编程接口。
  • 如果它是一个建立在 NoSQL 上的现有应用,请根据底层数据存储选择相关的 API。例如,如果现有的数据库是 MongoDB,选择 Mongo API 等等。
  • 对于处理特定的场景,比如建立数据之间的关系,使用 Gremlin API。

对于我们的企业应用,由于我们是从头开始构建这个应用,我们将使用核心(SQL)应用编程接口作为我们与 Azure Cosmos DB 交互的应用编程接口。

让我们先创建一个简单的控制台应用,并在 Azure Cosmos DB 上执行一些操作,稍后我们将在构建我们的数据访问服务时重用这些概念:

  1. 首先,我们需要有一个 Azure Cosmos DB 帐户,所以登录 Azure 门户,点击创建资源,选择数据库 | Azure Cosmos DB 。

  2. This will open the Create Azure Cosmos DB Account page. Fill in the details as shown in the following screenshot and click Review + create. This is the page where we select the API we want to choose, which is the Core (SQL) API in our case:

    https://github.com/OpenDocCN/freelearn-csharp-zh/raw/master/docs/enter-app-dev-cs9-dnet5/img/Figure_9.7_B15927.jpg

    图 9.7–创建 Azure 宇宙数据库帐户页面

  3. 一旦账户被创建,导航到蔚蓝公司烟雾数据库账户 | 键。复制 URI 和主键值。

  4. 打开命令行,使用以下命令创建控制台应用:

    dotnet new console –output EcommerceSample
    
  5. 导航到EcommerceSample文件夹,使用以下命令安装 Azure Cosmos DB SDK】

  6. 打开Program.cs并将以下静态变量添加到Program类,该类将保存在步骤 3 中复制的 URI 和主键值:

     private static readonly string Uri = "YOUR URI HERE";
     private static readonly string PrimaryKey = 
    "YOUR PRIMARY KEY HERE";
    
  7. 现在,让我们添加代码来创建一个CosmosClient类的对象,并使用它来创建一个 Azure Cosmos DB 数据库。随后,该对象将用于与我们的 Azure Cosmos DB 数据库进行通信。当CosmosClient实现IDisposable时,我们将在using块内创建它,以便在using块后自动处理该对象。运行此代码并导航到 Azure 门户中的 Azure Cosmos 资源 | 数据资源管理器后,可以看到将创建一个名为Ecommerce的数据库。由于我们已经使用核心(SQL)应用编程接口创建了 Azure Cosmos DB 帐户,该数据库将支持 SQL 语法查询:

    using (CosmosClient cosmosClient = new CosmosClient(Uri,
     PrimaryKey))
    {
     DatabaseResponse createDatabaseResponse
    = await cosmosClient.CreateDatabaseIfNotExistsAsync
    ("ECommerce");
     Database database = createDatabaseResponse.Database;
    }
    
  8. Now, let’s create a container that is analogous to a table in SQL by adding the following code after createDatabaseResponse. As we are using CreateDatabaseIfNotExistsAsync to create the database, running the same code will not cause any exceptions. Once we run this code, we can see in the Azure portal that a container with the name Products is created under the Ecommerce database:

    var containerProperties = new ContainerProperties
    ("Products", "/Name");
    var createContainerResponse = await database.CreateContainerIfNotExistsAsync(containerProperties, 
    10000); var productContainer = createContainerResponse.
    Container;
    

    在前面的代码中,我们在创建容器的时候传递了ContainerProperties,可以看到其中一个值就是Name,无非就是一个分区键。

分区是 Azure Cosmos DB 的关键特性之一,它根据分区键将容器内的数据分隔成多个逻辑分区。使用分区键,Azure Cosmos DB 实现了数据库的水平扩展,从而满足了应用的可伸缩性和性能需求。选择分区键是一个关键的设计决策,因为它将极大地帮助数据库扩展和更好地运行。此外,分区键不能更改,必须在创建容器时定义。选择分区键时,请记住以下几点:

  • 应该具有最大数量的唯一值;唯一值的数量越多,分区就越好。例如,如果我们正在为产品创建容器,产品标识或名称可能是分区键,因为这两个属性可以唯一地标识大多数产品。在兜帽下,如果为分区键选择了一个产品名称,并且内部有 100 个产品,则它在 Azure Cosmos DB 中由 100 个逻辑容器表示。这里,产品类别也可以是一个分区键,但是在选择它作为分区键之前,我们需要评估样本数据并根据需求来决定。

  • If there is no obvious unique choice, we can pick the most used field in the filtering query, so basically, a column that is very often used in the where clause.

    小费

    在现实应用中,应该使用 ARM 模板来实现 Azure Cosmos DB 帐户的创建,以便模板可以轻松地与连续部署 ( 光盘)集成。

有了这个,让我们在我们的产品容器中添加一些数据并进行查询:

  1. We will add this entity based on the following sample JSON. Based on the product category, there could be different attributes.

    例如,如果产品类别为Books,则Authors、Format等字段中会有值;但是,如果类别是Clothing,则有诸如Size和Color等字段的值。这个模式可以在我们的电子商务应用中重用:

    {
      "Id": "Book.1",
      "Name": "Mastering enterprise application 
          development Book",
      "Category": "Books",
      "Price": 100,
      "Quantity": 100,
      "CreatedDate": "20-02-2020T00:00:00Z",
      "ImageUrls": [],
      "Rating": [
        {"Stars": 5, "Percentage": 95},
        {"Stars": 4, "Percentage": 5}
      ],
      "Format": ["PDF","Hard Cover"],
      "Authors": ["Rishabh Verma","Neha Shrivastava",
          "Ravindra Akela","Bhupesh Guptha"],
      "Size": [],
      "Color": []
    }
    
  2. 现在,让我们创建普通旧 CLR 对象 ( POCOs )可以序列化到前面的 JSON。我们需要两个类来表示它们:一个用于产品,一个用于评级,这是Product的子类。在带有核心(SQL) API 的 Azure Cosmos DB 中,任何实体的必备字段之一是id字段,它有点像主键。因此,我们的父模型有必要定义id字段。这些类如下所示:

    public class Rating{
        public int Stars { get; set; } 
        public int Percentage { get; set; } 
    }
    public class Product{
        [JsonProperty(PropertyName = "id")]
        public string ProductId { get; set; } 
        public string Name { get; set; } 
        public string Category { get; set; } 
        public int Price { get; set; } 
        public int Quantity { get; set; }
        public DateTime CreatedDate { get; set; }
        public List<string>  ImageUrls { get; set; }
        public List<Rating> Rating { get; set; } 
        public List<string> Format { get; set; } 
        public List<string> Authors { get; set; } 
        public List<int> Size { get; set; } 
        public List<string> Color { get; set; } 
    }
    
  3. 现在,让我们创建以下Product类的对象,并将其插入数据库:

    Product book = new Product()
    {
        ProductId = "Book.1", Category = "Books", Price = 
        100,
        Name = "Mastering enterprise application 
        development Book",                    
        Rating = new List<Rating>() { new Rating { Stars = 
        5, Percentage = 95 }, new Rating { Stars = 4, 
        Percentage = 5 } },
        Format = new List<string>() { "PDF", "Hard Cover" },
        Authors = new List<string>() { "Rishabh Verma", "Neha Shrivastava", "Ravindra Akela", "Bhupesh Guptha" }
    };
    
  4. Now, we will call the CreateItemAsync method using the productContainer object, as shown in the following code snippet. Also, we should ensure that an object with the same ProductId value isn’t already present:

    try
    {
        // Check if item it exists.  
        ItemResponse<Product> productBookResponse = await productContainer.ReadItemAsync<Product>(book.ProductId, new PartitionKey(book.Name));
    }
    catch (CosmosException ex) when (ex.StatusCode == System.Net.HttpStatusCode.NotFound)
    {
        ItemResponse<Product> productBookResponse = await productContainer.CreateItemAsync<Product>(book, new PartitionKey(book.Name));
        Console.WriteLine($"Created item {productBookResponse.Resource.ProductId}");
    }
    

    一旦我们运行了这段代码,数据就应该被插入到Products容器下的Ecommerce数据库中。

  5. 如果我们想查询这个记录,我们可以使用下面的代码来查询数据库。如您所见,语法非常类似于从 SQL 数据库中查询数据:

    string getAllProductsByBooksCAtegory = "SELECT * FROM p WHERE p.Category = 'Books'";
    QueryDefinition query = new QueryDefinition(getAllProductsByBooksCAtegory);
    FeedIterator<Product> iterator = productContainer.GetItemQueryIterator<Product>(query);
    while (iterator.HasMoreResults)
    {
        FeedResponse<Product> result = await iterator.ReadNextAsync();
        foreach (Product in result)
        {
            Console.WriteLine($"Book retrived – 
            {product.Name}");
        }
    }
    

类似地,ContainerClass提供了可用于各种 CRUD 操作的所有相关方法。所有那些 api 都可以在这里找到:https://docs . Microsoft . com/en-us/dotnet/API/Microsoft . azure . cosmos . container?view=azure-dotnet 。

在此基础上,我们将设计我们的电子商务应用所需的数据模型以及各种 API 要消费的相关数据服务层。到目前为止,我们已经看到了 SQL 和 NoSQL 提供程序。让我们看看还有哪些其他选项可以保存数据。

天青储

Azure Storage 是一个高度可用且可扩展的数据存储,支持以各种格式存储数据,包括文件。Azure Storage 主要支持以下四种类型的数据:

  • Azure 表:支持持久化无模式数据的 NoSQL 实现。
  • Azure Blob:Blob 是非结构化数据,适用于需要上传、下载或流式传输大量文件的应用。
  • Azure 队列:这个允许你以任何可序列化的格式对消息进行排队,然后由服务进行处理。队列非常适合具有大量服务对服务通信的场景,并且充当消息的持久层。
  • Azure 文件/Azure 磁盘:用于文件的数据存储,非常适合基于本地文件应用编程接口构建的系统。

以下几点使 Azure 存储成为应用开发的重要组成部分之一:

  • 高可用性:存储在 Azure Storage 中的数据为跨数据中心/区域的复制提供了开箱即用的支持,这进一步确保了一个区域的硬件故障不会导致数据丢失。
  • 性能:对 CDN 集成的现成支持,有助于从离用户和更近的位置(边缘服务器)缓存和加载数据(尤其是静态文件),进一步提高性能。除此之外,存储类型可以升级为高级存储,利用固态硬盘进一步加快磁盘输入/输出并提高性能。
  • 完全管理:硬件由 Azure 完全管理,用于任何更新/维护。
  • 安全性:磁盘上存储的所有数据都是加密的,对 Azure Storage 中数据的访问进一步支持私有、公共和匿名模式。
  • 现收现付:和所有其他 Azure 服务一样,Azure Storage 也支持基于数据/运营规模的现收现付模式。

Azure 存储帐户

让我们创建一个简单的控制台应用,将文件上传到 Blob 并从 Blob 下载文件。要与 Azure Storage 服务通信,先决条件是创建一个 Azure Storage 帐户,该帐户提供对所有 Azure Storage 服务的访问,并通过 Azure Storage 的唯一命名空间,使我们能够通过 HTTP/HTTPS 访问存储在 Azure Storage 中的数据。要创建 Azure 存储帐户,请执行以下步骤:

  1. Sign in to the Azure portal, click Create resource, and select Storage Account. This will open the Create storage account page. Fill in the details as shown in the following screenshot and click Review + create:

    https://github.com/OpenDocCN/freelearn-csharp-zh/raw/master/docs/enter-app-dev-cs9-dnet5/img/Figure_9.8_B15927.jpg

    图 9.8–创建 Azure 存储帐户

    标准层有两个可能值的重要属性,对于账户类,我们有:

    • StorageV2(通用 v2) :最新版本的账户类型,允许访问所有存储类型,如文件、blobs、队列等。这对于新创建的存储帐户更为可取。
    • 存储(通用 v1) :旧版本的账户类型,允许访问所有存储类型,如文件、博客、队列等。
    • 【blob 存储:只支持 blob 存储的账户类型。

另一个是复制,支持跨数据中心/区域的存储数据复制。以下屏幕截图显示了可能的值:

https://github.com/OpenDocCN/freelearn-csharp-zh/raw/master/docs/enter-app-dev-cs9-dnet5/img/Figure_9.9_B15927.jpg

图 9.9–Azure 存储帐户中的复制选项

  1. 创建账户后,导航至存储账户 | 键。复制连接字符串值。

  2. 创建新的.NET 5 控制台应用并安装Azure.Storage.Blobs NuGet 包。

  3. 要将内容上传到 Azure Storage,我们需要首先创建一个容器。如果容器不存在,我们将使用Azure.Storage.Blobs.BlobContainerClient类及其CreateIfNotExistsAsync方法来创建容器。有了这个,更新Main方法,如下面的代码片段所示:

    static async Task Main(string[] args)
    {
                string connectionString = 
                "CONNECTION_STRING";
                string containerName = "fileuploadsample";
                string blobFileName = "sample.png";
                // Upload file to blob            
                BlobContainerClient containerClient = new 
                BlobContainerClient(connectionString, 
                containerName);
                await containerClient.CreateIfNotExists
                Async(PublicAccessType.None); 
                //Making blob private.
    }
    
  4. 接下来,我们需要将文件上传到我们将使用Azure.Storage.Blobs.BlobClient的容器中,该容器将连接字符串、容器名称和 blob 名称作为输入参数。对于这个示例,我们正在将一个本地sample.png文件上传到 blob,我们将使用FileStream类读取该文件,并将其传递给Azure.Storage.Blobs.BlobClient类的UploadAsync方法。在Main方法中创建容器后,添加以下代码片段:

    BlobClient blobClient = new BlobClient(connectionString, 
    containerName, blobFileName);
    using FileStream fileStream = File.OpenRead(blobFileName);
    await blobClient.UploadAsync(fileStream, true);
    fileStream.Close();
    Console.WriteLine(blobClient.Uri.ToString());
    

在此阶段运行示例会将文件上传到 blob,并在命令行中显示 blob 网址。然而,如果我们试图访问该网址,它将无法访问,因为创建的 blob 是私有的。要访问私有 blobs,我们需要生成一个共享访问签名 ( SAS )并将其作为查询字符串参数传递。为此,在Main方法中的上传代码后添加以下代码:

BlobSasBuilder sasBuilder = new BlobSasBuilder()
{
    BlobContainerName = containerClient.Name,
    Resource = "b", // c for container
    BlobName = blobClient.Name
};
sasBuilder.ExpiresOn = DateTimeOffset.UtcNow.AddHours(1);
sasBuilder.SetPermissions(BlobContainerSasPermissions.Read);
if (blobClient.CanGenerateSasUri)
{
    Uri blobSasUri = blobClient.GenerateSasUri(sasBuilder);
    Console.WriteLine(blobSasUri.ToString());
}
Console.ReadLine();

这里,我们使用Azure.Storage.Sas.BlobSasBuilder类来配置各种参数,如权限和到期时间,为上传的文件生成一个 SAS URI。最后,前面代码的输出如下图所示:

https://github.com/OpenDocCN/freelearn-csharp-zh/raw/master/docs/enter-app-dev-cs9-dnet5/img/Figure_9.10_B15927.jpg

图 9.10–Blob 上传输出和存储资源管理器

这是一个利用 Azure Storage 上传文件的小示例。这可以作为一个应用编程接口进一步增强,最终可以用于文件上传和下载场景。对于我们的电子商务应用,我们将使用 Azure Blob 来存储产品的图像。

注意

有关 Azure 存储的更多高级概念和示例,请参考以下链接:

https://docs . Microsoft . com/en-us/azure/storage/common/storage-account-overview

https://github . com/Azure/Azure-SDK-for-net/tree/master/SDK/storage/Azure。Storage.Blobs/samples

https://docs . Microsoft . com/en-us/azure/cdn/cdn-create-a-storage-account-with-cdn

在本节中,我们已经讨论了中可用的各种数据提供程序.NET 5。然而,简化持久化数据的一个重要库是 EF。让我们看看如何将 EF 集成到.NET 5 应用。

与英孚核心合作

EF Core 是一个 ORM,推荐给任何使用关系数据库作为数据存储的 ASP.NET Core 5 应用。早些时候,我们看到在 ADO.NET,我们必须创造Connection、Command和Reader物体。EF 通过提供抽象并允许开发人员编写应用代码来简化这个过程,并且像任何其他 ORM 一样,EF 帮助使用对象模型范式在数据库上执行各种操作。

配置 EF Core 就像安装所需的 NuGet 包一样简单,在Startup类中注入所需的服务,然后在需要的地方使用它们。作为这个过程的一部分,需要定义的关键类之一是数据库上下文,它需要继承Microsoft.EntityFrameworkCore.DbContext类。让我们看看如何在剩下的英孚核心配置中做到这一点。

配置和查询

英孚核心中的DbContext类保存了我们的应用与数据库通信所需的所有抽象,因此需要集成英孚核心的一个关键设置是定义我们特定于应用的上下文类。该类将主要保存DbSet类型的公共属性形式的所有 SQL 表/视图,如以下代码所示:

public virtual DbSet<Employee> Employees { get; set; }

这里,Employee是表示我们数据库中的表的 POCO 类。应用上下文类应该具有接受DbContextOptions或DbContextOptions<T>并将其传递给基类的参数化构造函数。

让我们基于 Razor Pages 和 SQLite 创建一个简单的 web 应用,并使用 EF Core 读取数据。对于此示例,我们将使用 SQLite 获取一个简单的员工数据库,该数据库包含具有以下数据模型的员工详细信息:

https://github.com/OpenDocCN/freelearn-csharp-zh/raw/master/docs/enter-app-dev-cs9-dnet5/img/Figure_9.11_B15927.jpg

图 9.11–员工数据库模型

如果你之前没有在剃刀页面工作过,那就不用担心;这是一个基于页面的框架,可用于在 ASP.NET Core 5 中构建数据驱动的应用,并在 第 11 章创建 ASP.NET Core 5 网络应用中进行了介绍。

现在,让我们按照以下步骤创建我们的应用:

  1. 使用命令行中的以下命令创建一个新的 Razor Pages 应用,这会在EmployeeEF文件夹内创建一个新的 Razor Pages 应用:

    dotnet new webapp -o EmployeeEF
    
  2. Navigate to the EmployeeEF folder and open it in Visual Studio Code, and then install the following NuGet packages:

    Microsoft.EntityFrameworkCore.Sqlite、Microsoft.EntityFrameworkCore.Design

    前一个包是 SQLite 的 EF Core 提供程序,后一个包用于使用 EF Core 迁移创建基于 C# POCOs 的数据库。

  3. 现在,添加Models文件夹并添加必要的 POCO 类,如下所示。这些类表示来自图 9.11 :

        public class Employee
        {
            public int EmployeeId { get; set; }
            public string Name { get; set; }
            public string Email { get; set; }
            public ICollection<Address> Address { get; set; }
        }
        public class Address
        {
            public int AddressId { get; set; }
            public int EmployeeId { get; set; }
            public string City { get; set; }
            public Employee Employee { get; set; }
        }
    

    的数据库模式

  4. 这里,数据库表中的所有列都表示为具有相关数据类型的属性。对于诸如外键之类的关系,创建子类型的属性,称为导航属性,该属性的类型由ICollection表示,而父类类型的另一个属性在子类中创建。例如,在前面的代码中,这在public Icollection<Address> Addresses和public Employee Employee属性中表示,这两个属性定义了Employee和Address表之间的外键约束。任何名为ID或<class name>ID (EmployeeID)的属性都会被自动视为主键。可以在OnModelCreating期间使用 Fluent API 或在System.ComponentModel.DataAnnotations中使用注释来进一步定义约束。有关模型创建的更多示例和详细信息,请参考https://docs.microsoft.com/en-us/ef/core/modeling。

  5. 添加一个继承自Microsoft.EntityFrameworkCore.DbContext的类,并命名为EmployeeContext。添加以下定义我们的数据库上下文的代码:

        public class EmployeeContext : DbContext
        {
            public DbSet<Employee> Employees { get; set;}
            public DbSet<Address> Addresses { get; set;}
             public EmployeeContext (DbContextOptions
             <EmployeeContext> options)
                : base(options)
            {
            }
            protected override void OnModelCreating
            (ModelBuilder modelBuilder)
            {
                modelBuilder.Entity<Employee>().ToTable
                ("Employee");
                modelBuilder.Entity<Address>().ToTable
                ("Address");
            }
        }
    
  6. 在appsettings.json中添加连接字符串。当我们使用 SQLite 时,在数据源中指定文件名应该足够好。但是,这将根据提供商而改变:

      "ConnectionStrings": {
        "EmployeeContext": "Data Source=Employee.db"
      }
    
  7. 现在,在Startup类中注入数据库上下文类,以便它在整个应用中可用。在这里,我们还传递连接字符串,并配置任何附加选项,如重试策略、查询日志记录等。在ConfigureServices方法中添加以下代码:

    Services.AddDbContext<EmployeeContext>(options =>
    { 
     options.UseSqlite(Configuration.GetConnectionString("EmployeeContext"));
    });
    

我们几乎完成了英孚核心的设置。现在,让我们创建一些可用于播种数据库的示例数据:

  1. 为此,我们将在数据库上下文中创建一个扩展方法,并在启动时调用它。创建一个DbContextExtension类,并向其中添加以下代码。这段代码什么都不做但是在数据库中添加了一些记录:

        public static void SeedData(this EmployeeContext 
        context)
        {
            SeedEmployees(context);            
        }
        private static void SeedEmployees(EmployeeContext 
        context)
        {
            if (context.Employees.Any())
            {
                return;
            }
            var employees = new Employee[]
            {
                new Employee{EmployeeId = 1, Name = 
                "Sample1", Email="Sample@sample.com"},
                new Employee{EmployeeId = 2, Name = 
                "Sample2", Email="Sample2@sample.com"},
            };
            context.Employees.AddRange(employees);
            var adresses = new Address[]
            {
             new Address{AddressId = 1, City = "City1", 
             EmployeeId = 1},
             new Address{AddressId = 2, City = "City2", 
             EmployeeId = 1},
             new Address{AddressId = 3, City = "City1", 
             EmployeeId = 2},
            };
            context.Addresses.AddRange(adresses);
            context.SaveChanges();
        }
    
  2. 打开Startup,在Configure方法中,添加以下代码,在应用启动时播种数据。由于我们在插入之前检查了雇员表,应用的多次运行不会覆盖数据:

    using (var serviceScope = app.ApplicationServices.GetService<IServiceScopeFactory>().CreateScope())
    {
                    using (var context = serviceScope.
                    ServiceProvider.GetRequiredService
                    <EmployeeContext>())
                    {
                            context.SeedData();
                    }
    }
    
  3. 现在,运行dotnet build并修复任何构建错误。为了从我们的模型中生成数据库并填充数据库,我们需要在本地或全局安装dotnet-ef并运行如下迁移命令,这将生成Migrations文件夹,然后生成Employee.db文件,这是我们的 SQLite 数据库:

    Dotnet tool install –global dotnet-ef //Installing dotnet ef.
    Dotnet ef migrations add InitialCreate //Generate DB migrations.
    Dotnet ef database update //Update database.
    
  4. 现在,到阅读Employee表,导航到Index.cshtml.cs并粘贴以下代码。这里我们正在注入EmployeeContext然后从员工表中读取数据:

    public class IndexModel : PageModel
        {
            private readonly EmployeeContext context;
            public IndexModel(EmployeeContext context)
            {
                this.context = context;
            }
             public Ilist<Employee> Employees { get; set; }
             public async Task OnGetAsync()
            {
                this.Employees = await this.context.
                Employees.Include(x => x.Addresses).
                AsNoTracking().ToListAsync();
            }
        }
    
  5. Update Index.cshtml with the following code, which loops through the employee records populated in the Employees property of IndexModel and displays them:

    <table class="table">
    <tbody>
        @foreach (var item in Model.Employees)
        {<tr>
                <td>@Html.DisplayFor(modelItem => 
                item.EmployeeId)</td>
                <td>@Html.DisplayFor(modelItem => 
                item.Name)</td>
                <td>@Html.DisplayFor(modelItem => 
                item.Email)</td>
                <td>
                    @foreach (var address in item.Address)
                    {
                        @Html.DisplayFor(modelItem => 
                        address.City) @Html.DisplayName(" 
                         ")
                    }
                </td>
            </tr>
        }
    </tbody>
    </table>
    

    运行这段代码后,我们可以在浏览器中看到以下输出:

https://github.com/OpenDocCN/freelearn-csharp-zh/raw/master/docs/enter-app-dev-cs9-dnet5/img/Figure_9.12_B15927.jpg

图 9.12–员工应用输出

类似地,DbContext类中还有其他可用的方法,如Add()、Remove()和Find(),来执行各种 CRUD 操作,还有方法,如FromSqlRaw()来执行原始 SQL 查询或存储过程。

这是一个非常简单的例子,它的主要目的是展示 EF Core 对于现实应用的能力。我们可以使用包含所有 CRUD 方法的通用存储库和特定存储库的存储库模式来对表执行专门的查询。此外,一个工作模式单元可以用于事务。

代码优先还是数据库优先

在之前的示例中,我们有新创建的概念验证操作系统,并由此生成了一个数据库,这种从概念验证操作系统生成数据库的方式被称为代码优先方法。正如定义所示,我们首先定义了概念验证操作系统,然后生成数据库。

然而,很多时候,尤其是在迁移场景中或者有专门数据库团队的情况下,我们需要从数据库表中生成概念验证操作系统。英孚核心通过数据库优先方法支持这样的场景,其中模型和应用数据库上下文类是从现有数据库生成的。从数据库模型生成概念验证操作系统的过程被称为支架。在这种方法中,我们可以使用.NET CLI 或 Visual Studio 中的包管理器控制台,并使用Scaffold-DbContext命令,该命令接受各种参数,如数据库连接字符串和应用数据库上下文类的名称,然后生成 EF Core 所需的所有必需类。

其余的配置与代码优先的方法相同。带有各种参数的示例脚手架命令如下所示:

Scaffold-DbContext "Data Source=.;Initial Catalog=Employee.DB;Trusted_Connection=True;" Microsoft.EntityFrameworkCore.SqlServer -Namespace Api.Data.Models -ContextNamespaceApi.Data -ContextDir Api.Data/Abstraction -Context EmployeeContext -Force

在这个命令中,我们正在读取一个数据库Employee.DB,生成Namespace Api.Data.Models内的所有模型,生成Api.Data/Abstraction内的上下文,并命名上下文EmployeeContext。在数据库优先中,类之间的关系是使用 Fluent API 定义的,而不是注释。

这里有一件事就是每次我们运行这个命令,所有的 POCOs 都会和应用上下文类一起被覆盖。其次,这个命令生成一个带有protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)方法的上下文类。仅当上下文类需要维护连接字符串和其他 EF Core 选项时,才需要此方法。然而,在大多数现实世界的应用中,连接字符串是在appsettings.json中维护的,而 EF Core 是在Startup类中配置的,因此可以删除该方法。

这意味着每次脚手架搭建之后都要进行清理,避免任何定制的更好方法是为我们的应用数据库上下文创建一个分部类,并在那里进行所有定制,例如为存储过程添加特定的模型或定义任何特定于应用的约束。这样,无论何时我们构建应用,定制都不会被覆盖,这仍然允许我们从数据库自动生成类。

选择数据库优先方法还是代码优先方法完全取决于开发团队,因为这两种方法各有利弊,并且没有任何特定的特性在一种方法中可用但在另一种方法中不可用。

注意

Scaffold-DbContext支持多参数;例如,您可以指定–一个模式,用于为模式生成概念验证操作,等等。详情请参考https://docs . Microsoft . com/en-us/ef/core/management-schemas/scaffolding?tab = dotnet-core-CLI。

基于这种理解,让我们在下一节中创建将在我们的企业应用中使用的数据访问服务。

使用 Azure Cosmos DB 设计数据访问服务

由于 NoSQL 数据库都是关于快速访问和高可伸缩性的,NoSQL 的模式是非规范化的,因此存在很大的数据冗余可能性。让我们将需求从 第 1 章设计和架构企业应用映射到各个实体。下图显示了体系结构中各种服务的快速更新:

https://github.com/OpenDocCN/freelearn-csharp-zh/raw/master/docs/enter-app-dev-cs9-dnet5/img/Figure_9.13_B15927.jpg

图 9.13–电子商务应用中的服务

为了更容易理解,我们将在进入概念验证之前在 JSON 中表示实体:

  • 用户容器:这个容器会保存所有的用户档案信息,比如姓名和地址。对于此容器,Email字段用作分区键:

    {
      "Id": "1",
      "Name": "John",
      "Email": "John@xyz.com",
      "Address":[{"Address1":"Gachibowli","City":
          "Hyderabad","Country":"India"}],
      "PhoneNumber":12345
    }
    
  • 产品容器:产品容器用于浏览产品并保存相关字段,支持分类搜索和到货日期排序。该模式将是我们在前面的示例中使用的模式,并且Name字段用作分区键。

  • 订单容器:订单容器将存储特定用户的所有历史订单及其状态。该容器将保存具有相关状态的购物车,并在下订单后更新状态。对于此容器,Id字段用作分区键:

    {
      "Id": "1",
      "UserId": "1",
      "Products": [{"Id":"1","Name": 
          "T-Shirt","Quantity": 1,"Price": 10}],
      "OrderStatus" : "Processed",
      "OrderPlacedDate" : "20-02-2020T00:00:00Z",
      "ShippingAddress": {"Address1":"Gachibowli",
          "City":"Hyderabad","Country":"India"},
      "TrackingId": 1,
      "DeliveryDate":"28-02-2020T00:00:00Z"
    }
    
  • 发票容器:发票容器将保存与特定订单的发票相关的所有信息。对于此容器,Id字段用作分区键:

    {
      "Id": "1",
      "OrderId": "1",
      "PaymentMode": "Credit Card",
      "ShippingAddress": {"Address1":"Gachibowli",
          "City":"Hyderabad","Country":"India"},
      "SoldBy": {"SellerName": "Seller1",  "Email": 
         "seller@ecommerce.com", "Phone": "98765432"},  
      "Products": [{"Id":"1", "Name": "T-Shirt", "Quantity": 1, "Price": 10}]
    }
    

Product和Order的组合如下图所示:

https://github.com/OpenDocCN/freelearn-csharp-zh/raw/master/docs/enter-app-dev-cs9-dnet5/img/Figure_9.14_B15927.jpg

图 9.14–电子商务数据库模型的产品和订单模式

正如你看到的,所有 1:N 的关系都是通过将子项目嵌入一个数组来处理的。类似地,Invoice和User实体模式如下图所示:

https://github.com/OpenDocCN/freelearn-csharp-zh/raw/master/docs/enter-app-dev-cs9-dnet5/img/Figure_9.15_B15927.jpg

图 9.15–电子商务数据库模型的发票和用户模式

在我们的企业应用中,我们将有一个服务与 Azure Cosmos DB 数据库交互。这项服务包括以下三个项目,下面将对此进行解释:

  • Packt.Ecommerce.Data.Models
  • Packt.Ecommerce.DataStore
  • Packt.Ecommerce.DataAccess

第一个项目是Packt.Ecommerce.Data.Models,它是一个. NET 标准 2.1 库,包含了我们所有与数据库通信的概念验证操作系统。正如前面所讨论的,所有的概念验证对象都有一个共同的id属性和前面章节中 JSON 模式中描述的其他属性。

小费

如果有示例 JSON,我们可以在 C#类生成工具中使用 JSON。

Packt.Ecommerce.DataStore是. NET Standard 2.1 库,是保存通用存储库和实体特定存储库的存储库层。这个项目中一个重要的类是BaseRepository,它有以下方法,每个方法调用CosmosClient类各自的方法:

  • GetAsync(string filterCriteria):基于filterCriteria从容器中获取记录。如果filterCriteria为空,则检索该容器中的所有记录。
  • GetByIdAsync(string id, string partitionKey):这个方法有助于通过容器的 ID 和分区键从容器中检索任何记录。
  • AddAsync(Tentity entity, string partitionKey):这个方法允许我们将记录插入容器。
  • ModifyAsync(Tentity entity, string partitionKey):这个方法允许我们在容器中UPSERT(如果有记录,修改,否则插入)一个记录。
  • RemoveAsync(string id, string partitionKey):该方法允许从容器中删除记录。

由于在 Azure Cosmos DB 中,每个记录都由 ID 和分区键的组合唯一标识,因此所有这些方法都接受分区键和id。由于这是一个通用存储库,类的签名如下,这允许我们为应用传递任何 POCO,并在相应的容器上执行 CRUD 操作:

public class BaseRepository<Tentity> : IbaseRepository<Tentity>
where Tentity : class

所有这些方法都需要一个Microsoft.Azure.Cosmos.Continer对象,我们为其创建一个readonly私有成员,该成员在类的构造函数中初始化,如下所示:

        private readonly Container container;
        public BaseRepository(CosmosClient cosmosClient, 
        string databaseName, string containerName)
        {
            if (cosmosClient == null)
            {
                throw new Exception("Cosmos client is 
                 null");
            }
            this.container = cosmosClient.GetContainer
            (databaseName, containerName);
        }

现在,CosmosClient将通过依赖注入进入系统,并在static类中配置。作为最佳实践,建议在应用的生命周期中只有一个CosmosClient实例,以更好地重用连接。因此,我们将在 ASP.NET Core 5 依赖注入容器中将其配置为单例。我们过一会儿再谈这个。

回到存储库层,BaseRepository在以下具体类中被继承,每个存储库代表一个相应的容器:

  • ProductRepository
  • UserRepository
  • OrderRepository
  • InvoiceRepository

以ProductRepository为例,它将具有以下实现,其中我们使用Ioptions模式传递CosmosClient的单例实例和附加属性:

    public class ProductRepository : 
    BaseRepository<Product>, IproductRepository
    {
        private readonly Ioptions<DatabaseSettingsOptions> 
        databaseSettings;
        public ProductRepository(CosmosClient, 
        Ioptions<DatabaseSettingsOptions> 
        databaseSettingsOption)
            : base(cosmosClient, databaseSettingsOption.
              Value.DataBaseName, "Products")
        {
            this.databaseSettings = databaseSettingsOption;
        }
    } 

所有的其他存储库将遵循类似的结构。每个存储库将实现自己的接口来支持依赖注入。

注意

这些存储库将随着我们应用实现的进展而发展。

下一个项目是Packt.Ecommerce.DataAccess,是一个以 Web API 为目标的项目.NET 5,并将主要拥有所有的控制器来公开我们的存储库。每个存储库都是与相应控制器的 1:1 映射。例如,会有ProductsController将ProductRepository方法公开为一个 REST 应用编程接口。所有控制器都将使用构造函数注入来实例化它们相应的存储库。Packt.Ecommerce.DataAccess中很重要的一点就是 Azure Cosmos DB 数据库的配置。各种控制器的设计将非常类似于Packt.Ecommerce.Product网络应用编程接口的设计,这将在 第 10 章中讨论,创建 ASP.NET Core 5 网络应用编程接口。

首先,我们将在appsettings.json中有一个对应的部分,如下所示:

  "CosmosDB": {
    "DataBaseName": "Ecommerce",
    "AccountEndPoint": "",
    "AuthKey": ""
  }

注意

对于本地开发环境,我们将使用管理用户机密,这里解释一下:https://docs . Microsoft . com/en-us/aspnet/core/security/app-Secrets?视图=aspnetcore-5.0 &选项卡=窗口。我们将设置以下值:

{
  "CosmosDB:AccountEndPoint": "", //Cosmos DB End Point
  "CosmosDB:AuthKey": "" //Cosmos DB Auth key
}

注意

但是,一旦部署了服务,就应该使用 Azure Key Vault,如中的 第 6 章配置中所述。净芯。

我们将定义一个包含依赖注入映射的扩展类。这里显示了其中的一个片段:

    public static class RepositoryExtensions
    {
        public static IserviceCollection 
        AddRepositories(this IserviceCollection services)
        {
            services.AddScoped<IproductRepository, 
            ProductRepository>();
            return services;
        }
    }

类似地,所有的存储库都会被映射。然后,我们将通过在ConfigureServices方法中添加以下代码,在Startup类和 Azure Cosmos DB 配置中对此进行配置:

services.AddOptions();
services.Configure<DatabaseSettingsOptions>(this.Configuration.GetSection("CosmosDB"));
string accountEndPoint = this.Configuration.GetValue<string>("CosmosDB:AccountEndPoint");
string authKey = this.Configuration.GetValue<string>("CosmosDB:AuthKey");
services.AddSingleton(s => new CosmosClient(accountEndPoint, authKey));
services.AddRepositories();

一旦完成配置,该服务就可以在其他服务中使用,例如Products、Orders和Invoice。这个库将拥有所有必要的 REST APIs 来对各种实体执行 CRUD 操作。

这结束了对各种实体执行 CRUD 操作的数据访问服务的创建,所有操作都作为 API 公开。该服务将从我们将在 第 10 章中开发的所有其他服务中调用,创建 ASP.NET Core 5 网络应用编程接口。

总结

在本章中,我们了解了中提供的各种持久性选项.NET 5,从处理文件和目录的 API 到数据库,如微软 SQL Server 和 Azure Cosmos DB。

我们还了解了表单、表单的重要性,以及如何在使用微软 SQL Server 时使用 EF Core 构建持久层。在此过程中,我们使用 Azure Cosmos DB SDK 为我们的电子商务应用构建了一个数据访问层。一些关键要点是我们在 SQL 和 NoSQL 之间做出的设计决策,以及我们如何用应用逻辑抽象数据层和用户界面层,以帮助您构建可扩展的企业应用。

在下一章中,我们将研究 RESTful APIs 的基础和 ASP.NET Core 5 Web API 的内部结构,并进一步为电子商务应用构建各种 RESTful 服务。

问题

  1. Say you are migrating an existing web application to use EF Core; however, there isn’t any change in the database schema and an existing one can be used as is. What is the preferable mode to use EF Core?

    a.数据库优先

    b.代码优先

    c.两者

  2. If we are building a recommendation system for our e-commerce application and we are using Azure Cosmos DB, what API is best recommended in this scenario?

    a.核心应用编程接口

    b.蒙古应用编程接口

    c.卡珊德拉应用编程接口

    d.格雷林图形应用编程接口

  3. I created a container in SQL API-based databases to store user profile information and defined Email as the partition key. My system has 100 unique emails. How many logical partitions will my container have?

    a.1.

    b.0.

    c.100.

    d.Azure Cosmos DB 不支持逻辑分区。

  4. Which of the following classes is derived from the TextWriter class?

    a.StreamWriter

    b.BinaryWriter

    c.XMLWriter

进一步阅读

下面提供了一些链接来进一步理解本章的主题:

十、创建 ASP.NET Core 5 网络应用编程接口

近年来,web 服务已经成为 web 应用开发的重要组成部分。随着需求的不断变化和业务复杂性的增加,松散耦合 web 应用开发中涉及的各种组件/层非常重要,没有什么比将应用的用户界面部分与核心业务逻辑解耦更好的了。这就是使用 RESTful 方法的 web 服务的简单性帮助我们开发可扩展的 web 应用的地方。

在本章中,我们将学习如何使用ASP.NET核心网络应用编程接口来构建 RESTful 服务,同时,我们将构建电子商务应用所需的所有应用编程接口。

在本章中,我们将详细介绍以下主题:

  • 表征状态转移介绍 ( REST )
  • 了解 ASP.NET Core 5 网络应用编程接口的内部结构
  • 使用控制器和动作处理请求
  • 与数据层的集成
  • 了解 gRPC

技术要求

这一章,你将需要 C#的基础知识.NET Core、web APIs、HTTP、Azure、依赖注入、邮递员等.NET 命令行界面。

本章的代码可以在这里找到:https://github . com/PacktPublishing/Enterprise-Application-Development-with-C-Sharp-9-and-.NET-5/树/主/第 10 章/测试程序。

更多代码示例,请参考以下链接:https://github . com/PacktPublishing/Enterprise-Application-Development-with-C-Sharp-9-and-.NET-5/树/主/企业% 20 应用

表征状态转移介绍

表示状态转移 ( REST )是一个用于构建 web 服务的架构指南。首先,它定义了一组在设计 web 服务时可以遵循的约束。主要的 REST 方法之一是建议 API 应该围绕资源进行设计,并且应该与媒体和协议无关。应用编程接口的底层实现独立于使用该应用编程接口的客户端。

考虑我们的电子商务应用的一个例子,假设我们正在使用产品的搜索字段在用户界面上搜索产品。应该有一个为产品创建的应用编程接口,在这里,产品只不过是电子商务应用上下文中的一种资源。该 API 的 URI 可能类似于以下内容,其中明确表示我们正在尝试对产品实体执行GET操作:

GET http://ecommerce.packt.com/products

应用编程接口的响应应该独立于调用应用编程接口的客户端,也就是说,在这种情况下,我们使用浏览器在产品搜索页面上加载产品列表。但是,相同的应用编程接口也可以在移动应用中使用,而无需任何更改。其次,在这种情况下,为了在内部检索产品信息,应用可能使用一个或多个物理数据存储;但是,这种复杂性对客户端应用是隐藏的,API 作为单个业务实体——产品——暴露给客户端。虽然 REST 原则没有规定协议是 HTTP,但是大多数 RESTful 服务都是建立在 HTTP 之上的。基于 HTTP 的 RESTful APIs 的一些关键设计原则/约束/规则如下:

  • 确定系统的业务实体,并围绕这些资源设计应用编程接口。在我们的电子商务应用中,我们所有的 API 都围绕着资源,比如产品、订单、支付和用户。

  • REST APIs 应该有一个统一的接口,帮助它独立于客户端。由于所有的应用编程接口都需要面向资源,每个资源都由一个 URI 唯一标识;此外,对资源的各种操作由 HTTP 动词唯一标识,如GET、POST、PUT、PATCH和DELETE。例如,应该使用GET ( http://ecommerce.packt.com/products/1)来检索 ID 为1的产品。同样,应该使用DELETE ( http://ecommerce.packt.com/products/1)来删除产品。

  • 由于 HTTP 是无状态的,REST 为 RESTful APIs 规定了许多事情。这意味着 API 应该是原子的,并在同一个调用中完成请求的处理。任何后续请求,即使来自同一个客户端(同一个 IP),都会被视为新请求。例如,如果应用编程接口接受身份验证令牌,它应该接受每个请求的身份验证。无状态的一个主要优势是服务器最终可以实现的可伸缩性,因为客户端可以对任何可用的服务器进行 API 调用,并且仍然会收到相同的响应。

  • 除了发回响应之外,应用编程接口还应该利用 HTTP 状态代码和响应头向客户端发送任何附加信息。例如,如果一个响应可以被缓存,那么应用编程接口应该向客户端发送相关的响应头,以便它可以被缓存。响应缓存,在 第 8 章中讨论,理解缓存就是基于这些头。另一个例子是,在成功和失败的情况下,应用编程接口都应该返回相关的 HTTP 状态代码,也就是说,1xx表示信息,2xx表示成功,3xx表示重定向,4xx表示客户端错误,5xx表示服务器错误。

  • Hypermedia As The Engine Of Application State (HATEOAS): APIs should give information about the resource such that client should be easily able to discover them without any prior information relating to the resource. For example, if there is an API to create a product, once a product is created, the API should respond with the URI of that resource so that the client can use that to retrieve the product later.

    请参考以下对检索所有产品列表(GET /products)的应用编程接口的响应,该应用编程接口具有检索每个产品的更多详细信息的信息:

    {
     "Products": [
     {
    	"Id": "1",
    	"Name": "Men's T-Shirt",
    	"Category": "Clothing"
    	"Uri": "http://ecommerce.packt.com/products/1"
     }
     {
    	"Id": "2",
    	"Name": "Mastering enterprise application development Book",
    	"Category": "books"
    	"Uri": "http://ecommerce.packt.com/products/2"
     }		
    ]
    }
    

前面的例子是实现HATEOAS原则的一种方式,但是它可以用一种更具描述性的方式来设计,比如包含关于被接受的 HTTP 动词、关系等信息的响应。

REST 成熟度模型

这些是各种各样的指导方针,一个应用编程接口应该遵循这些指导方针,才能使它成为 RESTful。然而,没有必要遵循所有的原则来使它完美地 RESTFUL。更重要的是,应用编程接口应该实现业务目标,而不是 100%符合 REST。RESTful API 设计专家 Leonard Richardson 提出了以下模型来对 API 的成熟度进行分类:

  • 0 级:普通旧 XML 的沼泽–任何有一个POST URI 来执行所有操作的应用编程接口都属于这一类。一个例子是基于 SOAP 的 web 服务,它只有一个 URI,所有的操作都是基于 SOAP 信封隔离的。
  • 级别 1:资源–所有的资源都是由 URI 驱动的,每个资源都有一个专用的 URI 模式的 API 都属于这个成熟度模型。
  • 第 2 级:HTTP 动词–除了每个资源有一个单独的 URI,每个 URI 都有一个基于 HTTP 动词的单独动作。如前所述,支持使用相同 URI 和不同 HTTP 动词的GET和DELETE的产品应用编程接口属于这种成熟度模型。大多数企业应用 RESTful APIs 都属于这一类。
  • 第 3 级:HATEOAS–API 设计有所有额外的发现信息(资源的 URI,资源支持的各种操作)属于这个成熟度模型。很少有 API 符合这个成熟度级别;然而,正如前面所讨论的,重要的是,我们的 API 应该满足业务目标,并尽可能符合 RESTful 原则,而不是 100%符合但不满足业务目标。

下图说明了理查森的成熟度模型:

https://github.com/OpenDocCN/freelearn-csharp-zh/raw/master/docs/enter-app-dev-cs9-dnet5/img/Figure_10.1_B15927.jpg

图 10.1–理查森的成熟度模型

到目前为止,我们已经讨论了 REST 架构的各种原理。在下一节中,让我们开始使用 ASP.NET Core 网络应用编程接口,我们将在我们的电子商务应用中为其创建各种 RESTful 服务。

了解 ASP.NET Core 5 网络应用编程接口的内部结构

ASP.NET Core是一个统一框架,运行在之上.NET Core,用于开发 web 应用(MVC/Razor)、RESTful 服务(web API)以及最近的基于 web 程序集的客户端应用(Blazor 应用)。ASP.NET Core 应用的基本设计基于模型视图控制器 ( MVC )模式,该模式将代码分为三个主要类别:

  • 模型:这是一个 POCO 类保存数据,用于在应用的各个层之间传递数据。层次包括在存储库类和服务类之间传递数据,或者在客户端和服务器之间来回传递信息。该模型主要表示应用的资源状态或域模型,并包含您请求的信息。例如,如果我们想要存储用户简档信息,它可以由 POCO 类UserInformation来表示,并且可以包含所有的简档信息。这将进一步用于在存储库和服务类之间传递,也可以在被发送回客户端之前序列化为 JSON/XML。在企业应用中,在与数据层的集成部分为我们的电子商务应用创建模型时,我们会遇到不同类型的模型。
  • 控制器:这些是一组类,它们接收所有请求,执行所有需要的处理,填充模型,然后将其发送回客户端。在企业应用中,它们通常利用服务类来处理业务逻辑,并利用存储库来与底层数据存储进行通信。使用 ASP.NET Core 的统一方法,MVC/Razor 应用和 web API 应用都使用同一个Microsoft.AspNetCore.Mvc.ControllerBase类来定义控制器。
  • 查看:这些是代表 ui 的页面。我们从控制器中检索的所有模型都绑定到视图上的各种 HTML 控件,并呈现给用户。视图通常在 MVC/Razor 应用中很常见;对于 web API 应用,该过程结束于将模型序列化为响应。

因此,在使用 ASP.NET Core 开发的网络应用中,每当请求来自客户端(浏览器、移动应用和类似来源)时,它都会通过 ASP.NET Core 请求管道,到达与数据存储交互的控制器,以填充模型/视图模型,并将它们作为 JSON/XML 形式的响应发送回视图,以进一步绑定响应并将其呈现给用户。正如您所看到的,在控制器不知道任何用户界面方面并在当前上下文中执行业务逻辑并通过模型做出响应的情况下,存在明显的关注点分离,而另一方面,视图接收模型并使用它们在 HTML 页面中向用户呈现它们。这种关注点的分离很容易帮助单元测试应用,以及根据需要维护和扩展它。MVC 模式不仅适用于 web 应用,而且可以用于任何需要分离关注点的应用。

由于本章的重点是构建 RESTful 服务,因此我们将在本章中重点介绍 ASP.NET Core Web API,并在 第 11 章中讨论 ASP.NET MVC 和 Razor 页面创建 ASP.NET Core 5 Web 应用。

为了开发 RESTful 服务,有很多可用的框架,但以下是继续使用 ASP.NET Core 的一些优势.NET 5:

  • 跨平台支持:不像 ASP.NET,以前是.NET 框架,它与 Windows 操作系统相结合,ASP.NET Core 现在是应用的一部分,从而消除了平台依赖性,使其兼容所有平台。

  • 高度可定制的请求 管道使用中间件和支持注入各种开箱即用的模块,比如日志和配置。

  • Out-of-the-box HTTP server implementation, which can listen to HTTP requests and forward them to controllers. The server implementation includes cross-platform servers such as Kestrel, and platform-specific servers such as IIS and HTTP.sys.

    注意

    默认情况下,红隼是 ASP.NET Core 模板中使用的 HTTP 服务器;但是,这可以根据需要被覆盖。

  • 强大的工具支持,以 VS Code、Visual Studio 和 DOTNET CLI 的形式,加上项目模板,意味着开发人员只需很少的设置就可以开始着手实现业务逻辑。

  • 最后,整个框架是开源的,可在https://github.com/aspnet/AspNetCore获得。

所以现在我们知道为什么选择 ASP.NET Core 作为我们开发 RESTful 服务的框架了。现在让我们研究一些有助于执行请求的关键组件,并使用以下命令创建一个示例 web API:

dotnet new webapi -o TestApi

前面的命令成功执行后,让我们导航到TestApi文件夹,在 Visual Studio 2019 或 VS Code 中打开,查看生成的各种文件,如下图截图所示:

https://github.com/OpenDocCN/freelearn-csharp-zh/raw/master/docs/enter-app-dev-cs9-dnet5/img/Figure_10.2_B15927.jpg

图 10.2–用 VS 代码测试网络应用编程接口项目

这里可以看到几个类,比如Program、Startup,还有设置文件,比如appsettings.json,用来运行一个 web API 项目,还有WeatherForecast,是控制器类中使用的一个模型类。让我们在下面的部分中检查测试应用编程接口的每个组件。

程序和启动类

程序和启动类是用于引导 ASP.NET Core 中的网络应用编程接口项目的类。让我们按照以下步骤来看看程序类执行的活动:

  1. Program类是我们的网络应用编程接口的入口点,它告诉 ASP.NET Core 每当有人执行网络应用编程接口项目时,就从Main方法开始执行。首先,这是用于引导应用的类。如您所见,它遵循控制台应用的典型惯例,具有static void Main方法和.NET Core 寻找开始执行的主要方法。类似地,在 ASP.NET Core 应用中,Program类有这个static void Main方法,ASP.NET Core 运行时寻找这个方法来开始应用的执行。

  2. 在Main方法中,我们有CreateHostBuilder(args).Build().Run(),它调用CreateHostBuilder方法来获取IHostBuilder的实例,对于我们的应用来说,这个实例只不过是宿主。前面,我们讨论了这样一个事实,即 ASP.NET Core 附带了一个内置的 HTTP 服务器实现和各种中间件来插入,主机只不过是一个封装这些组件的对象,就像默认为红隼的 HTTP 服务器、所有中间件组件以及注入的任何附加服务,如日志记录。CreateHostBuilder内部调用CreateDefaultBuilder和ConfigureWebHostDefaults。

  3. CreateDefaultBuilder方法加载来自各种提供者的所有配置,例如appsettings.json、环境变量和任何命令行参数(请参见args作为参数传递)。然后,加载默认日志记录提供程序,以便将所有日志记录到控制台和调试窗口中。

  4. ConfigureWebHostDefaults启用红隼作为默认 HTTP 服务器,并初始化Startup类。

  5. Additionally, we can call additional methods on both CreateDefaultBuilder and ConfigureWebHostDefaults. Let’s say, for example, that we wanted to call an additional configuration provider, along the lines of what we implemented in Chapter 6, Configuration in .NET Core, or change some Kestrel default parameters, we can configure additional parameters as shown in the following code snippet:

    public static IHostBuilder CreateHostBuilder(string[] args) =>
        Host.CreateDefaultBuilder(args)
            .ConfigureWebHostDefaults(webBuilder =>
            {
                webBuilder.UseStartup<Startup>()
                .ConfigureKestrel((options) =>
                {
                    options.AddServerHeader = false;
                });
            });
    

    从前面的代码中,我们看到最后,在IhostBuilder的对象上,应用了以下内容:

    • 调用Build()方法运行动作并初始化Host。
    • 调用Run()方法保持Host运行。

现在我们已经将Host加载了所有默认组件,并且它已经启动并运行。作为其中的一部分,我们已经看到其中的一个步骤是实例化Startup类。让我们研究一下Startup类是干什么的,看看它如何被用来注入额外的 ASP.NET Core 类/特定于应用的类(存储库、服务、选项)和中间件。Startup类如下所示:

public Startup(IConfiguration configuration)
{
    Configuration = configuration;
}
public IConfiguration Configuration { get; }
public void ConfigureServices(IServiceCollection services)
{
    services.AddControllers();
}
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
    if (env.IsDevelopment())
    {
        app.UseDeveloperExceptionPage();
    }
    app.UseHttpsRedirection();
    app.UseRouting();
    app.UseAuthorization();
    app.UseEndpoints(endpoints =>
    {
        endpoints.MapControllers();
    });
}

如您所见,除了IConfiguration属性之外,Startup类主要有两种方法,使用构造函数注入在构造函数中初始化为应用配置(该配置在主机设置期间加载)。此属性还可以在Startup类中用于加载任何配置,如中的 第 6 章配置中所述。网芯。Startup类中的另外两个方法具有以下功能:

  • ConfigureServices: This method is used to inject any ASP.NET Core provided services so that applications can use those services. A few of the common services that enterprise applications can inject are shown in the following code snippet:

    services.AddAuthentication() // To enable authentication.
    services.AddControllers(); // To enable controllers like web API.
    services.AddControllersWithViews(); // To enable controller with views.
    services.AddDistributedMemoryCache(); // To enable distributed caching.
    services.AddApplicationInsightsTelemetry(appInsightInstrumentKey); // To enable application insights telemetry.
    services.AddDbContext<EmployeeContext>(options =>
    { 
     options.UseSqlite(Configuration.GetConnectionString("EmployeeContext"));
    }); // Integrating SQL lite.
    

    除了 Apart 芯提供的服务,我们还可以注入任何针对我们应用的定制服务。例如,ProductService可以映射到IProductService并可用于整个应用。首先,这是我们可以使用将任何东西垂直放入依赖注入 ( DI )容器的地方,如中的 第 5 章 、依赖注入所述.NET 。ConfigureServices方法的一些要点如下:

  • 由于这将加载特定于应用的服务,因此该方法是可选的。

  • 所有服务,包括 ASP.NET Core 服务和定制服务,都可以接入到应用中,并作为IServiceCollection的扩展方法提供。

  • Configure: This method is used to integrate all the middlewares required to be applied to the request pipeline. This method primarily controls how applications respond to the HTTP requests, that is, how applications should respond to exceptions or how they should respond to static files, or how URI routing should happen. All can be configured in this method. Additionally, any specific handling on a request pipeline, such as calling a custom middleware or adding specific response headers, or even defining a specific endpoint, can be implemented here in the configure method. So, apart from what we have seen earlier, the following code snippet shows a few common additional configurations that can be integrated in this method:

    // Endpoint that responds to /subscribe route.
    app.UseEndpoints(endpoints =>
    {
     endpoints.MapGet("/subscribe", async context =>
     {   
      await context.Response.WriteAsync("subscribed");
     });
    });
    // removing any unwanted headers.
    app.Use(async (context, next) =>
    {
     context.Response.Headers.Remove("X-Powered-By");
     context.Response.Headers.Remove("Server");
     await next().ConfigureAwait(false);
    });
    

    这里,app.UseEndpoints正在为匹配/subscribe的 URI 配置响应。app.UseEndPoints与路由规则一起工作,并在使用控制器和动作处理请求一节中解释,而app.Use则用于添加内联中间件。在这种情况下,我们将从响应中删除X-Powered-By, Server响应头。

总而言之,Program和Startup类在引导应用,然后根据需要定制应用服务和 HTTP 请求/响应管道方面起着至关重要的作用。现在让我们看看中间件如何帮助定制 HTTP 请求/响应管道。

注意

Configure和ConfigureServices方法可以是Program类的一部分,因为IHostBuilder、IWebHostBuilder和WebHostBuilderExtensions中有可用的方法。然而,使用Startup类使代码更加清晰。

理解中间件

我们提到中间件已经有一段时间了,所以让我们了解什么是中间件,以及我们如何构建一个中间件并在我们的企业应用中使用它。中间件是拦截传入请求,对请求执行一些处理,然后根据需要将其交给下一个中间件或跳过它的类。中间件是双向的,因此所有的中间件都会拦截请求和响应。让我们假设一个应用编程接口检索产品信息,在这个过程中,它通过各种中间件。用图片的形式表现它们看起来像这样:

https://github.com/OpenDocCN/freelearn-csharp-zh/raw/master/docs/enter-app-dev-cs9-dnet5/img/Figure_10.3_B15927.jpg

图 10.3–中间件处理

每个中间件都有一个Microsoft.AspNetCore.Http.RequestDelegate实例。使用这个的结果是,中间件调用下一个中间件。通常,流程会按照您希望中间件对请求执行的一些处理逻辑来处理请求,然后调用RequestDelegate将请求移交给管道中的下一个中间件。

如果我们从制造的角度进行类比,它就像制造过程中的装配线,零件从一个工作站到另一个工作站被添加/修改,直到最终产品被生产出来。在上图中,让我们将每个中间件视为一个工作站,因此它将经历以下步骤:(下面对每个中间件的解释只是对我们理解的假设性解释;这些中间件的内部工作原理与这里解释的略有不同。更多详情可以在这里找到:https://docs . Microsoft . com/en-us/dotnet/API/Microsoft . aspnetcore . builder?view = aspnetcore-3.1&view fallbackfrom = aspnetcore-5.0。

  • 使用 HTTP 重定向:一个 HTTP 请求到达GET/Products并检查协议。如果请求是通过 HTTP 发送的,则通过 HTTP 状态代码发回重定向;如果请求在 HTTPS,它将被移交给下一个中间件。
  • 使用静态文件:如果请求是针对静态文件的(通常根据扩展名 MIME 类型来检测),这个中间件会处理请求并发回响应,否则会将请求交给下一个中间件。在这里,正如您所看到的,如果请求是针对静态文件的,那么管道的其余部分甚至不会被执行,因为这个中间件可以处理完整的请求,从而减少服务器上任何不需要的处理的负载,也减少了响应时间。这个过程也被称为短路,每个中间件都可以支持。
  • 用户退出:进一步检查请求,识别能够处理该请求的控制器/动作。如果没有匹配,这个中间件通常会用一个 404 HTTP 状态代码来响应。
  • 使用授权:这里,如果控制器/动作需要对认证用户可用,那么这个中间件会在报头中寻找任何有效的令牌并做出相应的响应。

一旦控制器从服务/存储库中获取数据,响应就以相反的顺序通过相同的中间件,即先UseAuthorization,后UseHttpsRedirection,并根据需要处理响应。

如前所述,所有中间件都是使用Configure方法安装的(在Startup类或Program类中),并使用实现IApplicationBuilder接口的类实例的扩展方法进行配置,该接口作为参数传递给Configure方法。因此,中间件执行的顺序将精确地遵循它在Configure方法中的配置方式。

有了这种理解,让我们创建一个中间件,它将用于处理我们电子商务应用的 RESTful 服务中的异常,因此我们将创建一个中间件,它将在请求管道的开始安装,然后捕获所有的异常,而不是用try…catch块污染代码。

构建定制中间件

由于中间件将在所有 RESTful 服务中被重用,我们将把中间件添加到Middlewares文件夹内的Packt.Ecommerce.Common项目中。

让我们首先创建一个概念验证类来表示错误情况下的响应。通常,这个模型将保存错误消息,一个在我们的日志存储应用洞察中搜索的唯一标识符,以及一个内部异常(如果需要)。在生产环境中,不应暴露内部异常;但是,对于开发环境,我们可以发送内部异常用于调试目的,我们将使用配置标志在中间件逻辑内部控制这种行为。因此,在此基础上,在Packt.Ecommerce.Common项目的Models文件夹中添加一个名为ExceptionResponse的类文件,并添加以下代码:

public class ExceptionResponse
{
    public string ErrorMessage { get; set; }
    public string CorrelationIdentifier { get; set; }
    public string InnerException { get; set; }
}

现在,创建另一个 POCO 类,它可以保存配置来切换在我们的响应中发送内部异常的行为。此类将使用选项模式填充,该模式已在中的 第 6 章配置中讨论过。网芯。因为它只需要保存一个设置,所以它将有一个属性。在Options文件夹中添加一个名为ApplicationSettings的类文件,然后向其中添加以下代码:

public class ApplicationSettings
{
    public bool IncludeExceptionStackInResponse { get; set; }
}

这个类将被进一步扩展,用于我们所有 API 中通用的任何配置。

导航到Middlewares文件夹并创建一个名为ErrorHandlingMiddleware的类。正如我们所讨论的,任何中间件的关键属性之一都是RequestDelegate类型的属性。此外,我们将为ILogger添加一个属性,以将异常记录到我们的日志提供程序中,最后,我们将添加一个bool includeExceptionDetailsInResponse类型的属性,以保存控制屏蔽内部异常的标志。这样,ErrorHandlingMiddleware等级将如下:

public class ErrorHandlingMiddleware
{
	private readonly RequestDelegate requestDelegate;
	private readonly ILogger logger;
	private readonly bool includeExceptionDetailsInResponse;
} 

添加一个参数化构造函数,在这里我们为日志提供者注入RequestDelegate和ILogger,为配置注入IOptions<ApplicationSettings>,并将它们分配给前面创建的属性。这里,我们再次依赖 ASP.NET Core 的构造函数注入来实例化相应的对象。有了这个,ErrorHandlingMiddleWare的构造器会出现如下:

public ErrorHandlingMiddleware(RequestDelegate, ILogger<ErrorHandlingMiddleware> logger, IOptions<ApplicationSettings> applicationSettings)
{
    NotNullValidator.ThrowIfNull(applicationSettings, nameof(applicationSettings));
    this.requestDelegate = requestDelegate;
    this.logger = logger;
    this.includeExceptionDetailsInResponse = applicationSettings.Value.IncludeExceptionStackInResponse;
}

最后,添加InvokeAsync方法,该方法将具有处理请求的逻辑,然后使用RequestDelegate调用下一个中间件。由于这是异常处理中间件作为我们逻辑的一部分,我们要做的就是将请求包装在try…catch块中。在catch块中,我们将使用ILogger将其记录到各自的日志提供者,最后发送一个对象ExceptionResponse,作为响应返回。这样,InvokeAsync会出现如下:

public async Task InvokeAsync(HttpContext context)
{
    try
    {
        if (this.requestDelegate != null)
        {
            // invoking next middleware.
		this.requestDelegate.Invoke(context).ConfigureAwait(false);
        }
    }
    catch (Exception innerException)
    {
        this.logger.LogCritical(1001, innerException, "Exception captured in error handling middleware"); // logging.
        ExceptionResponse currentException = new ExceptionResponse()
        {
            ErrorMessage = Constants.ErrorMiddlewareLog,
            CorrelationIdentifier = System.Diagnostics.Activity.Current?.RootId,
        };
        if (this.includeExceptionDetailsInResponse)
        {
            currentException.InnerException = $"{innerException.Message} {innerException.StackTrace}";
        }
        context.Response.StatusCode = StatusCodes.Status500InternalServerError;
        context.Response.ContentType = "application/json";
  await context.Response.WriteAsync(JsonSerializer.Serialize(innerException)).ConfigureAwait(false);
    }
}

现在我们可以将这个中间件注入到Startup类的Configure方法中,代码如下:

app.UseMiddleware<GlobalExceptionHandlingMiddleware>();

由于这是一个异常处理程序,建议在Configure方法的开始配置它,以便捕获所有后续中间件中的任何异常。此外,我们需要确保将ApplicationSettings类映射到一个配置,因此将以下代码添加到ConfigureServices方法中:

services.Configure<ApplicationSettings>(this.Configuration.GetSection("ApplicationSettings")); 

在appsettings.json增加相关章节:

"ApplicationSettings": {
    "IncludeExceptionStackInResponse": true
  }

现在,如果我们的任何一个 API 中有任何错误,那么响应将会像下面的代码片段中所示的那样:

{
	"ErrorMessage": "Exception captured in error handling middleware",
	"CorrelationIdentifier": "03410a51b0475843936943d3ae04240c ",
	"InnerException": "No connection could be made because the target machine actively refused it.    at System.Net.Http.ConnectHelper.ConnectAsync(String host, Int32 port, CancellationToken cancellationToken)\r\n   at System.Net.Http.HttpConnectionPool.ConnectAsync(HttpRequestMessage request, Boolean allowHttp2, CancellationToken cancellationToken)\r\n   at System.Net.Http.HttpConnectionPool.CreateHttp11ConnectionAsync(HttpRequestMessage request, CancellationToken cancellationToken)\r\n   at System.Net.Http.HttpConnectionPool.GetHttpConnectionAsync(HttpRequestMessage request, CancellationToken cancellationToken)\r\n   at System.Net.Http.HttpConnectionPool.SendWithRetryAsync(HttpRequestMessage request, Boolean doRequestAuth, CancellationToken cancellationToken)\r\n   at System.Net.Http.RedirectHandler.SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)\r\n   at System.Net.Http.DiagnosticsHandler.SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)\r\n   at Microsoft.Extensions.Http.Logging.LoggingHttpMessageHandler.SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)\r\n   at Microsoft.Extensions.Http.Logging.LoggingScopeHttpMessageHandler.SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)\r\n   at System.Net.Http.HttpClient.FinishSendAsyncBuffered(Task`1 sendTask, HttpRequestMessage request, CancellationTokenSource cts, Boolean disposeCts, CancellationToken callerToken, Int64 timeoutTime)\r\n   at Packt.Ecommerce.Product.Services.ProductsService.GetProductsAsync(String filterCriteria) in src\\platform-apis\\services\\Packt.Ecommerce.Product\\Services\\ProductsService.cs:line 82\r\n   at Packt.Ecommerce.Product.Controllers.ProductsController.GetProductsAsync(String filterCriteria) in src\\platform-apis\\services\\Packt.Ecommerce.Product\\Controllers\\ProductsController.cs:line 46\r\n   at Microsoft.AspNetCore.Mvc.Infrastructure.ActionMethodExecutor.TaskOfIActionResultExecutor.Execute(IActionResultTypeMapper mapper, ObjectMethodExecutor executor, Object controller, Object[] arguments)\r\n   at Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.<InvokeActionMethodAsync>g__Logged|12_1(ControllerActionInvoker invoker)\r\n   at Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.<InvokeNextActionFilterAsync>g__Awaited|10_0(ControllerActionInvoker invoker, Task lastTask, State next, Scope scope, Object state, Boolean isCompleted)\r\n   at Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.Rethrow(ActionExecutedContextSealed context)\r\n   at Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.Next(State& next, Scope& scope, Object& state, Boolean& isCompleted)\r\n   at Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.<InvokeInnerFilterAsync>g__Awaited|13_0(ControllerActionInvoker invoker, Task lastTask, State next, Scope scope, Object state, Boolean isCompleted)\r\n   at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.<InvokeFilterPipelineAsync>g__Awaited|19_0(ResourceInvoker invoker, Task lastTask, State next, Scope scope, Object state, Boolean isCompleted)\r\n   at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.<InvokeAsync>g__Logged|17_1(ResourceInvoker invoker)\r\n   at Microsoft.AspNetCore.Routing.EndpointMiddleware.<Invoke>g__AwaitRequestTask|6_0(Endpoint endpoint, Task requestTask, ILogger logger)\r\n   at Microsoft.AspNetCore.Authorization.AuthorizationMiddleware.Invoke(HttpContext context)\r\n   at Packt.Ecommerce.Common.Middlewares.ErrorHandlingMiddleware.InvokeAsync(HttpContext context) in src\\platform-apis\\core\\Packt.Ecommerce.Common\\Middlewares\\ErrorHandlingMiddleware.cs:line 65"
}

从前面的代码片段中,我们可以取CorrelationIdentifier,也就是03410a51b0475843936943d3ae04240c,在我们的日志提供程序 Application Insights 中搜索该值,我们可以确定关于该异常的其他信息,如下图所示:

https://github.com/OpenDocCN/freelearn-csharp-zh/raw/master/docs/enter-app-dev-cs9-dnet5/img/Figure_10.4_B15927_rej.jpg

图 10.4–在应用洞察中追踪相关标识符

CorrelationIdentifier在没有内部异常的生产环境中非常有用。

我们关于中间件的讨论到此结束。在下一节中,让我们看看控制器和动作是什么,以及它们如何帮助处理请求。

使用控制器和动作处理请求

控制器是使用 ASP.NET Core 网络应用编程接口设计 RESTful 服务的基本模块。这些是保存处理请求的逻辑的主要类,包括从数据库中检索数据,将记录插入数据库,等等。控制器是我们定义方法来处理请求的类。这些方法通常包括验证输入、与数据存储对话、应用业务逻辑(在企业应用中,控制器也会调用服务类),最后将响应序列化,并以 JSON/XML 形式使用 HTTP 协议发送回客户端。所有这些包含处理请求逻辑的方法都被称为动作。HTTP 服务器接收的所有请求都通过路由引擎传递给动作方法。然而,路由引擎根据可以在请求管道中定义的特定规则将请求转移到动作。这些规则就是我们在路由中定义的。让我们看看 URI 是如何映射到控制器中的特定动作的。

了解 ASP.NET Core 路由

到目前为止,我们已经看到任何 HTTP 请求都要经过中间件,最终移交给配置方法中定义的控制器或端点,但是谁来负责这种移交给控制器/端点,ASP.NET Core 如何知道控制器内部的哪个控制器和方法来触发?这就是路由引擎的用途,它是在添加以下中间件时注入的:

app.UseRouting();
app.UseEndpoints(endpoints =>
{
     endpoints.MapControllers();
});

这里,app.UseRouting()注入Microsoft.AspNetCore.Routing.EndpointRoutingMiddleware,用于基于 URI 做出所有的路由决策。这个中间件的主要工作是用特定 URI 需要执行的动作的值来设置Microsoft.AspNetCore.Http.Endpoint方法的实例。

例如,如果我们试图根据产品的标识获取产品的详细信息,并且有一个产品控制器具有GetProductById方法来满足这个请求,当我们对api/products/1 URI 进行 API 调用时,在EndpointRoutingMiddleware之后的中间件中放置一个断点,向您显示Endpoint类的一个实例是可用的,其中包含了与 URI 匹配并且应该执行的操作的信息。我们可以在下面的截图中看到这一点:

https://github.com/OpenDocCN/freelearn-csharp-zh/raw/master/docs/enter-app-dev-cs9-dnet5/img/Figure_10.5_B15927.jpg

图 10.5–路由中间件

如果没有任何匹配的控制器/操作,此对象将为空。在内部,EndpointRoutingMiddleware使用 URI、查询字符串参数以及 HTTP 谓词和请求头来找到正确的匹配。

一旦识别出正确的动作方法,app.UseEndPoints的工作就是将控制权交给Endpoint对象识别的动作方法并执行。UseEndPoints注射Microsoft.AspNetCore.Routing.EndpointMiddleware以执行适当的方法来完成请求。填充适当的EndPoint对象的一个重要方面是在UseEndPoints中配置的各种 URIs,这可以通过 ASP.NET Core 中可用的静态扩展方法来实现。例如,如果我们只想配置控制器,我们可以使用MapControllers扩展方法,为UseRouting添加控制器中所有动作的端点,以进一步匹配。如果我们正在构建 RESTful APIs,建议使用MapControllers扩展。但是,对于以下常用的扩展,有许多这样的扩展方法:

  • MapGet / MapPost :这些是扩展方法,可以为GET / POST动词匹配特定的模式,执行请求。它们接受两个参数,一个是 URI 的模式,第二个是当模式匹配时可以用来执行的请求委托。例如,以下代码可用于匹配/aboutus路线,并用文本Welcome to default products route进行响应:

    endpoints.MapGet("/aboutus", async context =>
    {
     await context.Response.WriteAsync("Welcome to default products route");
    });
    
  • RazorPages:如果我们使用的是 Razor Pages,需要根据路线路由到合适的页面,就使用这个扩展方法。

  • mapcontrolleroute:这个扩展方法可以用来匹配特定模式的控制器;例如,在 ASP.NET Core MVC 模板中可以看到下面的代码,它匹配基于模式的方法:

    endpoints.MapControllerRoute(
    name: "default",
    pattern: "{controller=Home}/{action=Index}/{id?}");
    

请求 URI 基于正斜杠(/)被拆分,并与控制器、动作方法和标识相匹配。因此,如果你想匹配控制器中的一个方法,你需要在 URI 传递控制器名(ASP.NET Core 自动给控制器关键字加后缀)和方法名。或者,可以将该标识作为参数传递给该方法。例如,如果我在ProductsController有GetProducts,你会用绝对的 URIproducts/GetProducts来称呼它。这种路由被称为常规路由,非常适合基于 UI 的 web 应用,因此可以在 ASP.NET Core MVC 模板中看到。

我们对路由基础知识的讨论到此结束,在 ASP.NET Core 中有许多这样的扩展方法,可以根据应用的需求将其引入请求管道。现在,让我们看看基于属性的路由,这是为使用 ASP.NET Core 构建的 RESTful 服务推荐的路由技术。

注意

与任何其他中间件序列一样,路由的另一个重要方面是注入非常重要,应该在UseEndpoints之前调用UseRouting。

基于属性的路由

对于 RESTful 服务,常规的路由违反了一些 REST 原则,尤其是声明动作方法对实体执行的操作应该基于 HTTP 动词的原则,所以理想情况下,为了得到产品,URI 应该是GET api/products。这就是基于属性的路由开始发挥作用的地方,在这种情况下,路由是在控制器级别、操作方法级别或两者都使用属性来定义的。这是使用Microsoft.AspNetCore.Mvc.Route属性实现的,该属性将字符串值作为输入参数,用于映射控制器和动作。我们以ProductsController为例,其代码如下:

[Route("api/[controller]")]
[ApiController]
public class ProductsController : ControllerBase
{
    [HttpGet]
    [Route("{id}")]
    public IActionResult GetProductById(int id)
    {
        return Ok($"Product {id}");
    }
    [HttpGet]
    public IActionResult GetProducts()
    {
        return Ok("Products");
    }
}

这里,在控制器级别的Route属性中,我们传递的是值api/[controller],这意味着任何匹配api/products的 URI 都映射到这个控制器,其中products是控制器的名称。使用方括号内的controller关键字是告诉 ASP.NET Core 自动将控制器名称映射到路线的一种特定方式。但是,如果您想坚持使用一个特定的名称,而不考虑控制器名称,则可以不使用方括号。作为最佳实践,建议将控制器名称与路由分离。因此,对于我们的电子商务应用,我们将使用路线中的精确值,即ProductsController将具有[Route("api/products")]的路线前缀。

Route属性也可以添加到动作方法中,并且可以用来唯一地附加识别特定的方法。在这里,我们还传递了一个可以用来标识方法的字符串。例如,[Route("GetProductById/{id}")]将与 URI api/products/GetProductById/1匹配,花括号内的值是一个动态值,可以作为参数传递给 action 方法并与参数名匹配。这意味着在前面的代码中,有一个参数标识,花括号内的值也应该命名为ID,这样 ASP.NET Core 就可以将 URI 的值映射到method参数。因此,对于api/products/1 URI,如果路线属性看起来像[Route("{id}")],则GetProductById方法中的标识参数将具有值1。

最后,HTTP 动词由[HttpGet]等属性表示,这些属性将用于将 HTTP 动词从 URI 映射到方法。下表显示了各种示例和可能的匹配,假设ProductsController有[Route("api/products")]:

https://github.com/OpenDocCN/freelearn-csharp-zh/raw/master/docs/enter-app-dev-cs9-dnet5/img/Figure_10.Table_1_B15927.jpg

表 10.1

如您所见,方法的名称在这里是不重要的,因此不是 URI 匹配的一部分,除非在Route属性中指定。

注意

一个重要的方面是,web API 支持从请求中的不同位置读取参数,无论是请求体、头、查询字符串还是 URI。以下文档涵盖了各种可用选项:https://docs.microsoft.com/en-us/aspnet/core/web-api/?view = aspnetcore-5.0 #绑定-源-参数-推断。

ASP.NET Core 中整个应用编程接口路由的概要可以表示如下:

https://github.com/OpenDocCN/freelearn-csharp-zh/raw/master/docs/enter-app-dev-cs9-dnet5/img/Figure_10.6_B15927.jpg

图 10.6–ASP.NET Core 应用编程接口路由

基于属性的路由更 RESTful,我们在电商服务中也会遵循这种路由。现在,让我们看看 ASP.NET Core 中可用的各种助手类,它们可以用来简化 RESTful 服务的构建。

小费

路由中的表达式{id}被称为路由约束,ASP.NET Core 附带了各种各样的路由约束,也可以在这里找到:https://docs . Microsoft . com/en-us/aspnet/Core/foundation/routing?视图= aspnetcore-5.0 #路线-约束-参考。

controller base 类、ApiController 属性和 ActionResult 类

如果我们回到迄今为止创建的任何控制器,您可以看到所有控制器都是从ControllerBase类继承的。在 ASP.NET Core 中,ControllerBase是一个抽象类,它提供了各种帮助方法来帮助处理请求和响应。例如,如果我想发送一个 HTTP 状态代码 400(错误请求),在ControllerBase中有一个助手方法BadRequest,可以用来发送一个 HTTP 状态代码 400,否则我们必须手动创建一个对象并用 HTTP 状态代码 400 填充它。ControllerBase有很多现成的辅助方法;然而,并不是每个 API 控制器都必须继承自ControllerBase类。这里提到了ControllerBase类的所有助手方法:https://docs . Microsoft . com/en-us/dotnet/API/Microsoft . aspnetcore . MVC . controller base?view = aspnetcore-3.1&view fallbackfrom = aspnetcore-5.0。

这让我们开始讨论我们的控制器方法的返回类型应该是什么,因为一般来说,对于任何 API,至少有两种可能的响应,如下所示:

  • 具有 2xx 状态代码的成功响应,并且可能以资源或资源列表进行响应
  • 带有 4xx 状态代码的验证失败案例

为了处理这样的场景,我们需要创建一个可以用来发送不同响应类型的泛型类型,这就是 ASP.NET Core 的IActionResult和ActionResult类型发挥作用的地方,为我们提供了各种场景的派生响应类型。IActionResult支持的一些重要的响应类型如下:

  • OkObjectResult:这是一种响应类型,将 HTTP 状态代码设置为200,并将资源添加到包含资源详细信息的响应正文中。这种类型非常适合所有用资源或资源列表进行响应的 API,例如 get products。
  • NotFoundResult:这是将 HTTP 状态码设置为404的响应类型,为空体。如果找不到特定的资源,可以使用此选项。但是,在没有找到资源的情况下,我们将使用NoContentResult ( 204),因为404也将用于没有找到的应用编程接口。
  • BadRequestResult:这是一种将 HTTP 状态码设置为400的响应类型,在响应正文中有一条错误消息。这是任何验证失败的理想选择。
  • CreatedAtActionResult:这是一种将 HTTP 状态代码设置为201的响应类型,可以将新创建的资源 URI 添加到响应中。这是创建资源的 API 的理想选择。

所有这些响应类型都是从IActionResult继承而来的,并且ControllerBase类中有可以创建这些对象的方法,所以IActionResult和ControllerBase将解决大部分业务需求,这就是我们所有 API 控制器方法的返回类型。

ASP.NET Core 中最后一个有用的重要类是ApiController类,它可以作为属性添加到控制器类或程序集,并为我们的控制器添加以下行为:

  • 它禁用常规路由,并强制使用基于属性的路由。

  • 它自动验证模型,所以我们不需要在每个方法中显式调用ModelState.IsValid。这种行为在插入/更新方法的情况下非常有用。

  • 它有助于从正文/路由/头/查询字符串自动映射参数。这意味着我们不指定应用编程接口的参数是否将成为主体或路线的一部分。例如,在下面的代码中,我们不需要明确地说 ID 参数将是路线的一部分,因为ApiController自动地使用了被称为推理规则的东西,以及 ID 中带有[FromRoute]的前缀:

    [Route("{id}")]
    public IActionResult GetProductById(int id)
    {
      return Ok($"Product {id}");
    }
    
  • 同样,在下面的代码片段中,ApiController会根据推理规则自动添加[FromBody]:

    public IActionResult CreateProduct(Product product)
    {
     //
    }
    
  • ApiController添加的其他一些行为是根据https://tools.ietf.org/html/rfc7807推断多部分/表单数据的请求内容和更详细的错误响应。

因此,总而言之,ControllerBase、ApiController、ActionResult提供了各种各样的助手方法和行为,从而为开发人员提供了编写 RESTful APIs 所需的所有工具,并允许他们在使用 ASP.NET Core 编写 API 时专注于业务逻辑。

在此基础上,让我们在下一节为我们的电子商务应用设计各种 API。

与数据层的集成

来自我们的 API 的响应可能或者可能看起来不像我们的领域模型。相反,它们的结构可以类似于用户界面或视图需要绑定的字段。因此,建议创建一组单独的概念验证类,与我们的用户界面集成。这些概念验证对象被称为数据传输对象 ( 数据验证对象)。

在本节中,我们将实现我们的 dto,与数据层集成的域逻辑,并使用 Cache-average 模式集成 第 8 章中讨论的缓存服务,然后使用控制器和动作最终集成所需的 RESTful APIs。一路走来,我们将使用 HTTP 客户端工厂进行我们的服务到服务的通信,使用AutoMapper库将领域模型映射到 dto。

我们将选择属于Packt.Ecommerce.Product的产品服务,这是一个使用的网络应用编程接口项目.NET 5,并详细讨论了它的实现。到本节结束时,我们将已经实现了下面截图中突出显示的项目:

https://github.com/OpenDocCN/freelearn-csharp-zh/raw/master/docs/enter-app-dev-cs9-dnet5/img/Figure_10.7_B15927.jpg

图 10.7–产品服务和 dto

类似的实现在所有 RESTful 服务中进行复制,根据需要对业务逻辑稍作修改,但高级实现在以下各种服务中保持不变:

  • Packt.Ecommerce.DataAccess
  • Packt.Ecommerce.Invoice
  • Packt.Ecommerce.Order
  • Packt.Ecommerce.Payment
  • Packt.Ecommerce.UserManagement

首先,我们将在appsettings.json中有相应的部分,如下所示:

    "ApplicationSettings": {
    "UseRedisCache": false, // For in-memory
    "IncludeExceptionStackInResponse": true,
    "DataStoreEndpoint": "",
    "InstrumentationKey": ""
  },
  "ConnectionStrings": {
    "Redis": ""
  }

对于本地开发环境,我们将使用管理用户机密,如这里所解释的,https://docs . Microsoft . com/en-us/aspnet/core/security/app-Secrets?view=aspnetcore-5.0 &选项卡=windows ,设置如下值。但是,一旦部署了服务,它将使用 Azure KeyVault,如中的 第 6 章配置所述。网芯:

{
  "ApplicationSettings:InstrumentationKey": "", //relevant key
  "ConnectionStrings:Redis": "" //connection string
}

让我们从为产品应用编程接口创建 DTOs 开始。

创建 dto

在产品服务方面的关键要求是提供搜索产品、查看与产品相关的其他详细信息,然后继续购买的能力。由于产品列表可能有有限的细节,让我们创建一个概念验证(所有的 dto 都是在Packt.Ecommerce.DTO.Models项目中创建的),并将其命名为ProductListViewModel。这个类将具有我们想要在产品列表页面上显示的所有属性,它应该如下所示:

public class ProductListViewModel
{
        [JsonProperty(PropertyName = "id")]
        public string Id { get; set; }
        public string Name { get; set; } 
        public int Price { get; set; }
        public Uri ImageUrl { get; set; }
        public double AverageRating { get; set; }
}

如您所见,这些是任何电子商务应用中通常显示的最小字段。因此,我们将继续这些领域,但想法是随着应用的发展而扩展。这里,Id和Name属性是重要的属性,因为一旦用户想要检索关于产品的所有进一步细节,这些属性将被用于查询数据库。我们正在用JsonProperty(PropertyName = "id")属性注释Id属性,以确保属性名在序列化和反序列化期间保持为Id。这很重要,因为在我们的宇宙数据库实例中,我们使用Id作为大多数容器的密钥。现在让我们创建另一个代表产品细节的 POCO,如下面的代码片段所示:

public class ProductDetailsViewModel
{
        [Required]
        public string Id { get; set; }
        [Required]
        public string Name { get; set; }
        [Required]
        public string Category { get; set; }
        [Required]
        [Range(0, 9999)]
        public int Price { get; set; }
        [Required]
        [Range(0, 999, ErrorMessage = "Large quantity, please reach out to support to process request.")]
        public int Quantity { get; set; }
        public DateTime CreatedDate { get; set; }
        public List<string> ImageUrls { get; set; }
        public List<RatingViewModel> Rating { get; set; }
        public List<string> Format { get; set; }
        public List<string> Authors { get; set; }
        public List<int> Size { get; set; }
        public List<string> Color { get; set; }
        public string Etag { get; set; }
}
public class RatingViewModel
{
        public int Stars { get; set; }
        public int Percentage { get; set; }
}

在这里,您可以看到 POCO 非常像我们的领域模型,这是因为我们的反规范化领域模型。但是,一般来说,如果我们使用规范化的领域模型,您会注意到领域模型和 dto 之间的显著差异。在这里,可以进一步讨论为什么我们不能重用产品领域模型。然而,出于可扩展性的目的,以及为了进一步将领域模型与我们的用户界面松散耦合,最好使用单独的 POCO 类。因此,在这个 DTO,除了Id和Name之外,一个重要的属性是Etag,它将用于实体跟踪,以避免对实体的并发覆盖。例如,如果两个用户访问一个产品,用户 A 在用户 B 之前更新,使用Etag,我们可以阻止用户 B 覆盖用户 A 的更改,并强制用户 B 在更新之前获取产品的最新副本。

另一个重要的方面是,我们在模型上使用 ASP.NET Core 的内置验证属性来定义模型上的所有约束。首先,我们将按照 https://docs . Microsoft . com/en-us/aspnet/core/MVC/models/validation 使用[Required]属性和任何相关属性?视图= aspnetcore-5.0 #内置属性。

所有的 d to 都将是Packt.Ecommerce.DTO.Models项目的一部分,因为它们将在我们的 ASP.NET MVC 应用中重用,该应用将用于构建我们的电子商务应用的用户界面。现在,让我们看看Products服务所需的合同。

服务类合同

在Packt.Ecommerce.Product中添加一个Contracts文件夹,并创建一个产品的服务类的合同/接口,我们将根据需要参考我们的需求并定义方法。首先,它将拥有基于该接口对产品执行 CRUD 操作的所有方法,如下所示:

public interface IProductService
    {
        Task<IEnumerable<ProductListViewModel>> GetProductsAsync(string filterCriteria = null);
        Task<ProductDetailsViewModel> GetProductByIdAsync(string productId, string productName);
        Task<ProductDetailsViewModel> AddProductAsync(ProductDetailsViewModel product);
        Task<HttpResponseMessage> UpdateProductAsync(ProductDetailsViewModel product);
        Task<HttpResponseMessage> DeleteProductAsync(string productId, string productName);
    }

在这里你可以看到我们在所有方法中返回Task,从而坚持我们在 第 4 章线程和异步操作中讨论的异步方法。

使用自动映射器的映射器类

接下来我们需要的是一种将我们的领域模型转换为 dto 的方法,这里我们将使用一个名为AutoMapper的著名库来配置和添加以下包:

  • Automapper
  • AutoMapper.Extensions.Microsoft.DependencyInjection

要配置AutoMapper,我们需要定义一个继承自AutoMapper.Profile的类,然后定义各种域模型和 d to 之间的映射。让我们添加一个类AutoMapperProfile,到项目Packt.Ecommerce.Product:

    public class AutoMapperProfile : Profile
    {
        public AutoMapperProfile()
        {
        }
    }

AutoMapper有许多内置的映射方法,其中之一是CreateMap,它接受源类和目标类,并基于相同的属性名来映射它们。任何不同名的属性都可以使用ForMember方法手动映射。由于ProductDetailsViewModel与我们的领域模型有一对一的映射,CreateMap应该足够好,可以进行映射。对于ProductListViewModel,我们有一个额外的字段,AverageRating,我们想要为其计算特定产品的所有评级的平均值。为了简单起见,我们将从Linq开始使用Average方法,然后将其映射到平均评级。对于模块化,我们将在一个单独的方法MapEntity中实现,如下所示:

private void MapEntity()
{
            this.CreateMap<Data.Models.Product, DTO.Models.ProductDetailsViewModel>();
            this.CreateMap<Data.Models.Rating, DTO.Models.RatingViewModel>();
            this.CreateMap<Data.Models.Product, DTO.Models.ProductListViewModel>()
                .ForMember(x => x.AverageRating, o => o.MapFrom(a => a.Rating != null ? a.Rating.Average(y => y.Stars) : 0));
}

现在,修改构造函数来调用这个方法。

设置AutoMapper涉及的最后一步是将其作为服务之一注入,为此我们将使用ConfigureServices方法的Startup类,使用下面的行:

services.AddAutoMapper(typeof(AutoMapperProfile));

如前所述,这将把AutoMapper库注入到我们的 API 中,这将允许我们把AutoMapper注入到各种服务和控制器中。现在我们来看看HttpClient工厂的配置,用于调用数据访问服务。

用于服务对服务调用的 HttpClient 工厂

为了检索数据,我们必须调用由我们在Packt.Ecommerce.DataAccess中定义的数据访问服务公开的 API。为此,我们需要一个能够有效使用可用套接字的弹性库,允许我们定义断路器以及重试/超时策略。IHttpClientFactory非常适合这类场景。

注意

HttpClient的一个常见问题是潜在的SocketException,这种情况发生在HttpClient即使在对象被处理掉之后仍然保持 TCP 连接打开的情况下,建议将HttpClient创建为静态/单例,它有自己的开销,同时连接到多个服务。以下链接总结了所有这些问题,https://software engineering . stackexchange . com/questions/330364/我们是否应该为所有请求创建一个新的 httpclient 单实例,这些问题现在都由IhttpClientFactory解决。

要配置IHttpClientFactory,请执行以下步骤:

  1. 安装Microsoft.Extensions.Http。
  2. 我们将使用类型化客户端配置IHttpClientFactory,所以添加一个Services文件夹和一个ProductsService类,并从IProductService继承它们。目前,将实现留空。现在,使用以下代码在Startup类的ConfigureServices中映射IProductService和ProductsService

这里,我们将ProductsService使用的HttpClient超时定义为 5 分钟,另外配置了重试策略和断路器。

实施断路器政策

为了定义这些策略,我们将使用一个名为Polly的库,它提供了现成的弹性和故障处理能力。安装Microsoft.Extensions.Http.Polly包,然后将以下静态方法添加到定义我们的断路器策略的Startup类中:

private static IAsyncPolicy<HttpResponseMessage> CircuitBreakerPolicy()
{
    return HttpPolicyExtensions
        .HandleTransientHttpError()
        .CircuitBreakerAsync(5, TimeSpan.FromSeconds(30));
} 

在这里,我们说如果在 30 秒内有 5 次故障,电路就会断开。断路器有助于避免不必要的 HTTP 调用,因为存在无法通过重试修复的严重故障。

实施重试策略

现在,让我们添加我们的重试策略,与在指定时间范围内退出的标准重试相比,该策略稍微聪明一点。因此,我们定义了一个策略,该策略将在五种情况下实现重试和 HTTP 服务调用,并且每次重试都有一个以秒为单位的时间差,其幂为 2。为了在时间变化方面增加一些随机性,我们将使用 C#的一个Random类来生成一个随机数,并将其添加到时间间隙中。该随机生成将如以下代码所示:

Random random = new Random();
TimeSpan.FromSeconds(Math.Pow(2, retry))  + TimeSpan.FromMilliseconds(random.Next(0, 100));

这里,retry是一个整数,每retry增加 1。有了这个,给Startup类添加一个静态方法,它有前面的逻辑:

private static IAsyncPolicy<HttpResponseMessage> RetryPolicy()
{
    Random random = new Random();
    var retryPolicy = HttpPolicyExtensions
        .HandleTransientHttpError()
        .OrResult(msg => msg.StatusCode == System.Net.HttpStatusCode.NotFound)
        .WaitAndRetryAsync(
        5,
        retry => TimeSpan.FromSeconds(Math.Pow(2, retry))
                          + TimeSpan.FromMilliseconds(random.Next(0, 100)));
    return retryPolicy;
}

这就完成了我们的 HTTP 客户端工厂配置,ProductsService可以使用构造函数注入来实例化IHttpClientFactory,可以进一步用来创建HttpClient。

有了所有这些配置,我们现在可以实现我们的服务类了。让我们在下一节看看。

实现服务类

现在让我们通过定义我们已经构建的各种属性并使用构造函数注入实例化它们来实现,如下面的代码块所示:

private readonly IOptions<ApplicationSettings> applicationSettings;
private readonly HttpClient httpClient;
private readonly IMapper autoMapper;
private readonly IDistributedCacheService cacheService;
public ProductsService(IHttpClientFactory httpClientFactory, IOptions<ApplicationSettings> applicationSettings, IMapper autoMapper, IDistributedCacheService cacheService)
{
    NotNullValidator.ThrowIfNull(applicationSettings, nameof(applicationSettings));
    IHttpClientFactory httpclientFactory = httpClientFactory;
    this.applicationSettings = applicationSettings;
    this.httpClient = httpclientFactory.CreateClient();
    this.autoMapper = autoMapper;
    this.cacheService = cacheService;
}

我们所有的服务都将使用我们在本章中定义的相同的异常处理中间件,因此在服务对服务调用期间,如果另一个服务出现故障,响应将是ExceptionResponse类型。因此,让我们创建一个私有方法,反序列化ExceptionResponse类并相应地提升它。这是必需的,因为在使用IsSuccessStatusCode和StatusCode属性时HttpClient将代表成功或失败,所以如果有异常,我们需要检查IsSuccessStatusCode并重新启动它。我们称这个方法为ThrowServiceToServiceErrors和参考下面的代码:

private async Task ThrowServiceToServiceErrors(HttpResponseMessage response)
{
    var exceptionReponse = await response.Content.ReadFromJsonAsync<ExceptionResponse>().ConfigureAwait(false);
    throw new Exception(exceptionReponse.InnerException);
}

现在让我们实现GetProductsAsync方法,其中我们将使用CacheService从缓存中检索数据,如果数据在缓存中不可用,我们将使用HttpClient调用数据访问服务,最后将Product域模型映射到 DTO 并异步返回。代码如下所示:

public async Task<IEnumerable<ProductListViewModel>> GetProductsAsync(string filterCriteria = null)
{
    var products = await this.cacheService.GetCacheAsync<IEnumerable<Packt.Ecommerce.Data.Models.Product>>($"products{filterCriteria}").ConfigureAwait(false);
    if (products == null)
    {
        using var productRequest = new HttpRequestMessage(HttpMethod.Get, $"{this.applicationSettings.Value.DataStoreEndpoint}api/products?filterCriteria={filterCriteria}");
        var productResponse = await this.httpClient.SendAsync(productRequest).ConfigureAwait(false);
        if (!productResponse.IsSuccessStatusCode)
        {
            await this.ThrowServiceToServiceErrors(productResponse).ConfigureAwait(false);
        }
        products = await productResponse.Content.ReadFromJsonAsync<IEnumerable<Packt.Ecommerce.Data.Models.Product>>().ConfigureAwait(false);
        if (products.Any())
        {
            await this.cacheService.AddOrUpdateCacheAsync<IEnumerable<Packt.Ecommerce.Data.Models.Product>>($"products{filterCriteria}", products).ConfigureAwait(false);
        }
    }
    var productList = this.autoMapper.Map<List<ProductListViewModel>>(products);
    return productList;
}

我们将遵循类似的模式,实施AddProductAsync、UpdateProductAsync、GetProductByIdAsync和DeleteProductAsync。每种方法的唯一区别是使用相关的HttpClient方法并相应地处理它们。现在我们已经实现了我们的服务,让我们实现我们的控制器。

在控制器中实现动作方法

让我们首先将上一节中创建的服务注入到 ASP.NET Core 5 DI 容器中,这样我们就可以使用构造函数注入来创建一个ProductsService的对象。我们将在Startup类的ConfigureServices中使用以下代码来实现这一点:

services.AddScoped<IProductService, ProductsService>();

还要确保配置了所有需要的框架组件,如ApplicationSettings、CacheService和AutoMapper。

在Controllers文件夹中添加一个控制器并命名为ProductsController,默认路由为api/products,然后添加IProductService的属性,并使用构造函数注入进行注入。控制器应该实现五个动作方法,每个方法调用一个服务方法,并使用本章的控制器类、加速控制器属性和动作结果类一节中讨论的各种现成的助手方法和属性。下面的代码块显示了检索特定产品和创建新产品的方法:

[HttpGet]
[Route("{id}")]
public async Task<IActionResult> GetProductById(string id, [FromQuery][Required]string name)
{
    var product = await this.productService.GetProductByIdAsync(id, name).ConfigureAwait(false);
    if (product != null)
    {
        return this.Ok(product);
    }
    else
    {
        return this.NoContent();
    }
}
[HttpPost]
public async Task<IActionResult> AddProductAsync(ProductDetailsViewModel product)
{
    // Product null check is to avoid null attribute validation error.
    if (product == null || product.Etag != null)
    {
        return this.BadRequest();
    }
    var result = await this.productService.AddProductAsync(product).ConfigureAwait(false);
    return this.CreatedAtAction(nameof(this.GetProductById), new { id = result.Id, name = result.Name }, result); // HATEOS principle
}

方法的实现是不言自明的,并且纯粹基于中讨论的使用控制器和动作处理请求的基本原理。同样,我们将通过调用相应的服务方法并返回相关的ActionResult来实现所有其他方法(Delete、Update、Get所有产品)。这样,我们将有下表所示的 API 来处理与产品实体相关的各种场景:

https://github.com/OpenDocCN/freelearn-csharp-zh/raw/master/docs/enter-app-dev-cs9-dnet5/img/Figure_10.Table_2_B15927.jpg

表 10.2

小费

应用编程接口的另一个常见场景是拥有一个支持文件上传/下载的应用编程接口。上传场景通过将IFormFile作为输入参数传递给应用编程接口来实现。这将序列化上传的文件,也可以保存在服务器上。同样,对于文件下载,FileContentResult可用,可以将文件流式传输到任何客户端。这是留给你进一步探索的活动。

对于测试原料药,我们将使用波兹曼(https://www.postman.com/downloads/)。所有邮差收藏都可以在Solution Items文件夹文件Mastering enterprise application development Book.postman_collection.json下找到。要在安装 Postman 后导入集合,请执行以下步骤:

  1. 打开邮差,然后点击文件。
  2. 点击导入 | 上传文件,导航到Mastering enterprise application development Book.postman_collection.json文件的位置,然后点击导入。

成功导入会在邮递员的收藏菜单中显示该收藏,如下图截图所示:

https://github.com/OpenDocCN/freelearn-csharp-zh/raw/master/docs/enter-app-dev-cs9-dnet5/img/Figure_10.8_B15927.jpg

图 10.8–邮差中的收集

这就完成了我们的Products RESTful 服务实现。本节开头提到的所有其他服务都是以类似的方式实现的,其中每个服务都是一个单独的 web API 项目,并为该实体处理相关的域逻辑。

了解 gRPC

按照grpc.io,gRPC 是一个高性能、开源通用远程过程调用 ( RPC )框架。gRPC 最初是由谷歌开发的,使用 HTTP/2 进行传输,一个协议缓冲区 ( protobuf )作为接口描述语言。gRPC 是一个基于契约的二进制通信系统。它适用于多个生态系统。gRPC 官方文档中的下图( https://grpc.io )说明了使用 grpc 的客户端-服务器交互:

https://github.com/OpenDocCN/freelearn-csharp-zh/raw/master/docs/enter-app-dev-cs9-dnet5/img/Figure_10.9_B15927.jpg

图 10.9–gRPC 客户端-服务器交互

像许多分布式系统一样,gRPC 是基于定义服务和指定接口的思想,这些接口的方法可以随契约一起远程调用。在 gRPC 中,服务器实现接口并运行 gRPC 服务器来处理客户端调用。在客户端,它有存根,提供与服务器定义的相同的接口。客户端调用存根的方式与调用任何其他本地对象中的方法来调用服务器上的方法的方式相同。

默认情况下,数据协定使用协议缓冲区来序列化来自和去往客户端的数据。protobufs 在扩展名为.proto的文本文件中定义。在 protobuf 中,数据被结构化为字段中包含的信息的逻辑记录。在下一节中,我们将学习如何在 Visual Studio 中为. NET 5 应用定义 protobuf。

注意

参考官方文档了解更多关于 gRPC: https://grpc.io 的信息。要了解更多关于 protobuf 的信息,请参考https://developers . Google . com/protocol-buffers/docs/概述。

考虑到高性能、语言无关的实现以及与 gRPC 的 protobuf 相关的网络使用减少的好处,许多团队正在探索 gRPC 在他们构建微服务的努力中的使用。

在下一节中,我们将学习在中构建 gRPC 服务器和客户端.NET 5。

在中构建 gRPC 服务器。网

年首次亮相后.NET core 3.0,gRPC 已经成为了一等公民中的佼佼者.NET 生态系统。使用 Visual Studio 2019 和,完全托管的 gRPC 实现现已在. NET 中提供.NET 5,我们可以轻松创建 gRPC 服务器和客户端应用。让我们使用 Visual Studio 中的 gRPC 服务模板创建一个 gRPC 服务,并将其命名为gRPCDemoService:

https://github.com/OpenDocCN/freelearn-csharp-zh/raw/master/docs/enter-app-dev-cs9-dnet5/img/Figure_10.10_B15927.jpg

图 10.10–gRPC VS 2019 项目模板

这将创建一个名为GreetService的示例 gRPC 服务解决方案。现在让我们理解用模板创建的解决方案。创建的解决方案将包含对Grpc.AspNetCore的包引用。这将包含托管 gRPC 服务和.proto文件的代码生成器所需的库。该解决方案将在Protos解决方案文件夹下为GreetService创建原型文件。以下代码片段定义了Greeter服务:

service Greeter {
  // Sends a greeting
  rpc SayHello (HelloRequest) returns (HelloReply);
}

Greeter服务只有一个名为SayHello的方法,该方法将输入参数作为HelloRequest,并返回一条HelloReply类型的消息。HelloRequest和HelloReply消息在同一个原型文件中定义,如以下代码片段所示:

message HelloRequest {
  string name = 1;
}
message HelloReply {
  string message = 1;
}

HelloRequest有一个名为name的文件,HelloReply有一个名为message的字段。字段旁边的数字显示该字段在缓冲区中的序号位置。原型文件是用Protobuf编译器编译的,以生成带有所有必需管道的存根类。我们可以从原型文件的属性中指定要生成的存根类的类型。由于这是一个服务器,它将配置设置为服务器仅。

现在,我们来看看GreetService的实现。这将如下面的代码片段所示:

public class GreeterService : Greeter.GreeterBase
{
    private readonly ILogger<GreeterService> _logger;
    public GreeterService(ILogger<GreeterService> logger)
    {
        _logger = logger;
    }
    public override Task<HelloReply> SayHello(HelloRequest request, ServerCallContext context)
    {
        return Task.FromResult(new HelloReply
        {
            Message = "Hello " + request.Name
        });
    }
}

GreetService继承自Greeter.GreeterService,由 protobuf 编译器生成。通过构造原型文件中定义的HelloReply,覆盖SayHello方法以提供实现,从而将问候返回给呼叫者。

要在. NET 5 应用中公开 gRPC 服务,需要通过调用Startup类的ConfigureServices方法中的AddGrpc将所有需要的 gRPC 服务添加到服务集合中。GreeterSerivce gRPC 服务通过调用MapGrpcService暴露:

app.UseEndpoints(endpoints =>
{
 endpoints.MapGrpcService<GreeterService>();
});

这就是在. NET 5 应用中公开 gRPC 服务所需的一切。在下一节中,我们将实现一个. NET 5 客户端来消费GreeterService。

在中构建 gRPC 客户端。网

如理解 gRPC 一节所述.NET 5 也有很好的工具来构建一个 gRPC 客户端。在本节中,我们将在控制台应用中构建一个 gRPC 客户端:

  1. 创建一个. NET 5 控制台应用并命名为gRPCDemoClinet。

  2. Now, right-click on the project and click on the menu items Add | Service reference…. This will open the Connected Services tab, as shown in the following screenshot:

    https://github.com/OpenDocCN/freelearn-csharp-zh/raw/master/docs/enter-app-dev-cs9-dnet5/img/Figure_10.11_B15927.jpg

    图 10.11–gRPC 连接服务选项卡

  3. 点击服务参考(OpenAPI,gRPC) 下的添加按钮,在添加服务参考对话框中选择 gRPC ,然后点击下一步。

  4. In the Add new gRPC service reference dialog, select the File option, select the greet.proto file from gRPCDemoService, and then click on the Finish button. This will add the proto file link to the project and marks the protobuf compiler to generate the Client stub classes:

    https://github.com/OpenDocCN/freelearn-csharp-zh/raw/master/docs/enter-app-dev-cs9-dnet5/img/Figure_10.12_B15927.jpg

    图 10.12–添加 gRPC 服务参考

    这还会将所需的 NuGet 包Google.Protobuf、Grpc.Net.ClientFactory和Grpc.Tools添加到项目中。

  5. Now, add the following code snippet to the main method of the gRPCDemoClient project:

    static async Task Main(string[] args)
    {
     var channel = GrpcChannel.ForAddress("https://localhost:5001");
     var client = new Greeter.GreeterClient(channel);
     HelloReply response = await client.SayHelloAsync(new HelloRequest { Name="Suneel" });
     Console.WriteLine(response.Message);
    }
    

    在这段代码中,我们创建了到gRPCDemoService端点的 gRPC 通道,然后通过传入 gRPC 通道实例化Greeter.GreeterClinet,这是到gRPCDemoService的存根。

现在,要调用服务,我们只需要通过传递HelloRequest消息来调用存根上的SayHelloAsync方法。该呼叫将从服务中返回HelloReply。

到目前为止,我们已经创建了一个简单的 gRPC 服务和该服务的控制台客户端。在下一节中,我们将了解 gRPC curl,这是一个测试 gRPC 服务的通用客户端。

测试 gRPC 服务

为了测试或调用一个 REST 服务,我们使用了一些工具,比如邮递员或提琴手。gRPCurl是一个命令行实用程序,帮助我们与 gRPC 服务交互。使用 grpcurl,我们可以在不构建客户端应用的情况下测试 grp 服务。grpcurl 可从https://github.com/fullstorydev/grpcurl下载。

一旦grpcurl被下载,我们可以使用以下命令调用GreeterService:

grpcurl -d "{\"name\": \"World\"}" localhost:5001 greet.Greeter/SayHello

注意

目前 gRPC 应用只能在 Azure App Service 和 IIS 中托管。因此,我们没有在 Azure 应用服务上托管的演示电子商务应用中利用 gRPC。但是,在本章演示中有一个版本的电子商务应用,其中根据产品的标识获取产品被公开为自托管服务中的 gRPC 端点。

总结

在本章中,我们介绍了 REST 的基本原理,并为我们的电子商务应用设计了企业级 RESTful 服务。

在此过程中,我们掌握了 ASP.NET Core 5 网络应用编程接口的各种网络应用编程接口内部,包括路由和示例中间件,并熟悉了用于测试我们的服务的工具,同时学习了如何使用控制器及其动作来处理请求,这也是我们学习构建的。此外,我们还看到了如何在中创建和测试基本的 gRPC 客户端和服务器应用.NET 5。到目前为止,您应该能够自信地使用 ASP.NET Core 5 网络应用编程接口构建 RESTful 服务。

在下一章中,我们将学习 ASP.NET MVC 的基础知识,使用 ASP.NET MVC 构建我们的 UI 层,并将其与我们的 API 集成。

问题

  1. Which of the following HTTP verbs is recommended for creating a resource?

    a.GET

    b.POST

    c.DELETE

    d.PUT

  2. Which of the following HTTP status codes represents No Content?

    a.200

    b.201

    c.202

    d.204

  3. Which of the following middlewares is used to configure routing?

    a.UseDeveloperExceptionPage()

    b.UseHttpsRedirection()

    c.UseRouting()

    d.UseAuthorization()

  4. If a controller is annotated with the [ApiController] attribute, do I need to class ModelState.IsValid explicitly in each action method?

    a.是的,模型验证不是ApiController属性的一部分,因此,您需要在每个动作方法中调用ModelState.Valid。

    b.不,模型验证是作为ApiController属性的一部分来处理的,因此,对于所有的动作项都自动触发ModelState.Valid。

进一步阅读

十一、创建 ASP.NET Core 5 网络应用

到目前为止,我们已经构建了应用的所有核心组件,如数据访问层和服务层,所有这些组件主要是服务器端组件,也称为后端组件。在这一章中,我们将为我们的电子商务应用构建表示层/ 用户界面 ( UI ),它也被称为客户端组件。UI 是应用的外观;拥有一个好的表示层不仅有助于让用户参与到应用中,还能鼓励用户回到应用中来。企业应用尤其如此,在企业应用中,良好的表示层有助于用户轻松浏览应用,并帮助他们轻松执行依赖于应用的各种日常活动。

在本章中,我们将主要关注理解 ASP.NET Core MVC 和使用 ASP.NET Core MVC 开发一个网络应用。首先,我们将讨论以下主题:

  • 前端 web 开发简介
  • 将应用编程接口与服务层集成
  • 创建控制器和操作
  • 使用 ASP.NET Core MVC 创建用户界面
  • 理解布拉佐

技术要求

对于这一章,你需要一个 C#的基础知识.NET Core、HTML 和 CSS。本章的代码示例可以在这里找到:https://github . com/PacktPublishing/Enterprise-Application-Development-with-C-Sharp-9-and-and.NET-5/树/主/第 11 章/剃刀样本。

您可以在这里找到更多代码示例:

https://github . com/PacktPublishing/Enterprise-Application-Development-with-C-Sharp-9-and-.NET-5/树/主/企业% 20 应用

前端 web 开发简介

表示层是关于浏览器可以呈现并显示给用户的代码。每当一个页面被加载到浏览器中时,它会创建一个各种元素的层次结构,如文本框和标签,这些元素出现在页面上。这个层次结构被称为文档对象模型 ( DOM )。一个好的前端就是能够根据需要操纵 DOM,并且有许多技术/库支持操纵 DOM 和使用事实上的网络语言 JavaScript 动态加载数据。不管是 jQuery,它简化了 JavaScript 的使用;成熟的客户端框架,如 Angular、React 或 Vue,支持完整的客户端渲染;或者 ASP.NET Core 框架,如 ASP.NET Core MVC、Razor Pages 或 Blazor,都归结为处理网络的三大构建块:HTML、CSS 和 JavaScript。让我们看看这三个组成部分:

  • 超文本标记语言 ( HTML ): HTML,如完整形式所述,是浏览器可以理解和显示内容的标记语言。它主要由一系列标签组成,这些标签是被称为 HTML 元素,允许开发人员定义页面的结构。例如,如果您想要创建一个需要允许用户输入他们的名字和姓氏的表单,可以通过使用输入的 HTML 元素来定义它。

  • Cascading Style Sheets (CSS): The presentation layer is all about presenting data in a way that makes a web application more appealing to users and ensures that the application is usable irrespective of the device/resolution that a user tries to load the application in. This is where CSS plays a critical role in defining how the content is displayed on the browser. It controls various things such as the styling of the pages, the theme of the application, and the color palette, and, more importantly, makes it responsive so that users have the same experience using the application, be it loaded on a mobile or a desktop.

    现代网络开发的好处是我们不需要从头开始写所有的东西,并且有很多可用的库可以在应用中按原样选择和使用。我们将为电子商务应用使用这样一个库,这将在使用 ASP.NET Core MVC 创建用户界面一节中解释。

  • JavaScript : JavaScript 是一种脚本语言,有助于执行各种高级动态操作,例如,验证在表单或事物中输入的输入文本如有条件地启用/禁用 HTML 元素或从 API 中检索数据。JavaScript 为网页提供了更多的功能,并增加了许多编程功能,开发人员可以使用这些功能在客户端执行高级操作。就像 HTML 和 CSS 一样,所有浏览器都可以理解 JavaScript,它构成了表示层的重要组成部分。所有这些组件都可以相互链接,如下图所示:

https://github.com/OpenDocCN/freelearn-csharp-zh/raw/master/docs/enter-app-dev-cs9-dnet5/img/Figure_11.1_B15927.jpg

图 11.1–HTML、CSS 和 JavaScript

注意

HTML、CSS 和 JavaScript 都是齐头并进的,在开发客户端/前端应用中发挥着重要作用,需要专门的书籍来全面解释。一些相关链接可以在进一步阅读部分找到。

现在我们已经理解了 HTML、CSS 和 JavaScript 的重要性,我们需要知道如何使用它们来构建 web 应用的表示层,以便它能够支持多种不同分辨率的浏览器和设备,并且能够管理状态(HTTP 是无状态的)。一种技术是创建所有的网页,并把它们放在网络服务器上;然而,虽然这在静态网站上运行良好,并且还涉及从头开始构建所有内容,但是如果我们希望内容更加动态并且想要丰富的 UI,我们需要使用能够动态生成 HTML 页面并提供无缝支持以与后端交互的技术。在下一节中,让我们来看看可以用来生成动态 HTML 的各种技术。

Razor 语法

在我们开始理解 ASP.NET Core 提供的各种可能的框架之前,让我们先了解什么是 Razor 语法。将服务器端组件嵌入到 HTML 中是一种标记语法。我们可以使用 Razor 语法绑定任何动态数据进行显示,或者从视图/页面将其发送回服务器进行进一步处理。Razor 语法主要写在 Razor 文件/Razor 视图页面中,这些页面只不过是 C#用来生成动态 HTML 的文件。它们使用.cshtml扩展并支持 Razor 语法。Razor 语法由名为视图引擎的引擎处理,默认的视图引擎被称为 Razor 引擎。

为了嵌入 Razor 语法,我们通常使用@,它告诉 Razor 引擎从引擎中解析和生成 HTML。@后面可以跟任意 C#内置方法生成 HTML。例如,<b>@DateTime.Now</b>可用于在 Razor 视图/页面中显示当前日期和时间。除此之外,就像 C#一样,Razor 语法也支持代码块和控制结构和变量等。下图显示了 Razor 引擎中的一些示例 Razor 语法:

https://github.com/OpenDocCN/freelearn-csharp-zh/raw/master/docs/enter-app-dev-cs9-dnet5/img/Figure_11.2_B15927.jpg

图 11.2–Razor 语法

Razor 语法也支持定义 HTML 控件;例如,要定义文本框,我们可以使用以下 Razor 语法:

<input asp-for=' FirstName ' />

前面的代码被称为input标签帮助器,Razor 语法负责处理所谓的指令标签帮助器,以将数据绑定到一个 HTML 控件并生成丰富的动态 HTML。让我们简单讨论一下:

  • Directives: Under the hood, each Razor view/page is parsed by the Razor engine and a C# class is used to generate dynamic HTML and then sends it back to the browser. Directives can be used to control the behavior of this class, which further controls the dynamic HTML that is generated.

    例如,@using指令可用于在 Razor 视图/页面中包含任何名称空间,或者@code指令可用于包含任何 C#成员。

    最常用的指令之一是@model,它允许您将模型绑定到视图,这有助于验证视图的类型,也有助于智能感知。将视图绑定到特定类/模型的过程被称为强类型化视图。我们将在我们的电子商务应用中强烈地输入我们所有的视图,我们将在使用 ASP.NET Core MVC 创建用户界面部分看到。

  • Tag helpers: If you used ASP.NET MVC before ASP.NET Core, you would have come across HTML helpers, which are classes that help to bind data and generate HTML controls.

    然而,对于 ASP.NET Core,我们有标签助手,帮助我们将数据绑定到一个 HTML 控件。标签助手相对于 HTML 助手的好处在于,标签助手使用与 HTML 相同的语法,并为可以从动态数据生成的标准 HTML 控件分配附加属性。例如,为了生成一个 HTML 文本框控件,我们通常编写以下代码:

    <input type='text' id='Name' name='Name' value=' Mastering enterprise application development Book'>
    

    使用标记助手,这将被重写,如下面的代码所示,其中@Name是视图被强类型化到的模型的属性:

    <input type='text' asp-for='@Name'>
    

所以,如你所见,这一切都是为了编写 HTML,但利用 Razor 标记来生成动态 HTML。ASP.NETCore 自带很多内置的标签助手,关于它们的更多细节可以在这里找到:

https://docs . Microsoft . com/en-us/aspnet/core/MVC/view/tag-helper/内置/?view=aspnetcore-5.0

不要求了解/记住每个标签助手,我们将在开发应用 UI 时使用该参考文档。

注意

因为 Razor 语法是标记,所以没有必要知道所有的语法。以下链接可以作为 Razor 语法的参考:

https://docs . Microsoft . com/en-us/aspnet/core/MVC/view/razor?view=aspnetcore-5.0

有了这些,让我们看看 ASP.NET Core 中的各种选项,以及开发表示层的其他常见框架。

探索剃刀页面

Razor Pages 是使用 ASP.NET Core 实现网络应用的默认方式。Razor Pages 依赖于一个 Razor 文件的概念,该文件可以直接为请求提供服务,并且有一个可选的 C#文件与该 Razor 文件相关联,用于任何额外的处理。可以使用dotnet new webapp命令创建一个典型的 Razor 应用,生成的项目看起来像下面截图中所示的项目:

https://github.com/OpenDocCN/freelearn-csharp-zh/raw/master/docs/enter-app-dev-cs9-dnet5/img/Figure_11.3_B15927.jpg

图 11.3–剃刀样本页面

如您所见,该项目有 Razor 页面及其相应的 C#文件。在打开任何 Razor 视图时,我们都会看到一个名为@page的指令,它有助于浏览页面。所以,比如说/index会被路由到index.cshtml。重要的是,所有 Razor 页面的顶部都有@page指令,并放在Pages文件夹中,因为 ASP.NET Core 运行时会在该文件夹中查找所有 Razor 页面。

通过使用另一个名为@model的指令,Razor 页面可以进一步与 C#类(也称为PageModel类)相关联。以下是index.cshtml页面的代码:

@page
@model  RazorSample.Pages.IndexModel
@{
    ViewData['Title'] = 'Home page';
}
<div class='text-center'>    
    <select name='day' asp-items='Model.WeekDay'></select>
</div>

PageModel类只不过是一个 C#类,它可以为GET和POST调用提供特定的方法,这样 Razor 页面上的数据就可以动态获取,比如说,从一个 API 中获取。这个类需要Microsoft.AspNetCore.Mvc.RazorPages.PageModel继承,是标准的 C#类,index.cshtml的PageModel是index.cshtml.cs的一部分,如下代码所示:

public class IndexModel : PageModel
{
    public IndexModel(ILogger<IndexModel> logger)
    {
    }
    public List<SelectListItem> WeekDay { get; set; }
    public void OnGet()
    {
        this.WeekDay = new List<SelectListItem>();
        this.WeekDay.Add(new SelectListItem 
                              {
                                  Value = 'Monday',Text =  'Monday'
                              });
        this.WeekDay.Add(new SelectListItem 
                              {
                                  Value = 'Tuesday',Text =  'Tuesday'
                              });                                  

    }
}    

在这里,您可以看到我们正在通过OnGet方法填充剃刀页面上使用的附加数据,该方法也称为PageModel处理程序,可用于剃刀页面的初始化。像OnGet一样,我们可以添加一个OnPost处理程序,用于将数据从 Razor 页面提交回PageModel并推进该过程。

如果满足以下两个条件,OnPost方法将自动绑定PageModel类中的所有属性:

  • 属性用BindProperty属性进行注释。
  • 剃刀页面有一个与属性同名的 HTML 控件。

因此,例如,如果我们想在前面的代码中绑定select控件的值,我们需要首先向PageModel类添加一个属性,如下面的代码所示:

[BindProperty]
 public string WeekDaySelected { get; set; }

然后,使用如下所示的select控件的属性名,Razor Pages 会自动将所选值绑定到该属性:

<select asp-for='WeekDaySelected' asp-items='Model.WeekDay'></select>

我们可以为OnGet和OnPost方法使用异步命名约定,这样如果我们使用异步编程,它们就可以被命名为OnGetAsync/OnPostAsync。

Razor Pages 还支持基于动词的调用方法。方法名的模式应遵循OnPost[handler]/OnPost[handler]Async惯例,其中[handler]是在任何标签助手的asp-page-handler属性上设置的值。

例如,以下代码将从相应的PageModel类调用OnPostDelete/OnPostDeleteAsync方法:

<input type='submit' asp-page-handler='Delete' value='Delete' />

对于服务配置部分,通过将AddRazorPages服务添加到 ASP.NET Core依赖注入 ( DI )容器中,可以使用Startup类的ConfigureServices方法配置 Razor Pages。此外,在Configure方法中,将MapRazorPages添加到endpoints,如下面的代码所示,这样就可以使用页面的名称请求所有的 Razor 页面:

app.UseEndpoints(endpoints =>
{
   endpoints.MapRazorPages();
});

这就完成了一个简单的 Razor 页面应用设置;我们在 第 9 章中看到了另一个使用数据的示例.NET 5 ,使用 Razor Pages 从数据库中检索数据,使用实体框架核心。

Razor Pages 是 ASP.NET Core 开发网络应用最简单的形式;然而,对于开发能够处理复杂特性的 web 应用的更结构化的形式,我们可以使用 ASP.NET Core MVC。让我们在下一节探索使用 ASP.NET Core MVC 开发网络应用。

探索 ASP.NET Core MVC 网站

顾名思义,ASP.NET Core MVC 是基于 第 10 章中讨论的 MVC 模式,创建一个 ASP.NET Core 5 Web API,是 ASP.NET Core 中的一个框架来构建 Web 应用。我们在 第 10 章创建 ASP.NET Core 5 Web API 中看到,ASP.NET Core Web API 也使用了 MVC 模式;不过,ASP.NET Core MVC 也支持视图显示数据。底层的设计模式是相同的,我们有一个保存数据的模型,一个传输数据的控制器,以及呈现和显示数据的视图。

ASP.NET Core MVC 支持在第 10 章**中讨论的创建 ASP.NET Core 5 网络应用编程接口的所有功能,如路由、DI、模型绑定和模型验证,并使用与使用Program和Startup类相同的自举技术。像网络应用编程接口,.NET 5/应用服务在Startup类的ConfigureServices方法中配置,中间件在Configure方法中注入。

*与 MVC 的一个关键区别是视图的额外加载,为此我们需要在Startup类的ConfigureServices方法中使用AddControllersWithViews而不是AddControllers。下图显示了一个示例:

https://github.com/OpenDocCN/freelearn-csharp-zh/raw/master/docs/enter-app-dev-cs9-dnet5/img/Figure_11.4_B15927.jpg

图 11.4–MVC 请求生命周期

AddControllersWithViews主要负责加载视图和处理控制器发送的数据,但最重要的是,它负责配置 Razor 引擎服务,该服务用于处理视图中的 Razor 语法并生成动态 HTML。

ASP.NET Core MVC 中的控制器动作需要根据 URL 中传递的动作名称进行路由,因此在路由部分,我们配置MapControllerRoute并传递一个模式给它,而不是调用MapController。因此,UseEndpoints中间件中的默认路由配置如下代码片段所示:

app.UseEndpoints(endpoints =>
{
endpoints.MapControllerRoute(
name: 'default',
pattern: '{controller=Products}/{action=Index}/{id?}');
});

这里,在模式中,我们告诉中间件,URL 的第一部分应该是一个controller名称,后面是动作名称和一个可选的id参数。如果 URL 中没有传递任何内容,则默认路由为ProductsController的Index动作方法。因此,首先,这是我们在 第 10 章中讨论的基于约定的路由,创建 ASP.NET Core 5 网络应用编程接口。

就像 Razor Pages 一样,ASP.NET Core MVC 应用中的视图支持 Razor 语法,允许强类型视图;也就是说,视图可以绑定到模型进行类型检查,模型属性可以与支持编译时 IntelliSense 的 HTML 控件相关联。

由于 ASP.NET Core MVC 为应用提供了更多的结构,我们将使用 ASP.NET Core MVC 进行我们的表示层开发,并在后续章节中实现表示层时详细讨论。

了解单页应用

单页应用,俗称 SPAs ,是可以在客户端生成动态 HTML 的应用,大多使用 JavaScript。在 SPAs 中,应用通常在浏览器内部工作,支持从服务器加载数据并导航到应用中的各个页面,而无需重新加载页面,这主要是它们被称为单页应用的原因。

通常,这类应用会将所有必需的 HTML、CSS 和 JavaScript 下载到浏览器中,然后加载页面。服务器所需的任何动态内容都是根据需要请求的,动态 HTML 是在客户端使用预加载的 JavaScript 生成的,并在同一页面中呈现,而无需完全刷新浏览器。React、Vue.js、Angular 和 Ember 是我们可以用来开发 SPa 的几个这样的框架。SPAs 发展很快,如果开发团队在这些框架中的任何一个都有技能,那么 SPAs 应该是开发表示层的首选。然而,由于我们将使用 ASP.NET Core MVC 开发表示层,我们将不讨论 spa 的细节,它们本身需要一本专门的书。不过,一些相关的链接可以在进一步阅读部分进行探索。

注意

选择哪种技术进行前端开发一直是一个常见的问题。以下链接有一些关于这个主题的建议:https://docs . Microsoft . com/en-us/dotnet/architecture/modern-web-apps-azure/在传统 web 和单页应用之间进行选择。在选择前端技术之前,应评估所有利弊,因为没有一刀切的要求。

有了这个基础,让我们进入下一部分,我们将开始将到目前为止开发的后端 API 与我们的表示层进行集成。

将 API 与服务层集成

在这一部分,我们将开发Packt.Ecommerce.WebASP.NET Core MVC 应用,这是通过添加ASP.NET Core web application(Model-View-Controller)模板创建的。由于我们已经开发了表示层所需的各种应用编程接口,我们将首先构建一个包装类,用于与这些应用编程接口进行通信。

这是一个单独的包装类,将用于与各种 API 通信,所以让我们为这个类创建契约。

为了简单起见,我们将把需求限制在电子商务应用中最重要的工作流,如下所示:

  • 登录页面,检索系统中的所有产品,并允许用户搜索/过滤产品。
  • 查看产品的详细信息,添加到购物车,以及向购物车添加更多产品的能力。
  • 完成订单并查看发票。

为了遵循更结构化的方法,我们将把不同的类和接口分离到不同的文件夹中。让我们看看如何:

  1. 首先,让我们在Packt.Ecommerce.Web项目中添加一个Contracts文件夹,并添加一个名为IECommerceService的界面。该界面将有以下方法:

    // Method to retrieve all products and filter.
    Task<IEnumerable<ProductListViewModel>> GetProductsAsync(string filterCriteria = null);
    // Method to get details of specific product.
    Task<ProductDetailsViewModel> GetProductByIdAsync(string productId, string productName);
    // Method to create and order, this method is primarily used to create a cart which is nothing but an order with order status as 'Cart'.
    Task<OrderDetailsViewModel> CreateOrUpdateOrder(OrderDetailsViewModel order);
    // Method to retrieve order by ID, also used to retrieve cart/order before checkout.
    Task<OrderDetailsViewModel> GetOrderByIdAsync(string orderId);
            Task<InvoiceDetailsViewModel> GetInvoiceByIdAsync(string invoiceId);
    // Method to submit cart and create invoice.
    Task<InvoiceDetailsViewModel> SubmitOrder(OrderDetailsViewModel order); 
    // Method to retrieve invoice details by Id.
    Task<InvoiceDetailsViewModel> GetInvoiceByIdAsync(string invoiceId);
    
  2. 现在,让我们添加一个名为Services的文件夹,并添加一个名为EcommerceService的类。这个类将继承IECommerceService并实现所有的方法。

  3. As we need to call various APIs, we need to make use of the HttpClient factory as mentioned in Chapter 10, Creating an ASP.NET Core 5 Web API. All the API URLs are maintained in the application settings, hence we will also populate Packt.Ecommerce.Common.Options.ApplicationSettings using the options pattern.

    Startup类的ConfigureServices方法将为我们的 MVC 应用配置以下服务:

    • AddControllersWithViews:为 ASP.NET Core MVC 注入必要的服务,使用控制器和视图。

    • ApplicationSettings:使用IOptions模式配置ApplicationSettings类,代码如下:

      services.Configure<ApplicationSettings>(this.Configuration.GetSection('ApplicationSettings'));
      
    • AddHttpClient:这将注入System.Net.Http.IHttpClientFactory和相关类,允许我们创建一个HttpClient对象。此外,我们将配置重试策略和断路策略,如 第 10 章创建 ASP.NET Core 5 网络应用编程接口中所述。

    • 使用将EcommerceService映射到IECommerceService.NET Core DI 容器。

  4. 使用以下代码配置应用洞察:

    string appinsightsInstrumentationKey = this.Configuration.GetValue<string>('AppSettings:InstrumentationKey');
    if (!string.IsNullOrWhiteSpace(appinsightsInstrumentationKey))
                {
                    services.AddLogging(logging =>
                    {                                       logging.AddApplicationInsights(appinsightsInstrumentationKey);
                    });                services.AddApplicationInsightsTelemetry (appinsightsInstrumentationKey);
                }
    

继续到中间件,除了默认路由中间件之外,我们将使用Startup类的Configure方法注入以下中间件:

  • UseStatusCodePagesWithReExecute:该中间件用于重定向到自定义页面,而不是500错误代码。我们将在下一节的ProductController中添加一个方法,该方法将被执行并基于错误代码加载相关视图。这个中间件以一个字符串作为输入参数,无非是出错时应该执行的路由,为了传递错误代码,它允许一个{0}的占位符。因此,中间件配置如下所示:

    app.UseStatusCodePagesWithReExecute('/Products/Error/{0}');
    
  • Error handling: As for the presentation layer, unlike with the API, we need to redirect users to a custom page that in the case of runtime failures has relevant information, such as a user-friendly failure message and a relevant logging ID that can be used to retrieve the actual failure at a later stage. However, in the case of a development environment, we can show the complete error along with the stack. So, we will configure two middlewares as shown in the following code:

    {
    if (env.IsDevelopment())
        app.UseDeveloperExceptionPage();
    }
    else
    {
        app.UseExceptionHandler('/Products/Error/500');
    }
    

    在这里,我们可以看到,对于开发环境,我们使用的是UseDeveloperExceptionPage中间件,它将加载完整的异常堆栈跟踪,而对于非开发环境,我们使用的是UseExceptionHandler中间件,它走的是需要执行的错误动作方法的路径。此外,这里我们不需要定制的错误处理中间件,因为 ASP.NET Core 中间件负责将详细的错误记录到日志提供者,在我们的例子中是 Application Insights。

  • 使用 StaticFiles :为了允许各种静态文件,比如 CSS、JavaScript、图像和任何其他静态文件,我们不需要经过整个请求管道,这就是这个中间件发挥作用的地方,它允许服务静态文件,并支持短路静态文件管道的其余部分。

回到EcommerceService类,我们首先定义这个类的局部变量和构造函数,它将使用以下代码注入HTTPClient工厂和ApplicationSettings:

private readonly HttpClient httpClient;
private readonly ApplicationSettings applicationSettings;
public ECommerceService(IHttpClientFactory httpClientFactory, IOptions<ApplicationSettings> applicationSettings)
{
 NotNullValidator.ThrowIfNull(applicationSettings, nameof(applicationSettings));
 IHttpClientFactory httpclientFactory = httpClientFactory;
 this.httpClient = httpclientFactory.CreateClient();
 this.applicationSettings = applicationSettings.Value;
}

现在,为了按照我们的IECommerceService接口实现方法,我们将对获取 API 使用以下步骤:

https://github.com/OpenDocCN/freelearn-csharp-zh/raw/master/docs/enter-app-dev-cs9-dnet5/img/Figure_11.5_B15927.jpg

图 11.5–获取对应用编程接口的调用

基于上图中的步骤,GetProductsAsync的实现主要用于为登录页面检索产品,并在进行产品搜索时应用任何过滤器,如下代码所示:

public async Task<IEnumerable<ProductListViewModel>> GetProductsAsync(string filterCriteria = null)
{
    IEnumerable<ProductListViewModel> products = new List<ProductListViewModel>();
    using var productRequest = new HttpRequestMessage(HttpMethod.Get, $'{this.applicationSettings.ProductsApiEndpoint}?filterCriteria={filterCriteria}');
    var productResponse = await this.httpClient.SendAsync(productRequest).ConfigureAwait(false);
    if (!productResponse.IsSuccessStatusCode)
    {        await this.ThrowServiceToServiceErrors(productResponse).ConfigureAwait(false);
    }
    if (productResponse.StatusCode != System.Net.HttpStatusCode.NoContent)
    {
        products = await productResponse.Content.ReadFromJsonAsync<IEnumerable<ProductListViewModel>>().ConfigureAwait(false);
    }
    return products;
}

对于的POST/PUTAPI,我们会有类似的步骤稍加修改,如下图所示:

https://github.com/OpenDocCN/freelearn-csharp-zh/raw/master/docs/enter-app-dev-cs9-dnet5/img/Figure_11.6_B15927.jpg

图 11.6–调用应用编程接口后

基于这一点,主要用来创建购物车的CreateOrUpdateOrder的策略实现如下代码所示:

public async Task<OrderDetailsViewModel> CreateOrUpdateOrder(OrderDetailsViewModel order)
{
    NotNullValidator.ThrowIfNull(order, nameof(order));
    using var orderRequest = new StringContent(JsonSerializer.Serialize(order), Encoding.UTF8, ContentType);
    var orderResponse = await this.httpClient.PostAsync(new Uri($'{this.applicationSettings.OrdersApiEndpoint}'), orderRequest).ConfigureAwait(false);
    if (!orderResponse.IsSuccessStatusCode)
    {
        await this.ThrowServiceToServiceErrors(orderResponse).ConfigureAwait(false);
    }
    var createdOrder = await orderResponse.Content.ReadFromJsonAsync<OrderDetailsViewModel>().ConfigureAwait(false);
    return createdOrder;
}

同样,我们将使用上述策略之一并使用相关的 API 端点来实现GetProductByIdAsync、GetOrderByIdAsync、GetInvoiceByIdAsync和SubmitOrder。

现在,让我们创建将与EcommerceService对话并加载相关视图的控制器和动作方法。

创建控制器和动作

我们已经看到路由负责将请求 URI 映射到一个控制器中的一个动作方法,所以让我们进一步了解动作方法是如何加载各自的视图的。正如您已经注意到的,ASP.NET Core MVC 项目中的所有视图都是Views文件夹的一部分,当动作方法执行完成时,它只是寻找Views/<ControllerName>/<Action>.cshtml。

例如,映射到Products/Index路线的动作方法将加载Views/Products/Index.cshtml视图。这是通过在每个动作方法的末尾调用Microsoft.AspNetCore.Mvc.Controller.View方法来处理的。

下面的截图显示了我们的 MVC 应用中的一个图片表示:

https://github.com/OpenDocCN/freelearn-csharp-zh/raw/master/docs/enter-app-dev-cs9-dnet5/img/Figure_11.7_B15927.jpg

图 11.7–控制器-视图映射

有额外的重载和辅助方法可以覆盖这种行为,并根据需要路由到不同的视图。在我们谈论这些助手方法之前,就像 Web API 一样,MVC 控制器中的每个动作方法也可以返回IActionResult,这意味着我们可以利用助手方法重定向到一个视图。在 ASP.NET Core MVC 中,每个控制器都由一个基类Microsoft.AspNetCore.Mvc.Controller继承,这个基类带有一些助手方法,通过一个动作方法加载视图由Microsoft.AspNetCore.Mvc.Controller类中的以下助手方法处理:

  • View:该方法有多个重载,主要是根据控制器名称从Views下的文件夹加载视图。例如,在ProductsController中调用此方法可以从Views/Products文件夹加载任何.cshtml文件。此外,它可以采用视图的名称,如果需要可以加载该名称,并支持传递一个对象,该对象可以通过在视图中强有力地键入视图来检索。
  • RedirectToAction:虽然View方法处理大部分场景,但是也会有需要在同一个控制器或者另一个控制器内调用另一个动作方法的场景,这也是RedirectToAction帮忙的地方。此方法附带各种重载,允许我们指定操作方法、控制器方法和对象的名称,操作方法可以接收这些名称作为路由值。

简而言之,为了加载视图并从控制器传递数据,我们将把各自的模型传递给View方法,并且根据需要,每当需要调用另一个动作方法时,我们将使用RedirectToAction。

现在的问题是如何处理数据检索(GET调用)和数据提交(调用),在 ASP.NET Core MVC 中,所有的动作方法都支持使用HttpGet和HttpPost属性用 HTTP 动词进行标注。以下是一些可用于注释方法的规则:

  • 如果我们想要检索数据,那么使用HttpGet对动作方法进行注释。
  • 如果我们想向一个动作方法提交数据,应该使用HttpPost进行注释,相关对象作为该动作方法的输入参数。

通常,需要从控制器向视图发送数据的方法应使用[HttpGet]进行注释,需要从视图接收数据以进一步提交到数据库的方法应使用[HttpPost]进行注释。

现在,让我们继续添加所需的控制器并实现它们。当我们添加Packt.Ecommerce.Web时,它会用默认创建的HomeController创建一个Controllers文件夹,我们需要删除它。然后我们需要通过右击控制器文件夹| 添加 | 控制器 | MVC 控制器 | 清空,命名为ProductsController、CartController和OrdersController。

所有这些控制器将具有以下两个共同的属性,一个用于日志记录,一个用于调用EcommerceService的方法。它们使用构造函数注入进一步初始化,如下所示:

private readonly ILogger<ProductsController> logger;
private readonly IECommerceService eCommerceService;

现在让我们讨论一下在这些控制器中定义了什么:

  • ProductsController:该控制器将包含public async Task<IActionResult> Index(string searchString, string category)动作方式,加载默认视图,列出所有产品,进一步支持过滤。还有一种方法public async Task<IActionResult> Details(string productId, string productName),取产品的 ID 和名称,加载指定产品的详细信息。由于这两种方法都用于检索,因此将使用[HttpGet]对它们进行注释。此外,该控制器将具有前面讨论的Error方法。因为它可以从UseStatusCodePagesWithReExecute中间件接收一个错误代码作为输入参数,所以我们将有简单的逻辑来相应地加载视图:

    [Route('/Products/Error/{code:int}')]
    public IActionResult Error(int code)
    {
        if (code == 404)
        {
            return this.View('~/Views/Shared/NotFound.cshtml');
        }
        else
        {
            return this.View('~/Views/Shared/Error.cshtml', new ErrorViewModel { CorrelationId = Activity.Current?.RootId ?? this.HttpContext.TraceIdentifier });
        }
    }
    
  • CartController:该控制器包含public async Task<IActionResult> Index(ProductListViewModel product)动作方法,将产品添加到购物车中,我们将创建一个订单,订单状态设置为'Cart',因为这需要接收数据,并进一步传递给 API,API 将使用[HttpPost]进行注释。为了简单起见,这个左边是匿名的,但是可以对登录的用户进行限制。一旦创建了订单,该方法将使用RedirectToAction助手方法和重定向到该控制器内的public async Task<IActionResult> Index(string orderId)动作方法,该方法进一步将所有产品和结账表单加载到购物车中。这个方法也可以用来直接导航到购物车。

  • OrdersController:这是流程中最后一个控制器,包含填写付款明细后提交订单的public async Task<IActionResult> Create(OrderDetailsViewModel order)动作方式。该方法将订单状态更新为Submitted,然后为订单创建发票,最后重定向到另一个动作方法public async Task<IActionResult> Index(string invoiceId),该动作方法加载订单的最终发票并完成交易。

下图显示了控制器中各种方法之间的流程,以完成购物工作流:

https://github.com/OpenDocCN/freelearn-csharp-zh/raw/master/docs/enter-app-dev-cs9-dnet5/img/Figure_11.8_B15927.jpg

图 11.8–控制器动作方法之间的流程

有了这些知识,让我们设计下一节的视图。

使用 ASP.NET Core MVC 创建用户界面

到目前为止,我们已经定义了一个与后端 API 通信的服务,并进一步定义了控制器,该控制器将使用模型将数据传递给视图。现在,让我们构建各种视图来呈现数据并呈现给用户。

首先,让我们看看呈现视图所涉及的各种组件:

  • The Views folder: All views are part of this folder with each controller-specific view segregated by a subfolder, and finally, each action method is represented by a .cshtml file.

    要添加视图,我们可以右键点击动作方法,点击添加视图,系统会自动创建一个文件夹(如果还没有的话),里面有控制器的名字,然后添加视图。此外,在这样做的同时,我们可以指定视图将绑定到的模型。

  • The Layout page: This is a common requirement in a web application where we have a common section across the application, such as a header with a menu or left navigation. To have a modular structure for our pages and to avoid any repetition, ASP.Net Core MVC comes with a layout page that is typically named _Layout.cshtml and is part of the Views/Shared folder. This page can be used as a parent page for all the views in our MVC project. A typical layout page looks like the one shown in the following code:

    <!DOCTYPE html>
    <html lang='en'>
    <head>
        <meta charset='utf-8'>
        <meta name='viewport' content='width=device-width, initial-scale=1'>
        <meta http-equiv='x-ua-compatible' content='ie=edge'>
        <title>Ecommerce Packt</title>
    </head>
    <body class='hold-transition sidebar-mini layout-top-nav'>    
            <!-- Navbar -->        
            <!-- Main content -->        
             @RenderBody()
    </body>
    </html>
    

    这里可以看到,它允许我们定义应用的骨架布局,然后最后还有一个名为@RenderBody()的 Razor 方法,它实际上加载了子视图。要在任何视图中指定布局页面,我们可以使用以下语法,它将_Layout.cshtml作为父页面添加到视图中:

    @{
        Layout = '~/Views/Shared/__Layout.cshtml';
    }
    

    但是不需要在所有视图中重复这个代码,这就是_ViewStart.cshtml派上用场的地方。让我们看看它如何帮助跨视图重用一些代码:

  • _ViewStart.cshtml:这是一个通用视图,直接位于Views文件夹下,被 Razor 引擎用来执行任何需要在视图中的代码之前执行的代码。因此,通常情况下,这用于定义布局页面,因此,可以将前面的代码添加到该文件中,以便在整个应用中应用。

  • _ViewImports.cshtml:这是另一个页面,可以用来跨应用导入任何通用指令或命名空间。就像_ViewStart一样,这个也是直接位于根文件夹下;然而,_ViewStart和_ViewImport都可以在一个或多个文件夹中,并且它们从根视图文件夹中的文件夹到任何子文件夹中的较低级别的文件夹分层执行。为了使用 Application Insights 启用客户端遥测,我们注入了JavaScriptSnippet,如下面的代码所示。我们在中的 第 5 章中了解了将依赖服务注入视图.NET 。在下面的代码中,JavaScriptSnippet被注入到视图中:

        @inject Microsoft.ApplicationInsights.AspNetCore.JavaScriptSnippet JavaScriptSnippet
    
  • wwwroot: This is the root folder of the application and all the static resources, such as JavaScript, CSS, and any image files, are placed here. This can further hold any HTML plugins that we want to use in our application. As we have already configured the UseStaticFiles middleware in our application, content from the folder can be directly served without any processing. The default template of ASP.NET Core MVC comes with segregation of folders based on their type; for example, all JavaScript files are placed inside a js folder, CSS files are placed in a css folder, and so on. We will stick to that folder structure for our application.

    注意

    通过右键单击动作方法并使用内置模板自动生成视图的过程被称为脚手架,如果您不熟悉 Razor 语法,可以使用该过程。但是,使用支架创建视图或手动将其放置在相应的文件夹中并强有力地键入视图会导致相同的行为。

设置管理、布局页面和视图

在整个应用中获得相同外观和感觉的一个重要事项是选择正确的造型框架。这样做不仅提供了一致的布局,还简化了响应设计,这有助于以各种分辨率正确呈现页面。我们用于Packt.Ecommerce.Web的 ASP.NET Core MVC 项目模板开箱即用,Bootstrap 作为其样式框架。我们将进一步扩展到一个名为AdminLTE的主题,它带有一些有趣的布局和仪表板,可以插入到我们的表示层中。

让我们执行以下步骤将AdminLTE集成到我们的应用中:

  1. 从这里下载AdminLTE的最新版本:https://github.com/ColorlibHQ/AdminLTE/releases。
  2. 提取上一步下载的 ZIP 文件,导航至AdminLTE-3.0.5\dist\css。复制adminlte.min.css并粘贴到Packt.Ecommerce.Web的wwwroot/css文件夹中。
  3. 导航至AdminLTE-3.0.5\dist\js。复制adminlte.min.js并粘贴到Packt.Ecommerce.Web的wwwroot/js文件夹中。
  4. 导航至AdminLTE-3.0.5\dist\img。复制需要的图片,粘贴到Packt.Ecommerce.Web的wwwroot/img文件夹中。
  5. 复制AdminLTE-3.0.5\plugins文件夹,粘贴到Packt.Ecommerce.Web的wwwroot文件夹中。

这样,我们的项目在解决方案探索者中应该如下所示。更多关于AdminLTE的信息可以在https://adminlte.io/docs/2.4/installation找到:

https://github.com/OpenDocCN/freelearn-csharp-zh/raw/master/docs/enter-app-dev-cs9-dnet5/img/Figure_11.9_B15927.jpg

图 11.9–管理主题设置

现在,导航到Views/_Layout.cshtml页面,删除所有的现有代码,并替换为Packt.Ecommerce.Web\Views\Shared\_Layout.cshtml的代码。在高层次上,布局分为以下几个部分:

  • 左侧带有主页导航的标题
  • 标题中有一个搜索框,中间有一个带有搜索类别的下拉列表
  • 右边头部的购物车
  • 显示导航的面包屑轨迹
  • 使用@RenderBody()渲染子视图的部分

完成AdminLTE模板集成所需的其他几个关键事项如下:

  • 添加在<head>标签中定义的以下样式:

    <link rel='stylesheet' href='~/plugins/fontawesome-free/css/all.min.css'>
    <link rel='stylesheet' href='~/css/adminlte.min.css'>
    
  • 在<body>标记的末尾添加以下 JavaScript 文件:

    <!-- REQUIRED SCRIPTS (Order shouldn't matter)-->
    <!-- jQuery -->
    <script src='~/plugins/jquery/jquery.min.js'></script>
    <!-- Bootstrap 4 -->
    <script src='~/plugins/bootstrap/js/bootstrap.bundle.min.js'></script>
    <!-- AdminLTE App -->
    <script src='~/js/adminlte.min.js'></script>
    

有了这个,我们将AdminLTE主题集成到我们的应用中。要使用应用 Insights 呈现启用客户端遥测所需的 JavaScript ,请在_Layout.cshtml的头标签中添加以下代码:

    @Html.Raw(JavaScriptSnippet.FullScript)

前面的代码注入了从视图中发送遥测数据所需的 JavaScript 以及工具键。与服务器端不同,在客户端,检测密钥是公开的。任何人都可以从浏览器开发工具中看到检测密钥。但这就是客户端遥测的设置方式。此时,风险在于恶意用户或攻击者可能会推送不需要的数据,因为检测密钥具有只写访问权限。如果您希望使客户端遥测更加安全,您可以从服务中公开一个安全的 REST API,并从那里记录遥测事件。您将在 第 14 章健康与诊断中了解更多应用洞察功能。

现在,应用布局已经准备好了。现在让我们继续定义应用中的各种视图。

创建产品/索引视图

该视图将用于列出我们的电子商务应用上可用的所有产品,并使用IEnumerable<Packt.Ecommerce.DTO.Models.ProductListViewModel>模型进行强类型化。它使用ProductsController的Index动作方法来检索数据。

在这个视图中,我们将使用一个简单的 Razor @foreach (var item in Model)循环,对于每个产品,我们将显示产品的图像、名称和价格。该视图的示例如下图所示:

https://github.com/OpenDocCN/freelearn-csharp-zh/raw/master/docs/enter-app-dev-cs9-dnet5/img/Figure_11.10_B15927_Corrected.jpg

图 11.10–产品视图

在这里,您可以看到布局页面中有一个搜索栏和一个类别下拉列表。点击产品图像将导航至Products/Details视图。为了支持该导航,我们将利用AnchorTagHelper并将产品标识和名称传递给ProductsController的Details动作方法,以进一步在Products/Details视图中加载产品的详细信息。

创建产品/详细信息视图

该视图将根据从Products/Index视图传递的产品标识和名称加载产品的详细信息。我们将使用来自AdminLTE的示例页面,如下所示:https://AdminLTE . io/themes/dev/AdminLTE/pages/examples/e _ commerce . html。

该页面将使用Packt.Ecommerce.DTO.Models.ProductDetailsViewModel进行强类型输入,并将显示产品的所有详细信息。下面的截图显示了该页面的示例:

https://github.com/OpenDocCN/freelearn-csharp-zh/raw/master/docs/enter-app-dev-cs9-dnet5/img/Figure_11.11_B15927_Corrected.jpg

图 11.11–产品详细信息视图

如您所见,这里有一个添加到购物车按钮;点击它将为用户创建购物车并将项目添加到该购物车。由于我们案例中的购物车只不过是订单状态设置为'Cart'的订单,因此这将调用CartController的Index动作方法来创建购物车。

为了将数据传递回动作方法,我们将借助FormTagHelper,它允许我们将页面包装成 HTML 形式,并使用以下代码指定页面可以提交到的动作和控制器:

<form asp-action='Index' asp-controller='Cart'>

有了这个代码,一旦点击Submit类型的添加到购物车按钮,页面就会被提交到CartController的Index动作方法,进一步保存到数据库中。但是,我们仍然需要将产品细节传递回Index动作方法,为此,我们将借助InputTagHelper为所有需要传递回动作方法的值创建隐藏字段。

这里最重要的是,隐藏变量的名称应该与模型中属性的名称相匹配,因此我们将在表单中添加以下代码,将产品值传递回控制器:

<input asp-for='Id' type='hidden'>
<input asp-for='Name' type='hidden'>
<input asp-for='Price' type='hidden'>
<input asp-for='ImageUrls[0]' type='hidden'>

ASP.NETCore MVC 的模型绑定系统读取这些值,并创建CartController的Index方法所需的产品对象,该方法进一步调用后端系统来创建订单。

注意

由于 第 12 章理解认证将涉及认证,因此对于本章,添加到购物车将直接向购物车添加产品。

创建购物车/索引视图

该视图将加载购物车详细信息,并将有一个结账表单来填写所有详细信息并完成订单。在这里,我们可以导航回主页添加更多产品或完成订单。

该视图使用Packt.Ecommerce.DTO.Models.OrderDetailsViewModel进行强类型化,并使用OrdersController的Index动作方法加载数据。这里,我们使用的自助结账表单示例来自于https://getbootstrap.com/docs/4.5/examples/checkout/。

这个表单利用模型验证和 HTML 属性对必需的字段进行验证,我们在 ASP.NET Core MVC 标签助手和一些 HTML 助手的帮助下呈现表单。具有模型验证的示例属性如以下代码所示:

public class AddressViewModel
{
        [Required(ErrorMessage = 'Address is required')]
        public string Address1 { get; set; }       
        [Required(ErrorMessage = 'City is required')]
        public string City { get; set; }
        [Required(ErrorMessage = 'Country is required')]
        public string Country { get; set; }
}

由于此表单也需要提交,所以整个表单被包裹在FormTagHelper中,如下代码所示:

<form asp-action='Create' asp-controller='Orders'>

要在用户界面上显示这些验证,在我们之前添加的所有其他脚本之后,将以下脚本添加到_layout.cshtml中:

<script src='~/lib/jquery-validation/dist/jquery.validate.min.js'></script>
<script src='~/lib/jquery-validation-unobtrusive/jquery.validate.unobtrusive.min.js'></script>

为了显示错误消息,我们可以使用验证消息标记帮助器,如下面的代码片段所示。在服务器端,这可以使用ModelState.IsValid进一步评估:

<input asp-for='ShippingAddress.Address1' class='form-control' placeholder='1234 Main St'/>
<span asp-validation-for='ShippingAddress.Address1' class='text-danger'></span>

该页面的示例如下图所示:

https://github.com/OpenDocCN/freelearn-csharp-zh/raw/master/docs/enter-app-dev-cs9-dnet5/img/Figure_11.12_B15927_Corrected.jpg

图 11.12–购物车和结账页面

我们将使用InputTagHelper作为隐藏字段和文本框,将任何附加信息传递回动作方法。文本框的好处是,如果文本框的的id属性与属性名匹配,数据将自动传递回动作方法,ASP.NET Core MVC 的模型绑定系统将负责将其映射到所需的对象,在这种情况下,该对象是Packt.Ecommerce.DTO.Models.OrderDetailsViewModel类型,最终提交订单,生成发票,并重定向到Orders/Index动作方法。

注意

在前面的截图中,虽然我们有一个结账表单,其中包括生产应用中的支付信息,但我们将与第三方支付网关集成,通常,出于各种安全原因,整个表单位于应用的支付网关端。https://stripe.com/docs/api和https://razor pay . com/docs/支付网关/服务器集成/dot-net/ 就是这样一对帮助支付网关集成的第三方提供商。

创建订单/索引视图

最后,我们会看到订单的发票视图,这是一个简单的只读视图,显示从OrdersController的Index动作方式发送的发票信息。下面的截图显示了该页面的示例:

https://github.com/OpenDocCN/freelearn-csharp-zh/raw/master/docs/enter-app-dev-cs9-dnet5/img/Figure_11.13_B15927_Corrected.jpg

图 11.13–最终发票

这就完成了各种视图的集成,正如您所看到的,我们将视图限制在电子商务应用中最重要的流程中。但是,您可以使用相同的原则进一步添加更多功能。

理解布拉佐

Blazor 是一个新的框架,可从.NET Core 3.1 继续开发应用的前端层。它是 MVC 和 Razor Pages 的替代品之一,应用模型非常接近 SPA 但是,我们可以用 C#和 Razor 语法编写逻辑,而不是 JavaScript。

用 Blazor 编写的所有代码都放在一个叫做 Razor 组件的东西中,它允许您编写 HTML 以及代码的 C#部分来构建任何网页。Razor 组件带有.Razor的扩展,用于表示应用;无论是整个网页还是一个小对话框弹出,一切都是作为组件在 Blazor 应用中创建的。典型的 Razor 组件看起来像下面代码片段中的组件:

@page '/counter'
<h1>Counter</h1>
<p>Current count: @currentCount</p>
<button class='btn btn-primary' @onclick='IncrementCount'>Click me</button>
@code {
    private int currentCount = 0;
    private void IncrementCount()
    {        currentCount++;    }
}

在这段代码中,我们创建了一个在点击按钮时递增计数器的页面,点击事件的逻辑在 C#代码中处理,它更新了 HTML 中的值。本页面可使用/counter相对网址访问。

Blazor 与其他 MVC/Razor Pages 的主要区别在于,与请求-响应模型不同,在请求-响应模型中,每个请求都被发送到服务器,HTML 被发送回浏览器,Blazor 将所有组件打包(就像 SPA 一样)并加载到客户端。当第一次请求应用时,对服务器的任何后续调用都是为了检索/提交任何应用编程接口数据或更新 DOM。Blazor 支持以下两种托管模式:

  • Blazor web assembly(WASM):WASM 是可以在现代浏览器上运行的低级指令,这进一步有助于在没有任何额外插件的浏览器上运行用 C#等高级语言编写的代码。Blazor WASM 托管模型利用了 WASM 给出的开放网络标准,并在浏览器的沙箱环境中运行任何 Blazor WASM 应用的 C#代码。在高层次上,所有的 Blazor 组件都被编译成。. NET 程序集和下载到浏览器,WASM 加载.NET Core 运行时并加载所有程序集。它进一步使用 JavaScript 互操作来刷新 DOM 对服务器的唯一调用将是任何后端 API。架构如下图所示:

https://github.com/OpenDocCN/freelearn-csharp-zh/raw/master/docs/enter-app-dev-cs9-dnet5/img/Figure_11.14_B15927.jpg

图 11.14–布拉佐 WASM 托管

  • Blazor 服务器:在 Blazor 服务器托管模型中,Blazor 应用托管在一个 web 服务器上,在那里进行编译,然后客户端利用 SignalR 从服务器接收更新。为了保持连接的活跃性,Blazor 创建了一个名为blazor.server.js的 JavaScript 文件,并使用 SignalR 接收所有的 DOM 更新,这进一步意味着每个用户交互都将有一个服务器调用(尽管很轻)。架构如下图所示:

https://github.com/OpenDocCN/freelearn-csharp-zh/raw/master/docs/enter-app-dev-cs9-dnet5/img/Figure_11.15_B15927.jpg

图 11.15–Blazor 服务器托管

.NET 5 对这两种托管模型都提供了完整的工具支持,都有自己的项目模板,各有利弊,这里进一步说明:https://docs . Microsoft . com/en-us/aspnet/core/blazor/hosting-models?view=aspnetcore-5.0 。

现在让我们使用 Blazor Server 应用按照以下步骤创建一个前端应用,它允许我们为我们的电子商务应用添加/修改产品详细信息:

  1. Add a new Blazor Server application called Packt.Ecommerce.Blazorweb to the enterprise solution and add the Products.razor, AddProduct.Razor, and EditProduct.razor Razor components to the Pages folder, as shown in the following screenshot:

    https://github.com/OpenDocCN/freelearn-csharp-zh/raw/master/docs/enter-app-dev-cs9-dnet5/img/Figure_11.16_B15927.jpg

    图 11.16–Blazor 服务器项目

  2. 这个项目包含Program和Startup类,它们与任何其他 ASP.NET Core 应用完全一样,只是有一些额外的 Blazor 服务。_Host.cshtml是应用的根,这个页面接收到对应用的初始调用,并用 HTML 进行响应。本页进一步引用了信号连接的blazor.server.js脚本文件。另一个重要的组件是App.Razor组件,它负责基于网址的路由。在 Blazor 中,任何需要映射到特定 URL 的组件都会在组件的开头有@page指令,指定应用的相对 URL。App.Razor拦截网址并将其路由到指定的组件。所有 Razor 组件都是Pages文件夹的一部分,Data文件夹附带了一个示例模型和一个在FetchData.razor组件中使用的服务。

  3. 让我们将以下代码添加到NavMenu.razor以将Products导航添加到左侧菜单。在这个阶段,如果你运行应用,你应该可以看到左侧菜单的Products导航;但是,它不会导航到任何页面:

    <li class='nav-item px-3'>
     <NavLink class='nav-link' href='products'>
      <span class='oi oi-list-rich' aria-hidden='true'></span> Products
     </NavLink>
    </li>
    
  4. 当我们要从 API 中检索数据时,我们需要将HTTPClient注入到我们的Startup类中,就像在 ASP.NET Core 应用中一样。因此,将以下代码添加到ConfigureServices方法中,并将ApplicationSettings:ProductsApiEndpoint添加到appsettings.json :

    services.AddHttpClient('Products', client => {
                    client.BaseAddress = new Uri(Configuration ['ApplicationSettings:ProductsApiEndpoint']);
                });
      'ApplicationSettings': {
        'ProductsApiEndpoint': 'https://localhost:44346/api/products/'
      },
    
  5. 既然我们要绑定products数据,那么就添加Packt.Ecommerce.DTO.Models作为Packt.Ecommerce.Blazorweb的项目引用。在Pages文件夹中,将以下代码添加到@code块内的Products.razor页面,在该页面中,我们正在使用IHttpClientFactory创建一个HttpClient对象,该对象将在下一步注入,并使用OnInitializedAsync方法检索products数据:

    private List<ProductListViewModel> products;
    protected override async Task OnInitializedAsync()
        {
            var client = Factory.CreateClient('Products');
            var result = await client.GetAsync('').ConfigureAwait(false);
            result.EnsureSuccessStatusCode();
            products = new List<ProductListViewModel>();
            products = await result.Content.ReadFromJsonAsync<List<ProductListViewModel>>().ConfigureAwait(false);
        }
    
  6. 接下来,在Products.Razor页面的开始处(在@code块之外)添加以下代码。在这里,我们通过@page指令到/products为该组件设置相对路线。接下来,我们注入IHttpClientFactory和其他需要的名称空间,然后添加呈现产品列表的 HTML 部分。如您所见,它是 HTML 和 Razor 语法的混合:

    @page '/products'
    @inject IHttpClientFactory Factory
    @using System.Net.Http.Json;@using Packt.Ecommerce.DTO.Models;
    <h1>Products</h1>
    <div>    <a class='btn btn-info' href='addproduct'><i class='oi oi-plus'></i> Add Product</a> </div>
    @if (products == null)
    { <p><em>Loading...</em></p> }
    else { <table class='table'><thead><tr>
                    <th>Id</th><th>Name</th>
                    <th>Price</th><th>Quantity</th>
                    <th>ImageUrls</th><th></th>
                </tr></thead><tbody>
                @foreach (var product in products)
                {<tr>
                       <td>@product.Id</td>
                        <td>@product.Name</td>
                        <td>@product.Price</td>
                        <td>@product.Quantity</td>
                        <td><img src='@product.ImageUrls[0]' class='product-image w-10 col-3' alt='Product' /></td>
                        <td><a class='btn btn-info' href='editproduct/@product.Id/@product.Name'><i class='oi oi-pencil'></i></a></td></tr>
                }
            </tbody></table> }
    

此时,如果您运行应用,您应该会看到如下截图所示的输出:

https://github.com/OpenDocCN/freelearn-csharp-zh/raw/master/docs/enter-app-dev-cs9-dnet5/img/Figure_11.17_B15927_Corrected_(1)_(1).jpg

图 11.17–产品列表 Blazor 用户界面

接下来,让我们创建Add/Edit页面,在其中我们将使用 Blazor 表单。表单可用的一些重要工具/组件如下:

  1. Blazor 表单是使用 Blazor 中称为EditForm的现成模板创建的,它可以使用模型属性直接绑定到任何 C#对象。典型的EditForm如下代码片段所示。这里,我们定义在表单提交时调用OnSubmit方法。让我们把这个加到AddProduct.razor :

    <EditForm Model='@product' OnSubmit='@OnSubmit'>
    </EditForm>
    
  2. 这里,product是我们想要使用的模型的对象,在我们的例子中是Packt.Ecommerce.DTO.Models.ProductDetailsViewModel。为了将数据绑定到任何控件,我们可以混合使用 HTML 和 Razor 语法,如下面的代码所示。这里,我们将产品对象的Name属性绑定到一个文本框,类似地,将Category属性绑定到下拉列表。一旦您在文本框中输入任何值或在下拉列表中选择一个值,这些属性中的将自动将其传递回任何后端应用编程接口或数据库。让我们以类似的方式将所有必需的属性添加到 HTML 元素中:

    <InputText id='category' @bind-Value='product.Name'></InputText>
    <InputSelect @bind-Value='product.Category'>
     <option selected disabled value='-1'> Choose Category</option>
     <option value='Clothing'>Clothing</option>
     <option value='Books'>Books</option>
    </InputSelect>
    
  3. Blazor 表单支持使用数据注释进行数据验证,因此我们想要绑定到 UI 的任何模型都可以有数据注释,Blazor 会将这些验证应用于属性绑定到的控件。为了应用验证,我们添加了DataAnnotationsValidator组件,并且可以使用ValidationSummary组件来显示所有验证失败的摘要。我们可以在控制级别进一步使用ValidationMessage组件,如下面的代码片段所示:

    <DataAnnotationsValidator />
    <ValidationSummary />
    <InputNumber id='quantity' @bind-Value='product.Quantity'></InputNumber>
    <ValidationMessage For='@(() => product.Quantity)' />
    
  4. 在code组件中,添加一个ProductDetailsViewModel的对象,并将其命名为产品,即EditForm的Model属性中定义的,进一步执行OnSubmit方法。

AddProduct.Razor和EditProduct.Razor的完整代码可以在 GitHub repo 中找到,一旦我们运行应用,我们可以看到以下页面:

https://github.com/OpenDocCN/freelearn-csharp-zh/raw/master/docs/enter-app-dev-cs9-dnet5/img/Figure_11.18_B15927.jpg

图 11.18–添加产品浏览器用户界面

这是一个使用 Blazor 构建前端的基本示例,它执行列表、创建和更新操作。然而,《布拉佐尔》中有很多概念可以在https://docs.microsoft.com/en-us/aspnet/core/blazor/?进一步探索 view=aspnetcore-5.0 。

总结

在本章中,我们了解了表示层和用户界面设计的各个方面。与此同时,我们还学习了使用 ASP.NET Core MVC 和 Razor Pages 开发表示层的各种技巧,最后,我们使用 ASP.NET Core MVC 和 Blazor 为我们的企业应用实现了表示层。

有了这些技能,您应该能够使用 ASP.NET Core MVC、Razor Pages 和 Blazor 构建表示层,并将其与后端 API 集成。

在下一章中,我们将看到如何跨应用的各个层在我们的系统中集成身份验证。

问题

  1. Which one of the following is a recommended page to define the left-side navigation that needs to appear throughout the web application?

    a._ViewStart.cshtml

    b._ViewImports.cshtml

    c._Layout.cshtml

    d.Error.cshtml

  2. Which of the following pages can be used to configure the Layout page for the entire application?

    a._ViewStart.cshtml

    b._ViewImports.cshtml

    c._Layout.cshtml

    d.Error.cshtml

  3. Which of the following special characters is used to write Razor syntax in a .cshtml page?

    a.@

    b.#

    c.<% %>

    d.以上都不是

  4. Which method will be called on a button click in the following tag helper code in a Razor page application?

    <input type='submit' asp-page-handler='Delete' value='Delete' />
    

    a.OnGet()

    b.onDelete()

    c.OnPostDelete()

    d.OnDeleteAsync()

进一步阅读

展锐双摄帧同步实战:从配置到调试的完整避坑指南 本文深入剖析了在展锐平台上实现双摄帧同步的完整流程与核心技术。详细对比了硬件帧同步与软件帧同步的差异、应用场景及核心挑战,提供了从XML配置、驱动层适配到调试验证的实战指南。重点强调了硬件帧同步必须与固定帧率强绑定这一关键避坑点,并分享了针对景深虚化等场景的同步质量验证方法及长稳测试策略。 阅读详情

相关推荐

Dbeaver连接TDengine时序数据库

也没啥好总结的,就一句话:Dbeaver真香,而且还是我大JAVA实现的,赞。希望能帮到大家,uping。

zwrlj527的专栏 1903

【.NET高手私藏干货】:appsettings.json性能优化与安全加密实战

掌握ASP.NET Core配置文件appsettings.json的性能优化与安全加密技巧,提升应用启动速度与配置安全性。涵盖敏感信息加密、多环境配置管理及缓存策略,适用于中大型项目部署,值得收藏。

Instrulink的博客 1039

AD7920实战:从咪头到ADC的音频信号采集电路设计全解析(附电路图)

本文详细解析了基于AD7920 ADC芯片的音频信号采集电路设计全流程。从驻极体咪头的驱动、AD8542运放的两级增益与偏置设计,到关键的抗混叠滤波器配置,以及ADC接口的驱动缓冲与PCB布局要点,提供了完整的实战指南与电路图,帮助开发者实现高质量音频信号采集。

red88的博客 802

微软正式发布 C# 10,支持.NET 6 和 Visual Studio 2022 (附更新内容大全)

微软中国 MSDN宣布 C# 10 作为 .NET 6 和 Visual Studio 2022 的一部分已经发布了。在这篇文章中,微软将介绍 C# 10 的许多新功能,这些功能使你的代码更漂亮、更具表现力、更快。 阅读Visual Studio 2022 公告和.NET 6 公告以了解更多信息,包括如何安装。 Visual Studio 2022公告 https://aka.ms/vs2022gablog .NET 6 https://aka.ms/dotnet6-GA 全局和隐...

m0_60538503的博客 2357

[Cocos2d塔防游戏开发]Cocos2dx-3.X完成塔防游戏《王国保卫战》--地图(一)

上面3章图是完成后的效果图 游戏已完成,除了英雄外,基本还原了90%的游戏内容,一共13关,20种防御塔,30+种敌人,如上图,以假乱真吧 下面从地图模块起介绍我的方法,如有更好的方法,请留言一起讨论,游戏资源下载原版游戏数据包,解压即可 推荐一款软件,TextureUnpackerRelease1.04可以分割plist形式的图片,提高效率 经过一个月学习也发现之前有很多化简为繁的错误

7172

onnx入门教程(一):解决模型部署中的难题

直接使用 PyTorch 模型的话,我们修改几行代码就能实现模型输入的动态化。但在模型部署中,我们要花数倍的时间来设法解决这一问题。现在,让我们顺着解决问题的思路,体验一下模型部署的困难,并学习使用自定义算子的方式,解决超分辨率模型的动态化问题。刚刚的报错是因为 PyTorch 模型在导出到 ONNX 模型时,模型的输入参数的类型必须全部是 torch.Tensor。而实际上我们传入的第二个参数" 3 "是一个整形变量。这不符合 PyTorch 转 ONNX 的规定。我们必须要修改一下原来的模型的输入。

图像学习之旅 1128

C#9 和 .NET5 企业级应用开发(一)

NET 5 是一个开源的免费平台,可以编写针对任何平台的应用。该平台还为您提供了轻松编写应用的机会,面向任何平台,包括云。作为软件开发人员,我们被赋予构建复杂企业应用的责任。在本书中,我们将学习使用 C# 9 和构建企业应用的各种高级架构和概念.NET 5。这本书将作为使用构建企业应用所需的所有功能的圣经.NET 5。通过对基本概念、实例和自我评估问题的逐步解释,您将深入了解和接触到的每个重要组成部分.NET 5 需要构建一个专业的企业应用。

龙哥盟 1145

TensorRT-LLM.V1.1.0rc0:在无 GitHub 访问权限的服务器上编译 TensorRT-LLM 的完整实践

1.NGC上的预构建发布容器进行部署,见《2.通过pip进行部署。3.从源头构建再部署。在实际开发中,我们常常面临这样的场景:本地笔记本为 Windows 系统,虽然可以访问 GitHub,但受限于硬件性能,编译大型项目(如 TensorRT-LLM)耗时过长;而公司或实验室提供的 Linux 服务器性能强劲,适合编译任务,但却因网络策略限制无法连接外网,尤其是无法访问 GitHub。。

xinvictory的专栏 1203

C#9 和 .NET5 高级教程(一)

原文:Pro C# 9 with .NET 5 协议:CC BY-NC-SA 4.0 一、C# 和 .NET(Core)5 简介 微软的。NET 平台和 C# 编程语言大约在 2002 年正式引入,并迅速成为现代软件开发的中流砥柱。的。NET 平台使得大量的编程语言(包括 C#、VB.NET 和 F#)能够相互交互。用 C# 写的程序可以被用 VB.NET 写的另一个程序引用。本章稍后将详细介绍这种互操作性。 2016 年,微软正式推出。NET 核心。比如。网,。NET Core 允许语言之间的互操作(

龙哥盟 2339

RT-DETR改进策略【注意力机制篇】| 2024 PPA 并行补丁感知注意模块,提高小目标关注度

本文记录的是利用改进检测精度,详细说明了优化原因,注意事项等。原论文在红外小目标检测任务中,小目标在多次下采样操作中容易丢失关键信息。通过替代编码器和解码器基本组件中的传统卷积操作,更好地保留小目标的重要信息。HCF-Net: Hierarchical Context Fusion Network for Infrared Small Object Detection采用多分支特征提取策略,通过不同分支提取不同尺度和层次的特征。利用局部、全局和串行卷积分支,对输入特征张量进行处理。通过控制 patch si

Limiiiing的博客 1648

摆脱英文束缚!群晖Transmission4.0.4全新中文界面攻略

在群晖安装完官方Transmission4.0.4套件后,界面是英文状态的,不太好用。但是按照网上分享的原来4.0以前版本的方法更新UI又失败,所以自己搞了下成功了,分享给需要的朋友。

XiaoqiangClub的博客 5215

C#9 和 .NET5 企业级应用开发(四)

ASP.NET Core 身份是一个基于会员资格的系统,提供了一个简单的方法来添加登录和用户管理功能到您的应用。它提供了一个 UI 和应用编程接口API)来创建新的用户帐户、提供电子邮件确认、管理用户配置文件数据、管理密码(如更改或重置密码)、登录、注销等,并启用多因素身份验证MFA它还允许您与外部登录提供商集成,如微软账户、谷歌、脸书、推特和许多其他社交网站,以便用户可以使用他们现有的账户注册,而不是创建新的账户,从而增强用户体验。

龙哥盟 792

数据挖掘实战-基于随机森林算法的交通事故预测模型

本实验数据集来源于Kaggle,原始数据集共有209306条,该数据集包含不同地区和时间段的交通事故详细信息。它包括各种指标,例如事故日期、天气状况、照明条件、碰撞类型、受伤人数和车辆参与情况。数据涵盖多个地点和事故类型,可全面了解交通事故及其原因。具体包括:crash_date:事故发生的日期。Traffic_control_device:所涉及的交通控制设备的类型(例如交通灯、标志)。weather_condition:事故发生时的天气状况。

m0_64336780的博客 1万+

NET 5、NET 6、NET 7、NET 8 和 .NET 9 之间的区别

各版本的演进体现了微软对云原生、高性能计算和开发者体验的持续投入,企业可根据自身需求和技术路线选择最适合的版本。

Code365 6266

ssm框架搭建

这个是一个在idea上成功跑起来的ssm框架项目,保存在这里方便以后使用直接下载

汉书HE8,好用的汇编代码编写工具

汉书HE8,好用的汇编代码编写工具早期编写汇编代码所用功能强大

C#9 和 .NET5 企业级应用开发(二)

企业应用可能面临的一个大问题是将不同的元素连接在一起并管理它们的生命周期的复杂性。为了解决这个问题,我们使用控制反转 ( IoC )原理,该原理建议消除对象之间的依赖性。通过委派控制流,IoC 使程序可扩展并增加了模块化。事件、回调委托、观察者模式和依赖注入 ( DI )是实现 IoC 的一些方法。在本章中,我们将了解以下内容:到本章结束时,您将对直接投资有一个很好的了解,充分利用直接投资.NET 5 应用,ASP.NET Core 5 中提供的范围类型,以及如何在项目中利用它们。对微软的基本了解.NET

龙哥盟 475

迈瑞BS系列全自动生化分析仪LIS协议接口手册(内含HL7和ASTM)

迈瑞BS系列全自动生化分析仪LIS协议接口手册,内含HL7和ASTM)

《C#高级编程(第9版)——C# 5.0 & .NET 4.5.1》试读试评论

关于厚书,我想很多程序员应该是出于一种“宝典情结”而喜欢的,而这本《C#高级编程》就像武功秘笈《葵花宝典》,当然,我的意思不是说学C#得自宫,而是因为我觉得它算得上博大精深,也能在掌握后用C#快速构建应用,正好符合《葵花宝典》中的武功以“快”为主的特点。

养心殿 1266

lwip-2.0.3

lwip是瑞典计算机科学院(SICS)的Adam Dunkels 开发的一个小型开源的TCP/IP协议栈。实现的重点是在保持TCP协议主要功能的基础上减少对RAM 的占用。

C#9 和 .NET5 企业级应用开发(五)

为了将一个产品的想法变为现实,不管你的团队规模有多大,你都需要一种有效的方法来规划你的工作,在你的团队中进行协作,以及构建、测试和部署。Azure DevOps 帮助您应对这些挑战,并为您的成功提供各种服务和工具。Azure DevOps 服务可以通过网络访问,也可以从流行的开发 ide 中访问,如 Visual Studio、Visual Studio Code、Eclipse 等。使用 Azure DevOps 服务器,Azure DevOps 服务既可以在云中使用,也可以在内部使用。Boards。

龙哥盟 452

【2021年新书推荐】Enterprise Application Development with C# 9 and .NET 5

各位好,此账号的目的在于为各位想努力提升自己的程序员分享一些全球最新的技术类图书信息,今天带来的是2021年3月由Packt出版社最新出版的一本关于C# 9和.NET 5的书。 Enterprise Application Development with C# 9 and .NET 5 作者:Ravindra Akella , Arun Kumar Tamirisa , Suneel Kumar Kunani 出版社:Packt 出版时间:2021-03-19 ISBN:9781800209442 图书

tushufenxiang的博客 316

【C#.Net】C#开发的未来前景

C#的前景稳定且有增量:企业级后端、Unity游戏开发是基本盘,云原生、MAUI、AI+NET是增量方向,不会被淘汰;核心竞争力在高价值场景:Unity游戏、金融/政务企业级开发、跨平台桌面应用是C#不可替代的优势领域;学习C#需绑定具体场景:单纯学语法价值有限,结合Unity/企业级后端/云原生等场景,能大幅提升就业竞争力。

u013136510的博客 1377

C#9 和 .NET5 软件架构(一)

本书涵盖了现代基于云和分布式软件架构中涉及的最常见设计模式和框架。它通过提供实际的现实场景,讨论了何时以及如何使用每种模式。本书还介绍了 DevOps、微服务、Kubernetes、持续集成和云计算等技术和流程,以便您可以开发并交付给客户最佳的软件解决方案。本书将帮助您了解客户想要的产品。它将指导您在开发过程中解决可能遇到的最大问题。它还涵盖了在云环境中管理应用程序时需要遵循的注意事项,以及您需要遵循的做和不做。

龙哥盟 1203

C#9 和 .NET5 软件架构(二)

微服务架构允许将解决方案的每个模块独立于其他模块进行扩展,以实现最大吞吐量和最小成本。事实上,对整个系统进行扩展而不是当前瓶颈部分必然会导致资源的明显浪费,因此对子系统扩展的细粒度控制对系统的整体成本有着重要影响。然而,微服务不仅仅是可扩展的组件-它们是可以独立开发、维护和部署的软件构建块。

龙哥盟 1041

视频教程-C#/.Net企业级系统架构设计实战精讲教程-.NET

C#/.Net企业级系统架构设计实战精讲教程 徐照兴,教研室主任,硕士,副教...

weixin_31708717的博客 974

用.NET9+Blazor+Semantic Kernel,打造企业级AI知识库和智能体平台——AntSK深度解读

实现接口,注册到服务容器即可。AntSK不是“又一个AI知识库”,而是“企业级AI应用的万能底座”。它用.NET9和Blazor的现代技术栈,集成了Semantic Kernel、Kernel Memory等AI能力,支持多模型、多插件、文生图、OCR等丰富功能,适合企业、开发者、AI爱好者快速搭建自己的AI应用。无论你是想打造企业知识库、智能客服、AI写作助手,还是开发自己的AI App,AntSK都能让你“少踩坑、多赚钱”。还等什么?赶紧下载体验,开启你的AI赋能之旅吧!

许泽宇的技术分享 1384

C#学习路径终极指南:从零基础到企业级开发的进阶密码

C#学习路径终极指南:从零基础到企业级开发的进阶密码

realzhangpeng的博客 1289

C#/.NET/.NET Core优秀项目和框架2025年12月简报

公众号每月定期推广和分享的C#/.NET/.NET Core优秀项目和框架(每周至少会推荐两个优秀的项目和框架当然节假日除外),公众号推文中有项目和框架的详细介绍、功能特点、使用方式以及部分功能截图等(打不开或者打开GitHub很慢的同学可以优先查看公众号推文,文末一定会附带项目和框架源码地址)。追逐时光者,第一时间获取每周精选分享资讯🔔)。

专注于C#/.NET/.NET Core学习、工作、面试干货和实战教程分享。这里聚焦了大量的C#/.NET/.NET Core优质文章、开源项目、实用工具和学习、工作、面试心得。 1289

C# 和 .NET 的前世今生:从诞生到生态崛起的全景回顾

摘要: C#与.NET历经20余年发展,从微软对抗Java的封闭框架蜕变为开源跨平台的现代开发生态。早期.NET Framework专注Windows桌面和Web开发;2014年后微软战略转型,推出开源的.NET Core实现跨平台支持;2020年.NET 5开启统一进程,现已形成覆盖云原生、AI、多端的全栈技术体系。C#语法持续革新,融入函数式等现代特性。今天的.NET生态兼具高性能与开放性,成为企业级开发的重要选择。

这里只有干货:从 Modbus 通信到 MES 对接,从 YOLO 训练到工控机部署,带你搞定工业软件开发全流程。 1008
上一篇: C#9 和 .NET5 企业级应用开发(二)
下一篇: C#9 和 .NET5 企业级应用开发(四)
绝不原创的飞龙
绝不原创的飞龙 优质创作者: 人工智能、数据科学与机器学习技术领域 优质创作者: 人工智能、数据科学与机器学习技术领域
博客等级 码龄15年 4万+粉丝 2万+原创
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

当前余额3.43元 前往充值 >
需支付:10.00元
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付元
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值