C-7-和--NET-Core-2-0-蓝图-全-

C#7 和 .NET Core 2.0 蓝图(全)

原文:zh.annas-archive.org/md5/7C3D5DACD7BE632FD426A045B35F94C4

译者:飞龙

协议:CC BY-NC-SA 4.0

前言

欢迎阅读《C# 7 和.NET Core 2.0 蓝图》。通过采用蓝图方法来展示.NET Core 2.0 的强大之处,您将学习如何在创建可用的令人兴奋的应用程序时使用.NET Core 2.0。

这本书适合谁

本书旨在面向那些对 C#编程语言有很好掌握但可能需要更多了解.NET Core 的开发人员。

本书涵盖的内容

第一章,电子书管理和目录应用,介绍了 C# 7 引入的新功能,使开发人员能够编写更少的代码并提高生产力。我们将创建一个电子书管理应用程序。如果你和我一样,在硬盘和外部驱动器上都散落着电子书,这个应用程序将提供一个机制将所有这些不同的位置汇聚到一个虚拟存储空间中。该应用程序已经具备功能,但可以进一步增强以满足您的需求。

第二章,板球比分计算器和跟踪器,指出面向对象编程(OOP)是编写.NET 应用程序的关键要素。适当的 OOP 确保开发人员可以轻松地在项目之间共享代码。在本章中,我们将创建一个 ASP.NET Bootstrap Web 应用程序,用于跟踪您两支最喜欢的板球队的比分。也正是通过这个应用程序,面向对象编程的原则将变得明显。

第三章,跨平台.NET Core 系统信息管理器,介绍了.NET Core 是什么;.NET Core 允许我们创建在 Windows、macOS 和 Linux 上运行的应用程序。为了在本章中加以说明,我们将创建一个简单的信息仪表板应用程序,显示我们正在运行的计算机的信息以及该计算机位置的天气情况。

第四章,使用 MongoDB 的任务错误记录 ASP .NET Core MVC 应用程序,通过创建一个任务/错误记录应用程序,介绍了在 ASP.NET Core MVC 中使用 MongoDB。MongoDB 可以让开发人员更加高效,并且可以轻松地添加到.NET Core 中。

第五章,ASP.NET SignalR 聊天应用程序,开始让你想象具有服务器端代码实时推送数据到网页的能力,而无需用户刷新页面。ASP.NET SignalR 库为开发人员提供了一种简化的方法,以向应用程序添加实时网络功能。当阅读第八章,使用 OAuth 的 Twitter 克隆时,请记住这一点。这是一个完美的应用程序,可以集成 SignalR。

第六章,使用 Entity Framework Core 的 Web 研究工具,讨论了 Entity Framework Core,这是我们.NET Core 教育中的一个重要组成部分。开发应用程序中最令人沮丧的部分之一是尝试建立代码与数据库之间的通信层。Entity Framework Core 可以轻松解决这个问题,并且本章向您展示了如何实现。

第七章,无服务器电子邮件验证 Azure 函数,向您展示如何创建 Azure 函数以及如何从 ASP.NET Core MVC 应用程序调用该函数。Azure 函数将只验证电子邮件地址。本章介绍了无服务器计算,并在阅读本章时将清楚地了解其好处。

第八章,使用 OAuth 创建 Twitter 克隆,表达了我有时希望能够调整 Twitter 以满足自己的需求,例如保存喜爱的推文。在本章中,我们将看看使用 ASP.NET Core MVC 创建基本 Twitter 克隆有多容易。然后,您可以轻松地向应用程序添加功能,以定制满足您特定需求。

第九章,使用 Docker 和 ASP.NET Core,探讨了当今非常流行的 Docker,以及其非常重要的原因。本章说明了 Docker 如何使开发人员受益。我还将向您展示如何创建 ASP.NET Core MVC 应用程序并在 Docker 容器中运行它。在本章的最后部分,我们将看到如何使用 Docker Hub 和 GitHub 设置自动构建。

充分利用本书

假设您至少对 C# 6.0 有很好的理解。本书中的所有示例将在相关的地方使用 C# 7。

您需要安装最新补丁的 Visual Studio 2017。如果您没有 Visual Studio 2017,可以免费从www.visualstudio.com/downloads/安装 Visual Studio Community 2017。

下载示例代码文件

您可以从www.packtpub.com的帐户中下载本书的示例代码文件。如果您在其他地方购买了本书,可以访问www.packtpub.com/support并注册,以便直接通过电子邮件接收文件。

您可以按照以下步骤下载代码文件:

  1. 在www.packtpub.com登录或注册。

  2. 选择“支持”选项卡。

  3. 点击“代码下载和勘误”。

  4. 在搜索框中输入书名,然后按照屏幕上的说明操作。

下载文件后,请确保使用最新版本的解压缩软件解压缩文件夹:

  • Windows 上的 WinRAR/7-Zip

  • Mac 上的 Zipeg/iZip/UnRarX

  • Linux 上的 7-Zip/PeaZip

该书的代码包也托管在 GitHub 上,网址为github.com/PacktPublishing/CSharp7-and-.NET-Core-2.0-Blueprints。如果代码有更新,将在现有的 GitHub 存储库上进行更新。

我们还有其他代码包,来自我们丰富的图书和视频目录,可在github.com/PacktPublishing/上找到。去看看吧!

使用的约定

本书中使用了许多文本约定。

CodeInText:表示文本中的代码词、数据库表名、文件夹名、文件名、文件扩展名、路径名、虚拟 URL、用户输入和 Twitter 用户名。例如:“您可以随意命名应用程序,但我将我的称为eBookManager。”

代码块设置如下:

namespace eBookManager.Engine 
{ 
    public class DeweyDecimal 
    { 
        public string ComputerScience { get; set; } = "000"; 
        public string DataProcessing { get; set; } = "004"; 
        public string ComputerProgramming { get; set; } = "005"; 
    } 
} 

任何命令行输入或输出都是这样写的:

    mongod -dbpath D:MongoTask 

粗体:表示新术语、重要单词或屏幕上看到的单词。例如,菜单或对话框中的单词会在文本中以这种方式出现。例如:“在添加了所有存储空间和电子书之后,您将看到列出的虚拟存储空间。”

警告或重要说明会出现在这样。

提示和技巧会出现在这样。

第一章:电子书管理器和目录应用程序

C# 7 是一个很棒的版本,可在 Visual Studio 2017 中使用。它向开发人员介绍了许多强大的功能,其中一些以前只在其他语言中可用。C# 7 引入的新功能使开发人员能够编写更少的代码,提高生产力。

可用的功能有:

  • 元组

  • 模式匹配

  • Out变量

  • 解构

  • 本地函数

  • 文字改进

  • 引用返回和本地变量

  • 泛化的异步和返回类型

  • 访问器、构造函数和终结器的表达式体

  • 抛出表达式

本章将介绍其中一些功能,而本书的其余部分将在学习过程中介绍其他功能。在本章中,我们将创建一个eBookManager应用程序。如果您和我一样,在硬盘和一些外部驱动器上散落着电子书,那么这个应用程序将提供一种机制,将所有这些不同的位置汇集到一个虚拟存储空间中。该应用程序是功能性的,但可以进一步增强以满足您的需求。这样的应用程序范围是广阔的。您可以从 GitHub(github.com/PacktPublishing/CSharp7-and-.NET-Core-2.0-Blueprints)下载源代码,并跟随它,看看 C# 7 的一些新功能是如何运作的。

让我们开始吧!

设置项目

使用 Visual Studio 2017,我们将创建一个简单的 Windows 窗体应用程序模板项目。您可以随意命名应用程序,但我将其命名为eBookManager:

项目将被创建,并将如下所示:

我们的解决方案需要一个类库项目来包含驱动eBookManager应用程序的类。在解决方案中添加一个新的类库项目,并将其命名为eBookManager.Engine:

将解决方案添加到类库项目中,默认类名更改为Document:

Document类将代表一本电子书。想到一本书,我们可以有多个属性来代表一本书,但又代表所有书籍。一个例子是作者。所有书籍都必须有作者,否则它就不存在。

我知道有些人可能会认为机器也可以生成文档,但它生成的信息可能最初是由人写的。以代码注释为例。开发人员在代码中编写注释,工具从中生成文档。开发人员仍然是作者。

我添加到类中的属性仅仅是我认为可能代表一本书的解释。请随意添加其他代码,使其成为您自己的。

打开Document.cs文件,并将以下代码添加到类中:

namespace eBookManager.Engine 
{ 
    public class Document 
    { 
        public string Title { get; set; } 
        public string FileName { get; set; } 
        public string Extension { get; set; } 
        public DateTime LastAccessed { get; set; } 
        public DateTime Created { get; set; } 
        public string FilePath { get; set; } 
        public string FileSize { get; set; } 
        public string ISBN { get; set; } 
        public string Price { get; set; } 
        public string Publisher { get; set; } 
        public string Author { get; set; } 
        public DateTime PublishDate { get; set; } 
        public DeweyDecimal Classification { get; set; } 
        public string Category { get; set; } 
    } 
} 

您会注意到我包括了一个名为Classification的属性,类型为DeweyDecimal。我们还没有添加这个类,接下来会添加。

在eBookManager.Engine项目中,添加一个名为DeweyDecimal的类。如果您不想为您的电子书进行这种分类,可以不添加这个类。我包括它是为了完整起见。

您的DeweyDecimal类必须与之前添加的Document类在同一个项目中:

“杜威十进制”系统非常庞大。因此,我没有考虑到每种书籍分类。我也只假设您想要处理编程电子书。然而,实际上,您可能想要添加其他分类,如文学、科学、艺术等。这取决于您。

所以让我们创建一个代表杜威十进制系统的类:

  1. 打开DeweyDecimal类并将以下代码添加到类中:
namespace eBookManager.Engine 
{ 
    public class DeweyDecimal 
    { 
        public string ComputerScience { get; set; } = "000"; 
        public string DataProcessing { get; set; } = "004"; 
        public string ComputerProgramming { get; set; } = "005"; 
    } 
}

字母狂人可能会不同意我的观点,但我想提醒他们,我是一个代码狂人。这里表示的分类只是为了让我能够编目与编程和计算机科学相关的电子书。如前所述,您可以根据自己的需要进行更改。

  1. 我们现在需要在eBookManager.Engine解决方案的核心中添加。这是一个名为DocumentEngine的类,它将是一个包含您需要处理文档的方法的类:

您的eBookManager.Engine解决方案现在将包含以下类:

    • DeweyDecimal
  • Document

  • DocumentEngine

  1. 我们现在需要从eBookManager项目中添加对eBookManager.Engine的引用。我相信你们都知道如何做到这一点:

eBookManager.Engine项目将在引用管理器屏幕的项目部分中可用:

  1. 添加了引用后,我们需要一个负责导入新书籍的 Windows 表单。在eBookManager解决方案中添加一个名为ImportBooks的新表单:

  1. 在我们忘记之前,向ImportBooks表单添加一个ImageList控件,并将其命名为tvImages。这将包含我们想要编目的不同类型文档的图像。

ImageList是您从工具箱添加到ImportBooks表单上的控件。您可以从ImageList属性访问图像集合编辑器。

图标可以在 GitHub 上可下载的源代码的img文件夹中找到,网址为github.com/PacktPublishing/CSharp7-and-.NET-Core-2.0-Blueprints。

这里的图标适用于 PDF、MS Word 和 ePub 文件类型。它还包含文件夹图像:

  1. 现在,要在 C# 7 中使用元组,您需要添加System.ValueTuple NuGet 包。右键单击解决方案,然后选择管理解决方案的 NuGet 包...

请注意,如果您正在运行.NET Framework 4.7,则System.ValueTuple已包含在该框架版本中。因此,您将不需要从 NuGet 获取它。

  1. 搜索System.ValueTuple并将其添加到您的解决方案项目中。然后单击安装,让进程完成(您将在 Visual Studio 的输出窗口中看到进度):

我喜欢在我的项目中使用扩展方法。我通常为此目的添加一个单独的项目和/或类。在这个应用程序中,我添加了一个eBookManager.Helper类库项目:

  1. 这个帮助类也必须作为引用添加到eBookManager解决方案中:

最后,我将使用 JSON 作为我的电子书目录的简单文件存储。JSON 非常灵活,可以被各种编程语言消耗。JSON 之所以如此好用,是因为它相对轻量级,生成的输出是人类可读的:

  1. 转到解决方案的 NuGet 包管理器并搜索Newtonsoft.Json。然后将其添加到解决方案中的项目并单击安装按钮。

您现在已经设置了eBookManager应用程序所需的基本内容。接下来,我们将通过编写一些代码进一步深入应用程序的核心。

虚拟存储空间和扩展方法

让我们首先讨论虚拟存储空间背后的逻辑。这是硬盘(或硬盘)上几个物理空间的单一虚拟表示。存储空间将被视为一个特定的电子书组存储的单一区域。我使用术语存储是因为存储空间并不存在。它更多地代表了一种分组,而不是硬盘上的物理空间:

  1. 要开始创建虚拟存储空间,将一个名为StorageSpace的新类添加到eBookManager.Engine项目中。打开StorageSpace.cs文件,并向其中添加以下代码:
using System; 
using System.Collections.Generic; 

namespace eBookManager.Engine 
{ 
    [Serializable] 
    public class StorageSpace 
    { 
        public int ID { get; set; } 
        public string Name { get; set; } 
        public string Description { get; set; } 
        public List<Document> BookList { get; set; } 
    } 
} 

请注意,您需要在这里包含System.Collections.Generic命名空间,因为StorageSpace类包含一个名为BookList的属性,类型为List<Document>,它将包含该特定存储空间中的所有书籍。

现在我们需要把注意力集中在eBookManager.Helper项目中的ExtensionMethods类上。这将是一个静态类,因为扩展方法需要以静态的方式来作用于扩展方法定义的各种对象。

  1. 在eBookManager.Helper项目中添加一个新类,并修改ExtensionMethods类如下:
public static class ExtensionMethods 
{ 

} 

让我们将第一个扩展方法添加到名为ToInt()的类中。这个扩展方法的作用是获取一个string值并尝试将其解析为一个integer值。每当我需要将string转换为integer时,我都懒得输入Convert.ToInt32(stringVariable)。正因为如此,我使用了一个扩展方法。

  1. 在ExtensionMethods类中添加以下静态方法:
public static int ToInt(this string value, int defaultInteger = 0) 
{ 
    try 
    { 
        if (int.TryParse(value, out int validInteger)) 
          // Out variables 
         return validInteger; 
        else 
         return defaultInteger; 
    } 
    catch  
    { 
        return defaultInteger; 
    } 
} 

ToInt()扩展方法仅对string起作用。这是由方法签名中的this string value代码定义的,其中value是将包含您要转换为integer的string的变量名称。它还有一个名为defaultInteger的默认参数,设置为0。除非调用扩展方法的开发人员想要返回默认的整数值0,否则他们可以将不同的整数传递给这个扩展方法(例如-1)。

这也是我们发现 C# 7 的第一个特性的地方。改进了out变量。在以前的 C#版本中,我们必须对out变量执行以下操作:

int validInteger; 
if (int.TryParse(value, out validInteger)) 
{ 

} 

有一个预声明的整数变量挂在那里,如果string值解析为integer,它就会得到它的值。C# 7 简化了代码:

if (int.TryParse(value, out int validInteger)) 

C# 7 允许开发人员在作为out参数传递的地方声明一个out变量。继续讨论ExtensionMethods类的其他方法,这些方法用于提供以下逻辑:

  • 读取和写入到数据源

  • 检查存储空间是否存在

  • 将字节转换为兆字节

  • 将string转换为integer(如前所述)

ToMegabytes方法非常简单。在各个地方都不必写这个计算,将其定义在一个扩展方法中是有意义的:

public static double ToMegabytes(this long bytes) 
{ 
    return (bytes > 0) ? (bytes / 1024f) / 1024f : bytes; 
} 

我们还需要一种方法来检查特定的存储空间是否已经存在。

确保从eBookManager.Helper项目中向eBookManager.Engine添加项目引用。

这个扩展方法的作用也是返回下一个存储空间 ID 给调用代码。如果存储空间不存在,返回的 ID 将是在创建新存储空间时可以使用的下一个 ID:

public static bool StorageSpaceExists(this List<StorageSpace> space, string nameValueToCheck, out int storageSpaceId) 
{ 
    bool exists = false; 
    storageSpaceId = 0; 

    if (space.Count() != 0) 
    { 
       int count = (from r in space 
                 where r.Name.Equals(nameValueToCheck) 
                 select r).Count(); 

        if (count > 0) 
            exists = true; 

        storageSpaceId = (from r in space 
                          select r.ID).Max() + 1;                                 
    } 
    return exists; 
} 

我们还需要创建一个方法,将我们的数据转换为 JSON 后写入文件:

public static void WriteToDataStore(this List<StorageSpace> value, string storagePath, bool appendToExistingFile = false) 
{ 
    JsonSerializer json = new JsonSerializer(); 
    json.Formatting = Formatting.Indented; 
    using (StreamWriter sw = new StreamWriter(storagePath,  
     appendToExistingFile)) 
    { 
        using (JsonWriter writer = new JsonTextWriter(sw)) 
        { 
            json.Serialize(writer, value); 
        } 
    } 
} 

这个方法相当不言自明。它作用于一个List<StorageSpace>对象,并将创建 JSON 数据,覆盖在storagePath变量中定义的文件中。

最后,我们需要能够再次将数据读取到List<StorageSpace>对象中,并将其返回给调用代码:

public static List<StorageSpace> ReadFromDataStore(this List<StorageSpace> value, string storagePath) 
{ 
    JsonSerializer json = new JsonSerializer(); 
    if (!File.Exists(storagePath)) 
    { 
        var newFile = File.Create(storagePath); 
        newFile.Close(); 
    } 
    using (StreamReader sr = new StreamReader(storagePath)) 
    { 
        using (JsonReader reader = new JsonTextReader(sr)) 
        { 
            var retVal = 
             json.Deserialize<List<StorageSpace>>(reader); 
            if (retVal is null) 
                retVal = new List<StorageSpace>(); 

            return retVal; 
        } 
    } 
} 

该方法将返回一个空的List<StorageSpace>对象,并且文件中不包含任何内容。ExtensionMethods类可以包含许多您经常使用的扩展方法。这是一个很好的分离经常使用的代码的方法。

DocumentEngine 类

这个类的目的仅仅是为文档提供支持代码。在eBookManager应用程序中,我将使用一个名为GetFileProperties()的单一方法,它将(你猜对了)返回所选文件的属性。这个类也只包含这一个方法。当应用程序根据您的特定目的进行修改时,您可以修改这个类并添加特定于文档的其他方法。

DocumentEngine类向我们介绍了 C# 7 的下一个特性,称为“元组”。元组到底是做什么的?开发人员经常需要从方法中返回多个值。除了其他解决方案外,当然可以使用out参数,但这在async方法中不起作用。元组提供了更好的方法来做到这一点。

在DocumentEngine类中添加以下代码:

public (DateTime dateCreated, DateTime dateLastAccessed, string fileName, string fileExtension, long fileLength, bool error) GetFileProperties(string filePath) 
{ 
    var returnTuple = (created: DateTime.MinValue,
    lastDateAccessed: DateTime.MinValue, name: "", ext: "",
    fileSize: 0L, error: false); 

    try 
    { 
        FileInfo fi = new FileInfo(filePath); 
        fi.Refresh(); 
        returnTuple = (fi.CreationTime, fi.LastAccessTime, fi.Name, 
        fi.Extension, fi.Length, false); 
    } 
    catch 
    { 
        returnTuple.error = true; 
    } 
    return returnTuple; 
} 

GetFileProperties()方法返回一个元组,格式为(DateTime dateCreated, DateTime dateLastAccessed, string fileName, string fileExtension, long fileLength, bool error),并且允许我们轻松地检查从调用代码返回的值。

在尝试获取特定文件的属性之前,我通过以下方式初始化“元组”:

var returnTuple = (created: DateTime.MinValue, lastDateAccessed: DateTime.MinValue, name: "", ext: "", fileSize: 0L, error: false); 

如果出现异常,我可以返回默认值。使用FileInfo类读取文件属性非常简单。然后我可以通过以下方式将文件属性分配给“元组”:

returnTuple = (fi.CreationTime, fi.LastAccessTime, fi.Name, fi.Extension, fi.Length, false); 

然后将“元组”返回给调用代码,在那里将根据需要使用。接下来我们将看一下调用代码。

导入书籍表单

ImportBooks表单正如其名称所示。它允许我们创建虚拟存储空间并将书籍导入到这些空间中。表单设计如下:

!

TreeView控件以tv为前缀,按钮以btn为前缀,组合框以dl为前缀,文本框以txt为前缀,日期时间选择器以dt为前缀。当这个表单加载时,如果已经定义了任何存储空间,那么这些存储空间将列在dlVirtualStorageSpaces组合框中。单击“选择源文件夹”按钮将允许我们选择源文件夹以查找电子书。

如果存储空间不存在,我们可以通过单击btnAddNewStorageSpace按钮添加新的虚拟存储空间。这将允许我们为新的存储空间添加名称和描述,并单击btnSaveNewStorageSpace按钮。

从tvFoundBooks TreeView 中选择电子书将填充表单右侧的“文件详细信息”组控件。然后您可以添加额外的书籍详细信息,并单击btnAddeBookToStorageSpace按钮将书籍添加到我们的空间中:

  1. 您需要确保以下命名空间添加到您的ImportBooks类中:
using eBookManager.Engine; 
using System; 
using System.Collections.Generic; 
using System.IO; 
using System.Linq; 
using System.Windows.Forms; 
using static eBookManager.Helper.ExtensionMethods; 
using static System.Math; 
  1. 接下来,让我们从最合乎逻辑的地方开始,即构造函数ImportBooks()和表单变量。在构造函数上方添加以下代码:
private string _jsonPath; 
private List<StorageSpace> spaces; 
private enum StorageSpaceSelection { New = -9999, NoSelection = -1 } 

枚举器的用处将在以后的代码中变得明显。"_jsonPath"变量将包含用于存储我们的电子书信息的文件的路径。

  1. 按照以下方式修改构造函数:
public ImportBooks() 
{ 
    InitializeComponent(); 
    _jsonPath = Path.Combine(Application.StartupPath, 
    "bookData.txt"); 
    spaces = spaces.ReadFromDataStore(_jsonPath); 
} 

_jsonPath初始化为应用程序的执行文件夹,并且文件硬编码为bookData.txt。如果您想要配置这些设置,可以提供一个设置屏幕,但我决定让应用程序使用硬编码设置。

  1. 接下来,我们需要添加另一个枚举器,定义我们将能够在应用程序中保存的文件扩展名。在这里,我们将看到 C# 7 的另一个特性,称为“表达式体”属性。

表达式体访问器、构造函数和终结器

如果以下表达式看起来令人生畏,那是因为它使用了 C# 6 中引入并在 C# 7 中扩展的一个特性:

private HashSet<string> AllowedExtensions => new HashSet<string>(StringComparer.InvariantCultureIgnoreCase) { ".doc",".docx",".pdf", ".epub" }; 
private enum Extention { doc = 0, docx = 1, pdf = 2, epub = 3 } 

前面的例子返回了我们应用程序允许的文件扩展名的HashSet。这些自 C# 6 以来就存在,但在 C# 7 中已经扩展到包括访问器、构造函数和终结器。让我们简化一下这些例子。

假设我们需要修改Document类以在类内部设置字段_defaultDate;传统上,我们需要这样做:

private DateTime _defaultDate; 

public Document() 
{ 
    _defaultDate = DateTime.Now; 
} 

在 C# 7 中,我们可以通过简单地执行以下操作大大简化这段代码:

private DateTime _defaultDate; 
public Document() => _defaultDate = DateTime.Now; 

这是完全合法的,可以正确编译。同样,终结器(或解构器)也可以这样做。AllowedExtensions属性也是表达式体属性的一个很好的实现。表达式体属性实际上自 C# 6 以来就一直存在,但谁在计数呢?

假设我们只想返回 PDF 的Extension枚举的string值,我们可以这样做:

public string PDFExtension 
{ 
    get 
    { 
        return nameof(Extention.pdf); 
    } 
} 

该属性只有一个获取器,永远不会返回除Extension.pdf之外的任何内容。通过更改代码来简化:

public string PDFExtension => nameof(Extention.pdf); 

就是这样。一行代码完全可以做到与以前的七行代码相同的事情。同样,表达式体属性访问器也被简化了。考虑以下 11 行代码:

public string DefaultSavePath 
{ 
    get 
    { 
        return _jsonPath; 
    } 
    set 
    { 
        _jsonPath = value; 
    } 
} 

有了 C# 7,我们可以简化为以下内容:

public string DefaultSavePath 
{ 
    get => _jsonPath; 
    set => _jsonPath = value; 
} 

这使我们的代码更易读,更快速编写。回到我们的AllowedExtensions属性;传统上,它将被写成如下形式:

private HashSet<string> AllowedExtensions 
{ 
    get 
    { 
        return new HashSet<string> 
        (StringComparer.InvariantCultureIgnoreCase) { ".doc", 
        ".docx", ".pdf", ".epub" }; 
    } 
} 

自 C# 6 以来,我们已经能够简化这个过程,就像我们之前看到的那样。这为开发人员提供了一个减少不必要代码的好方法。

填充 TreeView 控件

当我们查看PopulateBookList()方法时,我们可以看到AllowedExtensions属性的实现。这个方法的作用只是用选定的源位置找到的文件和文件夹填充TreeView控件。考虑以下代码:

public void PopulateBookList(string paramDir, TreeNode paramNode) 
{ 
    DirectoryInfo dir = new DirectoryInfo(paramDir); 
    foreach (DirectoryInfo dirInfo in dir.GetDirectories()) 
    { 
        TreeNode node = new TreeNode(dirInfo.Name); 
        node.ImageIndex = 4; 
        node.SelectedImageIndex = 5; 

        if (paramNode != null) 
            paramNode.Nodes.Add(node); 
        else 
            tvFoundBooks.Nodes.Add(node); 
        PopulateBookList(dirInfo.FullName, node); 
    } 
    foreach (FileInfo fleInfo in dir.GetFiles().Where
    (x => AllowedExtensions.Contains(x.Extension)).ToList()) 
    { 
        TreeNode node = new TreeNode(fleInfo.Name); 
        node.Tag = fleInfo.FullName; 
        int iconIndex = Enum.Parse(typeof(Extention), 
         fleInfo.Extension.TrimStart('.'), true).GetHashCode(); 

        node.ImageIndex = iconIndex; 
        node.SelectedImageIndex = iconIndex; 
        if (paramNode != null) 
            paramNode.Nodes.Add(node); 
        else 
            tvFoundBooks.Nodes.Add(node); 
    } 
} 

我们需要调用这个方法的第一个地方显然是在方法内部,因为这是一个递归方法。我们需要调用它的第二个地方是在btnSelectSourceFolder按钮的单击事件中:

private void btnSelectSourceFolder_Click(object sender, EventArgs e) 
{ 
    try 
    { 
        FolderBrowserDialog fbd = new FolderBrowserDialog(); 
        fbd.Description = "Select the location of your eBooks and 
        documents"; 

        DialogResult dlgResult = fbd.ShowDialog(); 
        if (dlgResult == DialogResult.OK) 
        { 
            tvFoundBooks.Nodes.Clear(); 
            tvFoundBooks.ImageList = tvImages; 

            string path = fbd.SelectedPath; 
            DirectoryInfo di = new DirectoryInfo(path); 
            TreeNode root = new TreeNode(di.Name); 
            root.ImageIndex = 4; 
            root.SelectedImageIndex = 5; 
            tvFoundBooks.Nodes.Add(root); 
            PopulateBookList(di.FullName, root); 
            tvFoundBooks.Sort(); 

            root.Expand(); 
        } 
    } 
    catch (Exception ex) 
    { 
        MessageBox.Show(ex.Message); 
    } 
} 

这都是非常简单直接的代码。选择要递归的文件夹,并使用我们的AllowedExtensions属性中包含的文件扩展名匹配找到的所有文件,然后填充TreeView控件。

当有人在tvFoundBooks TreeView控件中选择一本书时,我们还需要查看代码。当选择一本书时,我们需要读取所选文件的属性,并将这些属性返回到文件详细信息部分:

private void tvFoundBooks_AfterSelect(object sender, TreeViewEventArgs e) 
{ 
    DocumentEngine engine = new DocumentEngine(); 
    string path = e.Node.Tag?.ToString() ?? ""; 

    if (File.Exists(path)) 
    { 
        var (dateCreated, dateLastAccessed, fileName, 
        fileExtention, fileLength, hasError) = 
        engine.GetFileProperties(e.Node.Tag.ToString()); 

        if (!hasError) 
        { 
            txtFileName.Text = fileName; 
            txtExtension.Text = fileExtention; 
            dtCreated.Value = dateCreated; 
            dtLastAccessed.Value = dateLastAccessed; 
            txtFilePath.Text = e.Node.Tag.ToString(); 
            txtFileSize.Text = $"{Round(fileLength.ToMegabytes(),
            2).ToString()} MB"; 
        } 
    } 
} 

您会注意到这里我们在DocumentEngine类上调用GetFileProperties()方法,该方法返回元组。

本地函数

这是 C# 7 中的一个功能,我真的很惊讶我会在哪里找到它的用途。事实证明,本地函数确实非常有用。有些人称之为嵌套函数,这些函数嵌套在另一个父函数中。显然,它只在父函数内部范围内有效,并提供了一种有用的方式来调用代码,否则在父函数外部没有任何真正的用途。考虑PopulateStorageSpacesList()方法:

private void PopulateStorageSpacesList() 
{ 
    List<KeyValuePair<int, string>> lstSpaces = 
    new List<KeyValuePair<int, string>>(); 
    BindStorageSpaceList((int)StorageSpaceSelection.NoSelection, 
    "Select Storage Space"); 

    void BindStorageSpaceList(int key, string value)
    // Local function 
    { 
        lstSpaces.Add(new KeyValuePair<int, string>(key, value)); 
    } 

    if (spaces is null || spaces.Count() == 0) // Pattern matching 
    { 
        BindStorageSpaceList((int)StorageSpaceSelection.New, "
        <create new>"); 
    } 
    else 
    { 
        foreach (var space in spaces) 
        { 
            BindStorageSpaceList(space.ID, space.Name); 
        } 
    } 

    dlVirtualStorageSpaces.DataSource = new 
    BindingSource(lstSpaces, null); 
    dlVirtualStorageSpaces.DisplayMember = "Value"; 
    dlVirtualStorageSpaces.ValueMember = "Key"; 
} 

要查看PopulateStorageSpacesList()如何调用本地函数BindStorageSpaceList(),请查看以下屏幕截图:

您会注意到本地函数可以在父函数内的任何地方调用。在这种情况下,BindStorageSpaceList()本地函数不返回任何内容,但您可以从本地函数返回任何您喜欢的内容。您也可以这样做:

private void SomeMethod() 
{ 
    int currentYear = GetCurrentYear(); 

    int GetCurrentYear(int iAddYears = 0) 
    { 
        return DateTime.Now.Year + iAddYears; 
    } 

    int nextYear = GetCurrentYear(1); 
} 

本地函数可以从父函数的任何地方访问。

模式匹配

继续使用PopulateStorageSpacesList()方法,我们可以看到另一个 C# 7 功能的使用,称为模式匹配。spaces is null代码行可能是最简单的模式匹配形式。实际上,模式匹配支持多种模式。

考虑一个switch语句:

switch (objObject) 
{ 
    case null: 
        WriteLine("null"); // Constant pattern 
        break; 

    case Document doc when doc.Author.Equals("Stephen King"): 
        WriteLine("Stephen King is the author"); 
        break; 

    case Document doc when doc.Author.StartsWith("Stephen"): 
        WriteLine("Stephen is the author"); 
        break; 

    default: 
        break; 
} 

模式匹配允许开发人员使用is表达式来查看某物是否与特定模式匹配。请记住,模式需要检查最具体到最一般的模式。如果您只是以case Document doc:开始,那么传递给switch语句的类型为Document的所有对象都会匹配。您永远不会找到作者是Stephen King或以Stephen开头的特定文档。

对于从 C 语言继承的构造,自 70 年代以来它并没有改变太多。C# 7 通过模式匹配改变了这一切。

完成 ImportBooks 代码

让我们来看看ImportBooks表单中的其余代码。如果之前已保存了任何现有存储空间,表单加载将只填充存储空间列表:

private void ImportBooks_Load(object sender, EventArgs e) 
{ 
    PopulateStorageSpacesList(); 

    if (dlVirtualStorageSpaces.Items.Count == 0) 
    { 
        dlVirtualStorageSpaces.Items.Add("<create new storage 
        space>"); 
    } 

    lblEbookCount.Text = ""; 
} 

现在我们需要添加更改所选存储空间的逻辑。dlVirtualStorageSpaces控件的SelectedIndexChanged()事件修改如下:

private void dlVirtualStorageSpaces_SelectedIndexChanged(object sender, EventArgs e) 
{ 
    int selectedValue = 
    dlVirtualStorageSpaces.SelectedValue.ToString().ToInt(); 

    if (selectedValue == (int)StorageSpaceSelection.New) // -9999 
    { 
        txtNewStorageSpaceName.Visible = true; 
        lblStorageSpaceDescription.Visible = true; 
        txtStorageSpaceDescription.ReadOnly = false; 
        btnSaveNewStorageSpace.Visible = true; 
        btnCancelNewStorageSpaceSave.Visible = true; 
        dlVirtualStorageSpaces.Enabled = false; 
        btnAddNewStorageSpace.Enabled = false; 
        lblEbookCount.Text = ""; 
    } 
    else if (selectedValue != 
    (int)StorageSpaceSelection.NoSelection) 
    { 
        // Find the contents of the selected storage space 
        int contentCount = (from c in spaces 
                            where c.ID == selectedValue 
                            select c).Count(); 
        if (contentCount > 0) 
        { 
            StorageSpace selectedSpace = (from c in spaces 
                                          where c.ID == 
                                          selectedValue 
                                          select c).First(); 

            txtStorageSpaceDescription.Text = 
            selectedSpace.Description; 

            List<Document> eBooks = (selectedSpace.BookList == 
            null) 
             ? new List<Document> { } : selectedSpace.BookList; 
            lblEbookCount.Text = $"Storage Space contains 
             {eBooks.Count()} {(eBooks.Count() == 1 ? "eBook" :
             "eBooks")}"; 
        } 
    } 
    else 
    { 
        lblEbookCount.Text = ""; 
    } 
} 

我不会在这里对代码进行任何详细的解释,因为它相对明显它在做什么。

抛出表达式

我们还需要添加保存新存储空间的代码。将以下代码添加到btnSaveNewStorageSpace按钮的Click事件中:

private void btnSaveNewStorageSpace_Click(object sender,
  EventArgs e) 
  { 
    try 
    { 
        if (txtNewStorageSpaceName.Text.Length != 0) 
        { 
            string newName = txtNewStorageSpaceName.Text; 

            // throw expressions: bool spaceExists = 
           (space exists = false) ? return false : throw exception                     
            // Out variables 
            bool spaceExists = (!spaces.StorageSpaceExists
            (newName, out int nextID)) ? false : throw new 
            Exception("The storage space you are 
             trying to add already exists."); 

            if (!spaceExists) 
            { 
                StorageSpace newSpace = new StorageSpace(); 
                newSpace.Name = newName; 
                newSpace.ID = nextID; 
                newSpace.Description = 
                txtStorageSpaceDescription.Text; 
                spaces.Add(newSpace); 
                PopulateStorageSpacesList(); 
                // Save new Storage Space Name 
                txtNewStorageSpaceName.Clear(); 
                txtNewStorageSpaceName.Visible = false; 
                lblStorageSpaceDescription.Visible = false; 
                txtStorageSpaceDescription.ReadOnly = true; 
                txtStorageSpaceDescription.Clear(); 
                btnSaveNewStorageSpace.Visible = false; 
                btnCancelNewStorageSpaceSave.Visible = false; 
                dlVirtualStorageSpaces.Enabled = true; 
                btnAddNewStorageSpace.Enabled = true; 
            } 
        } 
    } 
    catch (Exception ex) 
    { 
        txtNewStorageSpaceName.SelectAll(); 
        MessageBox.Show(ex.Message); 
    } 
} 

在这里,我们可以看到 C# 7 语言中的另一个新功能,称为throw 表达式。这使开发人员能够从表达式中抛出异常。相关代码如下:

bool spaceExists = (!spaces.StorageSpaceExists(newName, out int nextID)) ? false : throw new Exception("The storage space you are trying to add already exists."); 

我总是喜欢记住代码的结构如下:

最后几个方法处理将电子书保存在所选虚拟存储空间中。修改btnAddBookToStorageSpace按钮的Click事件。此代码还包含一个 throw 表达式。如果您没有从组合框中选择存储空间,则会抛出新异常:

private void btnAddeBookToStorageSpace_Click(object sender, EventArgs e) 
{ 
    try 
    { 
        int selectedStorageSpaceID = 
         dlVirtualStorageSpaces.SelectedValue.ToString().ToInt(); 
        if ((selectedStorageSpaceID !=   
         (int)StorageSpaceSelection.NoSelection) 
        && (selectedStorageSpaceID !=
          (int)StorageSpaceSelection.New)) 
        { 
            UpdateStorageSpaceBooks(selectedStorageSpaceID); 
        } 
        else throw new Exception("Please select a Storage 
       Space to add your eBook to"); // throw expressions 
    } 
    catch (Exception ex) 
    { 
        MessageBox.Show(ex.Message); 
    } 
} 

开发人员现在可以立即在代码中抛出异常。这相当不错,使代码更清晰。

将所选书籍保存到存储空间

以下代码基本上更新了所选存储空间中的书籍列表(在与用户确认后)如果它已经包含特定书籍。否则,它将将书籍添加到书籍列表作为新书:

private void UpdateStorageSpaceBooks(int storageSpaceId) 
{ 
    try 
    { 
        int iCount = (from s in spaces 
                      where s.ID == storageSpaceId 
                      select s).Count(); 
        if (iCount > 0) // The space will always exist 
        { 
            // Update 
            StorageSpace existingSpace = (from s in spaces 
              where s.ID == storageSpaceId select s).First(); 

            List<Document> ebooks = existingSpace.BookList; 

            int iBooksExist = (ebooks != null) ? (from b in ebooks 
              where $"{b.FileName}".Equals($"
               {txtFileName.Text.Trim()}") 
                 select b).Count() : 0; 

            if (iBooksExist > 0) 
            { 
                // Update existing book 
                DialogResult dlgResult = MessageBox.Show($"A book 
                with the same name has been found in Storage Space 
                {existingSpace.Name}. 
                Do you want to replace the existing book
                entry with this one?", 
                "Duplicate Title", MessageBoxButtons.YesNo,
                 MessageBoxIcon.Warning,
                 MessageBoxDefaultButton.Button2); 
                if (dlgResult == DialogResult.Yes) 
                { 
                    Document existingBook = (from b in ebooks 
                      where $"
                      {b.FileName}".Equals($"
                      {txtFileName.Text.Trim()}") 
                       select b).First(); 

                    existingBook.FileName = txtFileName.Text; 
                    existingBook.Extension = txtExtension.Text; 
                    existingBook.LastAccessed = 
                    dtLastAccessed.Value; 
                    existingBook.Created = dtCreated.Value; 
                    existingBook.FilePath = txtFilePath.Text; 
                    existingBook.FileSize = txtFileSize.Text; 
                    existingBook.Title = txtTitle.Text; 
                    existingBook.Author = txtAuthor.Text; 
                    existingBook.Publisher = txtPublisher.Text; 
                    existingBook.Price = txtPrice.Text; 
                    existingBook.ISBN = txtISBN.Text; 
                    existingBook.PublishDate = 
                    dtDatePublished.Value; 
                    existingBook.Category = txtCategory.Text; 
               } 
            } 
            else 
            { 
                // Insert new book 
                Document newBook = new Document(); 
                newBook.FileName = txtFileName.Text; 
                newBook.Extension = txtExtension.Text; 
                newBook.LastAccessed = dtLastAccessed.Value; 
                newBook.Created = dtCreated.Value; 
                newBook.FilePath = txtFilePath.Text; 
                newBook.FileSize = txtFileSize.Text; 
                newBook.Title = txtTitle.Text; 
                newBook.Author = txtAuthor.Text; 
                newBook.Publisher = txtPublisher.Text; 
                newBook.Price = txtPrice.Text; 
                newBook.ISBN = txtISBN.Text; 
                newBook.PublishDate = dtDatePublished.Value; 
                newBook.Category = txtCategory.Text; 

                if (ebooks == null) 
                    ebooks = new List<Document>(); 
                ebooks.Add(newBook); 
                existingSpace.BookList = ebooks; 
            } 
        } 
        spaces.WriteToDataStore(_jsonPath); 
        PopulateStorageSpacesList(); 
        MessageBox.Show("Book added"); 
    } 
    catch (Exception ex) 
    { 
        MessageBox.Show(ex.Message); 
    } 
} 

最后,作为一种整理的方式,ImportBooks表单包含以下代码,用于根据btnCancelNewStorageSpace和btnAddNewStorageSpace按钮的单击事件显示和启用控件:

private void btnCancelNewStorageSpaceSave_Click(object sender, EventArgs e) 
{ 
    txtNewStorageSpaceName.Clear(); 
    txtNewStorageSpaceName.Visible = false; 
    lblStorageSpaceDescription.Visible = false; 
    txtStorageSpaceDescription.ReadOnly = true; 
    txtStorageSpaceDescription.Clear(); 
    btnSaveNewStorageSpace.Visible = false; 
    btnCancelNewStorageSpaceSave.Visible = false; 
    dlVirtualStorageSpaces.Enabled = true; 
    btnAddNewStorageSpace.Enabled = true; 
} 

private void btnAddNewStorageSpace_Click(object sender, EventArgs e) 
{ 
    txtNewStorageSpaceName.Visible = true; 
    lblStorageSpaceDescription.Visible = true; 
    txtStorageSpaceDescription.ReadOnly = false; 
    btnSaveNewStorageSpace.Visible = true; 
    btnCancelNewStorageSpaceSave.Visible = true; 
    dlVirtualStorageSpaces.Enabled = false; 
    btnAddNewStorageSpace.Enabled = false; 
} 

现在我们只需要完成Form1.cs表单中的代码,这是启动表单。

主 eBookManager 表单

首先将Form1.cs重命名为eBookManager.cs。这是应用程序的启动表单,它将列出之前保存的所有现有存储空间:

设计您的eBookManager表单如下:

  • 用于现有存储空间的ListView控件

  • 用于所选存储空间中包含的电子书的ListView

  • 打开电子书文件位置的按钮

  • 菜单控件以导航到ImportBooks.cs表单

  • 各种只读字段用于显示所选电子书信息

当您添加了控件后,您的 eBook Manager 表单将如下所示:

查看我们之前使用的代码,您需要确保导入以下using语句:

using eBookManager.Engine; 
using eBookManager.Helper; 
using System; 
using System.Collections.Generic; 
using System.IO; 
using System.Windows.Forms; 
using System.Linq; 
using System.Diagnostics; 

构造函数与ImportBooks.cs表单的构造函数非常相似。它读取任何可用的存储空间,并使用先前保存的存储空间填充存储空间列表视图控件:

private string _jsonPath; 
private List<StorageSpace> spaces; 

public eBookManager() 
{ 
    InitializeComponent(); 
    _jsonPath = Path.Combine(Application.StartupPath, 
    "bookData.txt"); 
    spaces = spaces.ReadFromDataStore(_jsonPath); 
} 

private void Form1_Load(object sender, EventArgs e) 
{             
    PopulateStorageSpaceList(); 
} 

private void PopulateStorageSpaceList() 
{ 
    lstStorageSpaces.Clear(); 
    if (!(spaces == null)) 
    { 
        foreach (StorageSpace space in spaces) 
        { 
            ListViewItem lvItem = new ListViewItem(space.Name, 0); 
            lvItem.Tag = space.BookList; 
            lvItem.Name = space.ID.ToString(); 
            lstStorageSpaces.Items.Add(lvItem); 
        } 
    } 
} 

如果用户点击了一个存储空间,我们需要能够读取该选定空间中包含的书籍:

private void lstStorageSpaces_MouseClick(object sender, MouseEventArgs e) 
{ 
    ListViewItem selectedStorageSpace = 
    lstStorageSpaces.SelectedItems[0]; 
    int spaceID = selectedStorageSpace.Name.ToInt(); 

    txtStorageSpaceDescription.Text = (from d in spaces 
                                       where d.ID == spaceID 
                                       select 
                                       d.Description).First(); 

    List<Document> ebookList = 
     (List<Document>)selectedStorageSpace.Tag; 
     PopulateContainedEbooks(ebookList); 
}

现在我们需要创建一个方法,该方法将使用所选存储空间中包含的书籍填充lstBooks列表视图:

private void PopulateContainedEbooks(List<Document> ebookList) 
{ 
    lstBooks.Clear(); 
    ClearSelectedBook(); 

    if (ebookList != null) 
    { 
        foreach (Document eBook in ebookList) 
        { 
            ListViewItem book = new ListViewItem(eBook.Title, 1); 
            book.Tag = eBook; 
            lstBooks.Items.Add(book); 
        } 
    } 
    else 
    { 
        ListViewItem book = new ListViewItem("This storage space 
        contains no eBooks", 2); 
        book.Tag = ""; 
        lstBooks.Items.Add(book); 
    } 
} 

你会注意到每个ListViewItem都填充了电子书的标题和我添加到表单的ImageList控件中的图像的索引。要在 GitHub 存储库中找到这些图像,请浏览以下路径:

github.com/PacktPublishing/CSharp7-and-.NET-Core-2.0-Blueprints/tree/master/eBookManager/eBookManager/img

查看图像集编辑器,你会看到我已经添加了它们如下:

当所选存储空间更改时,我们还需要清除所选书籍的详细信息。我在文件和书籍详细信息周围创建了两个组控件。这段代码只是循环遍历所有子控件,如果子控件是文本框,则清除它。

private void ClearSelectedBook() 
{ 
    foreach (Control ctrl in gbBookDetails.Controls) 
    { 
        if (ctrl is TextBox) 
            ctrl.Text = ""; 
    } 

    foreach (Control ctrl in gbFileDetails.Controls) 
    { 
        if (ctrl is TextBox) 
            ctrl.Text = ""; 
    } 

    dtLastAccessed.Value = DateTime.Now; 
    dtCreated.Value = DateTime.Now; 
    dtDatePublished.Value = DateTime.Now; 
} 

添加到表单的 MenuStrip 上有一个点击事件,点击ImportEBooks菜单项。它只是打开ImportBooks表单:

private void mnuImportEbooks_Click(object sender, EventArgs e) 
{ 
    ImportBooks import = new ImportBooks(); 
    import.ShowDialog(); 
    spaces = spaces.ReadFromDataStore(_jsonPath); 
    PopulateStorageSpaceList(); 
} 

以下方法总结了选择特定电子书并在eBookManager表单上填充文件和电子书详细信息的逻辑:

private void lstBooks_MouseClick(object sender, MouseEventArgs e) 
{ 
    ListViewItem selectedBook = lstBooks.SelectedItems[0]; 
    if (!String.IsNullOrEmpty(selectedBook.Tag.ToString())) 
    { 
        Document ebook = (Document)selectedBook.Tag; 
        txtFileName.Text = ebook.FileName; 
        txtExtension.Text = ebook.Extension; 
        dtLastAccessed.Value = ebook.LastAccessed; 
        dtCreated.Value = ebook.Created; 
        txtFilePath.Text = ebook.FilePath; 
        txtFileSize.Text = ebook.FileSize; 
        txtTitle.Text = ebook.Title; 
        txtAuthor.Text = ebook.Author; 
        txtPublisher.Text = ebook.Publisher; 
        txtPrice.Text = ebook.Price; 
        txtISBN.Text = ebook.ISBN; 
        dtDatePublished.Value = ebook.PublishDate; 
        txtCategory.Text = ebook.Category; 
    } 
} 

最后,当所选的书是您想要阅读的书时,请点击“阅读电子书”按钮以打开所选电子书的文件位置:

private void btnReadEbook_Click(object sender, EventArgs e) 
{ 
    string filePath = txtFilePath.Text; 
    FileInfo fi = new FileInfo(filePath); 
    if (fi.Exists) 
    { 
        Process.Start(Path.GetDirectoryName(filePath)); 
    } 
} 

这完成了eBookManager应用程序中包含的代码逻辑。

您可以进一步修改代码,以打开所选电子书所需的应用程序,而不仅仅是文件位置。换句话说,如果您点击 PDF 文档,应用程序可以启动加载了文档的 PDF 阅读器。最后,请注意,此版本的应用程序中尚未实现分类。

是时候启动应用程序并测试一下了。

运行 eBookManager 应用程序

当应用程序第一次启动时,将没有可用的虚拟存储空间。要创建一个,我们需要导入一些书籍。点击“导入电子书”菜单项:

打开导入电子书屏幕,您可以添加新的存储空间并选择电子书的源文件夹:

一旦你选择了一本电子书,添加有关该书的适用信息并将其保存到存储空间:

添加了所有存储空间和电子书后,您将看到列出的虚拟存储空间。当您点击一个存储空间时,它包含的书籍将被列出:

选择一本电子书并点击“阅读电子书”按钮将打开包含所选电子书的文件位置:

最后,让我们看一下为eBook Manager应用程序生成的JSON文件:

正如你所看到的,JSON 文件排列得很好,很容易阅读。

摘要

C# 7 是语言的一个很棒的版本。在本章中,我们看了out变量。您会记得,使用 C# 7,我们现在可以在作为 out 参数传递的地方声明变量。然后,我们看了元组,它提供了一种优雅的方式从方法中返回多个值。

接下来,我们看了“表达式体”属性,这是一种更简洁的编写代码的方式。然后,我们讨论了本地函数(我最喜欢的功能之一)及其在另一个函数中创建辅助函数的能力。如果使用本地函数的函数是唯一使用它的代码,这是有道理的。

接下来是模式匹配,它是一种语法元素,用于查看特定值是否具有特定的“形状”。这使得使用switch语句(例如)更加方便。最后,我们看了抛出表达式。这使得我们可以将异常抛出到我们的expression-bodied成员、条件和空值合并表达式中。

随着您继续使用 C# 7,您将发现更多使用这些新功能的机会。起初(至少对我来说),我不得不刻意训练自己使用新功能来编写代码(out 变量就是一个完美的例子)。

过了一会儿,这样做的便利性就变得很自然。您很快就会开始自动使用可用的新功能来编写代码。

第二章:板球比分计算器和跟踪器

面向对象编程(OOP)是编写.NET 应用程序的关键要素。正确的面向对象编程确保开发人员可以在项目之间轻松共享代码。你不必重写已经编写过的代码。这就是继承。

多年来关于面向对象编程的话题已经写了很多。事实上,在互联网上搜索面向对象编程的好处将返回无数的结果。然而,面向对象编程的基本好处是编写代码的模块化方法,代码共享的便利性以及扩展共享代码的功能。

这些小构建块(或类)是自包含的代码单元,每个都执行一个功能。开发人员在使用它时不需要知道类内部发生了什么。他们可以假设类将自行运行并始终工作。如果他们实现的类没有提供特定功能,开发人员可以自由扩展类的功能。

我们将看一下定义面向对象编程的特性,它们是:

  • 继承

  • 抽象

  • 封装

  • 多态

我们还将看一下:

  • 单一职责

  • 开闭原则

在本章中,我们将玩得开心。我们将创建一个 ASP.NET Bootstrap Web 应用程序,用于跟踪你两个最喜欢的球队的板球比分。正是通过这个应用程序,面向对象编程的原则将变得明显。

板球比分跟踪器应用程序可以在 GitHub 上找到,我鼓励你下载源代码并将其作为你自己的应用程序。GitHub 存储库的 URL 是-github.com/PacktPublishing/CSharp7-and-.NET-Core-2.0-Blueprints/tree/master/cricketScoreTrack。

在这样的应用程序中,一个人可以构建很多功能,但是关于面向对象编程的话题在本书中只有一个章节来传达这个话题。因此,重点是面向对象编程(而不是板球的硬性规则),并且对某些功能进行了一些自由处理。

让游戏开始!

设置项目

使用 Visual Studio 2017,我们将创建一个 ASP.NET Web 应用程序项目。你可以给应用程序起任何你喜欢的名字,但我把我的叫做cricketScoreTrack。当你点击新的 ASP.NET Web 应用程序模板时,你将看到一些 ASP.NET 模板。

ASP.NET 模板有:

  • 空

  • Web Forms

  • MVC

  • Web API

  • 单页应用程序

  • Azure API 应用

  • Azure 移动应用程序

我们只会选择 Web Forms 模板。对于这个应用程序,我们不需要身份验证,所以不要更改这个设置:

我假设你也已经从 GitHub 下载了本章的应用程序,因为在讨论架构时你会需要它。URL 是-github.com/PacktPublishing/CSharp7-and-.NET-Core-2.0-Blueprints/tree/master/cricketScoreTrack。

点击确定创建 Web 应用程序。项目将被创建,并将如下所示:

为了让你了解我们正在构建的东西,UI 将如下所示:

各个部分如下:

  • 击球手选择(1在上面的截图中)

  • 投手选择(2在上面的截图中)

  • 击球手比赛统计-得分、球数、4 分、6 分、打击率(3在上面的截图中)

  • 投手比赛统计-投掷数、无得分局数、得分、击球、经济(4在上面的截图中)

  • 击球手得分(5在上面的截图中)

  • 游戏动作(6在上面的截图中)

  • 比赛得分和球队(7在上面的截图中)

  • 当前击球手详情(8在上面的截图中)

  • 每球和每局的得分(9在上面的截图中)

正如你所看到的,这里有很多事情。显然还有很多地方可以继续扩展。另一个有趣的想法是添加一个游戏统计面板,甚至是 Duckworth-Lewis 计算,如果你有时间去尝试实现的话。我说尝试,因为实际的计算算法是一个秘密。

然而,在网上有很多实现,我特别感兴趣的是 Sarvashrestha Paliwal 的文章,他是微软印度的 Azure 业务负责人。他们使用机器学习来分析历史板球比赛,从而提供不断改进的 Duckworth-Lewis 计算。

你可以在以下链接阅读他的文章-azure.microsoft.com/en-us/blog/improving-the-d-l-method-using-machine-learning/。

让我们更仔细地看一下应用程序结构。展开Scripts文件夹,你会注意到应用程序使用了 jQuery 和 Bootstrap:

展开Content文件夹,你会看到正在使用的 CSS 文件:

请注意,这个文件夹中有一个我添加的custom.css文件:

.score { 
    font-size: 40px; 
} 
.team { 
    font-size: 30px; 
} 
.player { 
    font-size: 16.5px; 
} 
.info { 
    font-size: 18px; 
} 
.btn-round-xs { 
    border-radius: 11px; 
    padding-left: 10px; 
    padding-right: 10px; 
    width: 100%; 
} 
.btn-round { 
    border-radius: 17px; 
} 
.navbar-top-links { 
    margin-right: 0; 
} 
.nav { 
    padding-left: 0; 
    margin-bottom: 0; 
    list-style: none; 
} 

这个 CSS 文件基本上是为表单上的按钮和一些其他文本字体设置样式。这个 CSS 并不复杂。Bootstrap、jQuery、JavaScript 和 CSS 文件的原因是为了在网页上启用 Bootstrap 功能。

为了看到 Bootstrap 的效果,我们将使用 Chrome 来运行 Web 应用程序。

本书使用的 Chrome 版本是 Version 60.0.3112.90 (Official Build) (64-bit)。

通过在菜单上点击 Debug 并点击 Start Without Debugging 或按Ctrl + F5来运行板球比分跟踪器 Bootstrap Web 应用程序。当 Web 应用程序在 Chrome 中加载后,按Ctrl + Shift + I打开开发者工具:

在屏幕左上角,点击切换设备工具栏按钮或按Ctrl + Shift + M。

Chrome 然后会将应用程序呈现为在移动设备上看到的样子。从工具栏到顶部,你会看到应用程序已经呈现为在 iPhone 6 Plus 上的样子:

点击设备类型,你可以改变你想要呈现页面的设备。将其改为 iPad Pro 会相应地呈现页面。你也可以模拟设备的旋转:

这个功能非常强大,允许现代 Web 开发人员测试他们的 Web 应用程序的响应性。如果在为特定设备呈现应用程序后,发现有些地方看起来不太对劲,你需要去调查你哪里出错了。

在撰写本文时,支持的设备有:

  • BlackBerry Z30 和 PlayBook

  • Galaxy Note 3,Note II,S3 和 S5

  • Kindle Fire HDX

  • LG Optimus L70

  • 带有 HiDPI 屏幕和 MDPI 屏幕的笔记本电脑

  • 带触摸的笔记本电脑

  • Microsoft Lumina 550 和 950

  • Nexus 7, 6, 5, 4, 10, 5X 和 6P

  • Nokia Lumina 520

  • Nokia N9

  • iPad Mini

  • iPhone 4, 5, 6 和 6 Plus

  • iPad 和 iPad Pro

要添加设备,转到设备菜单底部。在分隔符之后,有一个 Edit...菜单项。点击它将带你到模拟设备屏幕。

查看模拟设备屏幕,你会注意到表单右侧有额外的设置:

对于开发人员来说,一个突出的设置应该是 Throttling 设置:

正如名字所示,Throttling 允许你测试你的应用程序,就好像它在一个较慢的连接上运行一样。然后你可以测试功能,并确保你的 Web 应用程序尽可能地优化,以确保它在较慢的连接上能够良好运行。

回到 Visual Studio 2017 中的解决方案资源管理器,看看名为BaseClasses、Classes和Interfaces的文件夹:

这些文件夹包含了整个章节的精髓。在这里,我们将看到面向对象编程的本质以及面向对象编程如何提供更好的方法来在代码中建模现实世界的场景(板球比赛)。

面向对象编程

正如前面简要提到的,面向对象编程提供了一种模块化的方法来编写自包含的代码单元。面向对象编程的概念围绕着我们所说的面向对象编程的四大支柱。

它们如下:

  • 抽象

  • 多态性

  • 继承

  • 封装

顺序并不重要,但我总是按照这个顺序写四大支柱,因为我使用A PIE这个记忆法来记住每一个。让我们更详细地讨论每个概念。

抽象

抽象描述了某件事应该做什么,而不实际展示如何做。根据微软文档:

“抽象是描述合同但不提供合同完整实现的类型。”

作为抽象的示例包括抽象类和接口。.NET Framework 中的抽象示例包括Stream、IEnumerable<T>和Object。如果抽象主题现在看起来有点模糊,不要担心。我将在封装和封装与抽象之间的区别部分中更详细地讨论。

多态性

你可能听说过多态性被称为面向对象编程的第三支柱。但如果我按照上面的顺序写,我的记忆法就不再起作用了!

多态性是一个希腊词,指的是具有许多形状或形式的东西。我们将在稍后的板球比分跟踪应用中看到这一点的例子。只需记住它有两个明显的方面:

  • 在运行时,从基类派生的类可以被视为继承的类的对象。这在参数、集合和数组中都可以看到。

  • 基类可以定义派生类将覆盖的虚拟方法。派生类然后提供它们自己对被覆盖方法的实现。

多态性是面向对象编程中非常强大的特性。

编译时多态性与运行时多态性

在我们继续之前,让我停顿一分钟,解释一下前面两个关于多态性的要点。

当我们说编译时多态时,我们是说我们将声明具有相同名称但不同签名的方法。因此,相同的方法可以根据接收到的签名(参数)执行不同的功能。这也被称为早期绑定、重载或静态绑定。

当我们说运行时多态时,我们是说我们将声明具有相同名称和相同签名的方法。例如,在基类中,该方法被派生类中的方法覆盖。这是通过我们所谓的继承和使用virtual或override关键字实现的。运行时多态也被称为延迟绑定、覆盖或动态绑定。

继承

能够创建自己的类,重用、扩展和修改基类定义的行为的能力被称为继承。另一个重要的方面是理解派生类只能直接继承单个基类。

这是否意味着你只能继承单个基类中定义的行为?是的,也不是。继承是具有传递性的。

为了解释这一点,想象一下你有三个类:

  • Person

  • Pedestrian

  • Driver

Person类是基类。Pedestrian继承自Person类,因此Pedestrian继承了Person类中声明的成员。Driver类继承自Pedestrian类,因此Driver继承了Pedestrian和Person中声明的成员:

这就是我们所说的继承是传递的意思。您只能从一个类继承,但您会得到从您继承的类本身继承的所有成员。 换句话说,Driver类只能从一个基类继承(在前面的图像中,Pedestrian类)。这意味着因为Pedestrian类继承自Person类,而Driver类继承自Pedestrian类,所以Driver类也继承了Person类中的成员。

封装

简而言之,这意味着类的内部工作(实现细节)不一定与外部代码共享。请记住,我们之前提到过类是您只想要使用并期望它能够工作的东西。类向调用代码公开它需要的内容,但它对实现的内部工作保持严格控制。

因此,您可以通过将变量、属性和方法作用域设置为private来隐藏它们。这样,您可以保护类内部包含的数据免受意外损坏。

封装与抽象

让我们再次停下来看看这个概念,因为它会让开发人员感到困惑(而且有点令人困惑,所以例子会帮助很多)。问题的一部分源于定义:

  • 抽象:只显示必要的内容

  • 封装:隐藏复杂性

如果我们必须考虑一个基本的类来加密一些文本,我们需要花一点时间来决定这个类必须做什么。我想象这个类需要:

  • 为文本获取一个字符串值

  • 有一种方法可以加密文本

因此,让我们编写代码:

public class EncryptionHelper
{
  public string TextToEncrypt = "";
  public void Encrypt()
  {
  }
}

我也知道,如果我想要加密一些文本,我需要一个随机生成的字节数组来给要加密的文本加盐。让我们添加这个方法:

public class EncryptionHelper
{
  public string TextToEncrypt = "";
  public void Encrypt()
  {
  }
  public string GenerateSalt()
  {
    Return "";
  }
}

现在再看一下类,我意识到加密文本需要保存在数据库中。所以,我添加了一个方法来做到这一点:

public class EncryptionHelper
{
  public string TextToEncrypt = "";
  public void Encrypt()
  {
  }
  public string GenerateSalt()
  {
    return "";
  }
  public void SaveToDatabase()
  {
  }
}

如果我们必须实现这个类,它会看起来像这样:

EncryptionHelper encr = new EncryptionHelper();
encr.TextToEncrypt = "Secret Text";
string salt = encr.GenerateSalt();
encr.Encrypt();
encr.SaveToDatabase();

好吧,但现在我们看到有一个问题。salt需要被加密方法使用,所以自然我们会想要在Encrypt()方法中添加一个参数来接受salt。因此,我们会这样做:

public void Encrypt(string salt)
{
}

在这里,代码开始变得有点模糊。我们在类上调用一个方法来生成一个salt。然后我们将从类中生成的salt传回类。想象一个有许多方法的类。哪些方法需要在何时调用,以及以什么顺序?

所以,让我们退一步思考。我们到底想要做什么?我们想要加密一些文本。因此,我们只想要以下内容:

public class EncryptionHelper
{
  public string TextToEncrypt = "";
  public void Encrypt()
  {
  }
}

这就是我们所说的抽象。回顾抽象的定义,我们在代码中所做的与定义相符,因为我们只显示必要的内容。

那么类中的其他方法呢?很简单地说...将它们设为private。实现您的类的开发人员不需要知道如何加密文本字符串。实现您的类的开发人员只想要加密字符串并将其保存。代码可以这样封装:

public class EncryptionHelper
{
  public string TextToEncrypt = "";
  public void Encrypt()
  {
    string salt = GenerateSalt();
    // Encrypt the text in the TextToEncrypt variable
    SaveToDatabase();
  }
  private string GenerateSalt()
  {
    return "";
  }
  private void SaveToDatabase()
  {
  }
}

调用加密类的代码现在也简单得多。它看起来像这样:

EncryptionHelper encr = new EncryptionHelper();
encr.TextToEncrypt = "Secret Text";
encr.Encrypt();

再次,这符合封装的定义,即隐藏复杂性。

请注意,前面加密示例中的代码没有任何实现。我只是在这里阐述一个概念。如果您愿意,您可以自由添加自己的实现。

最后,不要将抽象与抽象类混淆。这些是不同的东西。抽象是一种思维方式。我们将在下一节中看看抽象类。

因此,请休息 5 分钟,呼吸新鲜空气或喝杯咖啡,然后回来,做好准备!事情即将变得有趣。

板球比分跟踪器中的类

根据我们已经学到的面向对象编程的四大支柱,我们将看看我们的应用程序中使用这些概念提供板球比分跟踪器的构建模块的领域。

抽象类

打开BaseClasses文件夹,双击Player.cs文件。您将看到以下代码:

namespace cricketScoreTrack.BaseClasses 
{ 
    public abstract class Player 
    { 
        public abstract string FirstName { get; set; } 
        public abstract string LastName { get; set; } 
        public abstract int Age { get; set; } 
        public abstract string Bio { get; set; } 
    } 
} 

这是我们的抽象类。类声明中的abstract修饰符和属性告诉我们,我们将要修改的东西具有缺失或不完整的实现。因此,它只用作基类。任何标记为抽象的成员必须由派生自我们的Player抽象类的类实现。

抽象修饰符与以下内容一起使用:

  • 类

  • 方法

  • 属性

  • 索引器

  • 事件

如果我们在抽象的Player类中包含一个名为CalculatePlayerRank()的方法,那么我们需要在任何从Player派生的类中提供该方法的实现。

因此,在Player抽象类中,该方法将被定义如下:

abstract public int CalculatePlayerRank(); 

在任何派生类中,Visual Studio 2017 将运行代码分析器,以确定抽象类的所有成员是否已被派生类实现。当您让 Visual Studio 2017 在派生类中实现抽象类时,方法主体默认为NotImplementedException():

public override int CalculatePlayerRank() 
{ 
  throw new NotImplementedException(); 
} 

这是因为您尚未为CalculatePlayerRank()方法提供任何实现。要做到这一点,您需要用实际的工作代码替换throw new NotImplementedException();来计算当前球员的排名。

有趣的是,虽然NotImplementedException()在CalculatePlayerRank()方法的主体内部,但它并没有警告您该方法没有返回 int 值。

抽象类可以被视为需要完成的蓝图。如何完成由开发人员决定。

接口

打开Interfaces文件夹,查看IBatter.cs和IBowler.cs文件。IBatter接口如下所示:

namespace cricketScoreTrack.Interfaces 
{ 
    interface IBatter 
    { 
        int BatsmanRuns { get; set; }         
        int BatsmanBallsFaced { get; set; }         
        int BatsmanMatch4s { get; set; }         
        int BatsmanMatch6s { get; set; }         
        double BatsmanBattingStrikeRate { get; }             
    } 
} 

查看IBowler接口,您将看到以下内容:

namespace cricketScoreTrack.Interfaces 
{ 
    interface IBowler 
    { 
        double BowlerSpeed { get; set; } 
        string BowlerType { get; set; }  
        int BowlerBallsBowled { get; set; } 
        int BowlerMaidens { get; set; }         
        int BowlerWickets { get; set; }         
        double BowlerStrikeRate { get; }         
        double BowlerEconomy { get; }  
        int BowlerRunsConceded { get; set; } 
        int BowlerOversBowled { get; set; } 
    } 
} 

接口将仅包含方法、属性、事件或索引器的签名。如果我们需要向接口添加一个计算球旋转的方法,它将如下所示:

void CalculateBallSpin(); 

在实现上,我们会看到以下代码实现:

void CalculateBallSpin()
{
}

下一个合乎逻辑的问题可能是抽象类和接口之间的区别是什么。让我们转向微软的优秀文档网站—docs.microsoft.com/en-us/。

打开微软文档后,尝试使用深色主题。主题切换在页面右侧,评论、编辑和分享链接的下方。对于夜猫子来说,这真的很棒。

微软用以下语句简洁地总结了接口:

接口就像抽象基类。实现接口的任何类或结构都必须实现其所有成员。

将接口视为动词;也就是说,接口描述某种动作。板球运动员所做的事情。在这种情况下,动作是击球和投球。因此,在板球比分跟踪器中,接口分别是IBatter和IBowler。请注意,约定规定接口以字母I开头。

另一方面,抽象类充当告诉您某物是什么的名词。我们有击球手和全能选手。我们可以说这两位板球运动员都是球员。这是描述板球比赛中板球运动员的普通名词。因此,在这里使用Player抽象类是有意义的。

类

Cricket Score Tracker应用程序中使用的类都在Classes文件夹中创建。在这里,你会看到一个Batsman类和一个AllRounder类。为了简单起见,我只创建了这两个类。在板球中,所有投手都必须击球,但并非所有击球手都必须投球。然后你会得到能够击球和投球同样出色的投手,他们被定义为全能手。这就是我在这里建模的内容。

首先让我们看一下Batsman类。我们希望击球手具有球员的抽象属性,但他也必须是击球手。因此,我们的类继承了Player基类(记住,我们只能继承自一个类),并实现了IBatter接口的属性:

因此,类定义读作Batsman公共类,继承自Player,并实现IBatter接口。因此,Batsman类如下所示:

using cricketScoreTrack.BaseClasses; 
using cricketScoreTrack.Interfaces; 

namespace cricketScoreTrack.Classes 
{ 
    public class Batsman : Player, IBatter 
    { 
        #region Player 
        public override string FirstName { get; set; } 
        public override string LastName { get; set; } 
        public override int Age { get; set; } 
        public override string Bio { get; set; } 
        #endregion 

        #region IBatsman 
        public int BatsmanRuns { get; set; } 
        public int BatsmanBallsFaced { get; set; } 
        public int BatsmanMatch4s { get; set; } 
        public int BatsmanMatch6s { get; set; } 

        public double BatsmanBattingStrikeRate => (BatsmanRuns * 100) 
         / BatsmanBallsFaced;  

        public override int CalculatePlayerRank() 
        { 
            return 0; 
        } 
        #endregion 
    } 
} 

请注意,Batsman类实现了抽象类和接口的属性。同时,请注意,此时我不想为CalculatePlayerRank()方法添加实现。

现在让我们看一下AllRounder类。我们希望全能手也具有球员的抽象属性,但他们也必须是击球手和投球手。因此,我们的类继承了Player基类,但现在实现了IBatter和IBowler接口的属性:

因此,类定义读作AllRounder公共类,继承自Player,并实现IBatter和IBowler接口。因此,AllRounder类如下所示:

using cricketScoreTrack.BaseClasses; 
using cricketScoreTrack.Interfaces; 
using System; 

namespace cricketScoreTrack.Classes 
{ 
    public class AllRounder : Player, IBatter, IBowler         
    { 
        #region enums 
        public enum StrikeRate { Bowling = 0, Batting = 1 } 
        #endregion 

        #region Player 
        public override string FirstName { get; set; } 
        public override string LastName { get; set; } 
        public override int Age { get; set; } 
        public override string Bio { get; set; } 
        #endregion 

        #region IBatsman 
        public int BatsmanRuns { get; set; } 
        public int BatsmanBallsFaced { get; set; } 
        public int BatsmanMatch4s { get; set; } 
        public int BatsmanMatch6s { get; set; } 
        public double BatsmanBattingStrikeRate => 
         CalculateStrikeRate(StrikeRate.Batting);  
        #endregion 

        #region IBowler 
        public double BowlerSpeed { get; set; } 
        public string BowlerType { get; set; }  
        public int BowlerBallsBowled { get; set; } 
        public int BowlerMaidens { get; set; } 
        public int BowlerWickets { get; set; } 
        public double BowlerStrikeRate => 
         CalculateStrikeRate(StrikeRate.Bowling);  
        public double BowlerEconomy => BowlerRunsConceded / 
         BowlerOversBowled;  
        public int BowlerRunsConceded  { get; set; } 
        public int BowlerOversBowled { get; set; } 
        #endregion 

        private double CalculateStrikeRate(StrikeRate strikeRateType) 
        { 
            switch (strikeRateType) 
            { 
                case StrikeRate.Bowling: 
                    return (BowlerBallsBowled / BowlerWickets); 
                case StrikeRate.Batting: 
                    return (BatsmanRuns * 100) / BatsmanBallsFaced; 
                default: 
                    throw new Exception("Invalid enum"); 
            } 
        } 

        public override int CalculatePlayerRank() 
        { 
            return 0; 
        } 
    } 
} 

你会再次注意到,我没有为CalculatePlayerRank()方法添加任何实现。因为抽象类定义了这个方法,所有继承自抽象类的类都必须实现这个方法。

现在你也看到AllRounder类必须实现IBowler和IBatter的属性。

把所有东西放在一起

现在,让我们看一下如何使用这些类来创建Cricket Score Tracker应用程序。在击球手部分和投球手部分下面的按钮用于选择特定局的击球手和投球手。

虽然每个按钮都由自己的点击事件处理,但它们都调用完全相同的方法。我们稍后将看一下是如何实现的:

点击 Batsmen 部分下的任一按钮将显示一个带有填充有该队伍中击球手的下拉列表的模态对话框:

同样,当我们点击选择投球手按钮时,我们将看到完全相同的模态对话框屏幕显示。不过这次,它将显示可供选择的投球手列表:

从下拉列表中选择球员将填充按钮点击时显示的文本为该球员的名字。然后设置当前局的参与球员。

请注意,我们在这里谈论的是类。我们有球员,但他们可以是击球手或全能手(投球手)。

每个球员都是击球手或投球手(AllRounder类):

那么我们是如何让一个方法返回两个不同的球员的呢?我使用了一个叫做GeneratePlayerList()的方法。这个方法负责在弹出的模态对话框中创建球员列表。这就是这个方法的全部责任。换句话说,它除了生成球员列表之外不执行任何其他功能。

让我们来看一下Default.aspx.cs文件是如何创建的。为了简单起见,我只为每个队伍创建了两个列表。我还创建了一个用于选择球员的enum。代码如下:

public enum SelectedPlayer { Batsman1 = 1, Batsman2 = 2, Bowler = 3 } 
List<Player> southAfrica; 
List<Player> india; 

然而,实际上,你可能会将列表名称命名为team1和team2,并允许用户从设置屏幕上选择这场比赛的队伍。我没有添加这个功能,因为我只是想在这里说明面向对象编程的概念。

在Page_Load中,我用以下方法填充列表:

protected void Page_Load(object sender, EventArgs e) 
{ 
    southAfrica = Get_SA_Players(); 
    india = Get_India_Players(); 
} 

再次为了简单起见,我已经将球员的名字硬编码并手动添加到列表中。

Get_India_Players()方法与Get_SA_Players()方法是相同的。然后你可以复制这个方法,将名字改成你最喜欢的板球运动员或最喜欢的板球队。

实际上,你可能会从一个团队和球员的数据库中读取这些信息。所以,你不会有Get_SA_Players()和Get_India_Players(),而是会有一个单一的Get_Players()方法,负责将球员读入列表中。

现在,看看Get_SA_Players()方法,我们只是做以下操作:

private List<Player> Get_SA_Players() 
{ 
    List<Player> players = new List<Player>(); 

    #region Batsmen 
    Batsman b1 = new Batsman(); 
    b1.FirstName = "Faf"; 
    b1.LastName = "du Plessis"; 
    b1.Age = 33; 
    players.Add(b1); 
    // Rest omitted for brevity 
    #endregion 

    #region All Rounders 
    AllRounder ar1 = new AllRounder(); 
    ar1.FirstName = "Farhaan"; 
    ar1.LastName = "Behardien"; 
    ar1.Age = 33; 
    players.Add(ar1); 
    // Rest omitted for brevity 
    #endregion 

    return players; 
} 

现在注意到players列表的类型是List<Player>,我们正在向其中添加Batsman和AllRounder类型。这就是多态性的含义。记住我们之前提到的多态性的一个方面是:

在运行时,从基类派生的类可以被视为它继承的类的对象。这在参数、集合或数组中可以看到。

因此,因为Batsman和AllRounder都继承自Player抽象类,它们被视为List<Player>的对象。

如果你回到本章前面关于多态性的部分,你会发现这是运行时多态性的一个例子。

回到选择击球手或投球手的逻辑,我们寻找一个生成球员列表的方法,称为GeneratePlayerList():

private void GeneratePlayerList(List<Player> team, Type type) 
{ 
    List<string> players = new List<string>(); 

    if (type == typeof(Batsman)) 
        players = (from r in team.OfType<Batsman>() 
                   select $"{r.FirstName} {r.LastName}").ToList(); 

    if (type == typeof(AllRounder)) 
        players = (from r in team.OfType<AllRounder>() 
                   select $"{r.FirstName} {r.LastName}").ToList(); 

    int liVal = 0; 
    if (ddlPlayersSelect.Items.Count > 0) 
        ddlPlayersSelect.Items.Clear(); 

    foreach (string player in players) 
    { 
        ListItem li = new ListItem(); 
        li.Text = player.ToString(); 
        li.Value = liVal.ToString(); 
        ddlPlayersSelect.Items.Add(li); 

        liVal += 1; 
    } 
} 

你会注意到这个方法接受一个List<Player>参数和一个Type。该方法检查type是Batsman还是AllRounder,并基于此读取列表中球员的名字。

我相信这种方法甚至可以进一步简化,但我想说明多态性的概念。

实际目标是尽量用最少的代码实现最大的效果。作为一个经验法则,一些开发人员认为,如果一个方法的长度超过了你在 IDE 中看到的代码页,你需要进行一些重构。

更少的代码和更小的方法使得代码更易于阅读和理解。它还使得代码更易于维护,因为更小的代码段更容易调试。事实上,你可能会遇到更少的 bug,因为你正在编写更小、更易管理的代码片段。

许多年前,我曾是开普敦一家大公司项目团队的一员。他们有一个名叫乌斯曼·亨德里克斯的系统架构师。我永远不会忘记这个家伙。他是我见过的最谦逊的家伙。他为我们所做系统的文档简直令人难以置信。几乎所有的思考工作都已经包含在我们需要编写的代码中。开发人员根本不需要决定如何设计项目。

这个项目实现了 SOLID 原则,理解代码真的很容易。我现在还有那份文档的副本。我时不时地会参考它。不幸的是,并不是所有的开发人员都有幸在他们所工作的项目中有一个专门的系统架构师。然而,开发人员了解 SOLID 设计原则是很有好处的。

SOLID 设计原则

这引出了面向对象编程中另一个有趣的概念,叫做SOLID设计原则。这些设计原则适用于任何面向对象的设计,旨在使软件更易于理解、更灵活和更易于维护。

SOLID 是一个记忆术,代表:

  • 单一职责原则

  • 开放/封闭原则

  • 里氏替换原则

  • 接口隔离原则

  • 依赖反转原则

在本章中,我们只会看一下前两个原则——单一责任原则和开闭原则。让我们接下来看一下单一责任原则。

单一责任原则

简而言之,一个模块或类应该只具有以下特征:

  • 它应该只做一件事情,并且只有一个改变的原因

  • 它应该很好地完成它的单一任务

  • 提供的功能需要完全由该类或模块封装

说一个模块必须负责一件事情是什么意思?谷歌对模块的定义是:

“一组标准化的部分或独立单元,可以用来构建更复杂的结构,比如家具或建筑物。”

由此,我们可以理解模块是一个简单的构建块。当与其他模块一起使用时,它可以被使用或重复使用来创建更大更复杂的东西。因此,在 C#中,模块确实与类非常相似,但我会说模块也可以扩展为一个方法。

类或模块执行的功能只能是一件事情。也就是说,它有一个狭窄的责任。它只关心它被设计来做的那一件事情,而不关心其他任何事情。

如果我们必须将单一责任原则应用于一个人,那么这个人只能是一个软件开发人员,例如。但如果一个软件开发人员也是医生、机械师和学校老师呢?那这个人在任何一个角色中都会有效吗?这将违反单一责任原则。对于代码也是如此。

看一下我们的AllRounder和Batsman类,你会注意到在AllRounder中,我们有以下代码:

private double CalculateStrikeRate(StrikeRate strikeRateType) 
{ 
    switch (strikeRateType) 
    { 
        case StrikeRate.Bowling: 
            return (BowlerBallsBowled / BowlerWickets); 
        case StrikeRate.Batting: 
            return (BatsmanRuns * 100) / BatsmanBallsFaced; 
        default: 
            throw new Exception("Invalid enum"); 
    } 
} 

public override int CalculatePlayerRank() 
{ 
    return 0; 
} 

在Batsman中,我们有以下代码:

public double BatsmanBattingStrikeRate => (BatsmanRuns * 100) / BatsmanBallsFaced;  

public override int CalculatePlayerRank() 
{ 
    return 0; 
} 

利用我们对单一责任原则的了解,我们注意到这里存在一个问题。为了说明问题,让我们将代码并排比较:

在Batsman和AllRounder类中,我们实际上在重复代码。这对于单一责任来说并不是一个好兆头,对吧?我的意思是,一个类只能有一个功能。目前,Batsman和AllRounder类都负责计算击球率。它们也都负责计算球员排名。它们甚至都有完全相同的代码来计算击球手的击球率!

问题出现在击球率计算发生变化时(虽然不太容易发生,但让我们假设它发生了)。我们现在知道我们必须在两个地方改变计算。一旦开发人员只改变了一个计算而没有改变另一个,就会在我们的应用程序中引入一个 bug。

让我们简化我们的类。在BaseClasses文件夹中,创建一个名为Statistics的新的抽象类。代码应该如下所示:

namespace cricketScoreTrack.BaseClasses 
{ 
    public abstract class Statistics 
    { 
        public abstract double CalculateStrikeRate(Player player); 
        public abstract int CalculatePlayerRank(Player player); 
    } 
} 

在Classes文件夹中,创建一个名为PlayerStatistics的新派生类(也就是它继承自Statistics抽象类)。代码应该如下所示:

using cricketScoreTrack.BaseClasses; 
using System; 

namespace cricketScoreTrack.Classes 
{ 
    public class PlayerStatistics : Statistics 
    { 
        public override int CalculatePlayerRank(Player player) 
        { 
            return 1; 
        } 

        public override double CalculateStrikeRate(Player player) 
        {             
            switch (player) 
            { 
                case AllRounder allrounder: 
                    return (allrounder.BowlerBallsBowled / 
                     allrounder.BowlerWickets); 

                case Batsman batsman: 
                    return (batsman.BatsmanRuns * 100) / 
                     batsman.BatsmanBallsFaced; 

                default: 
                    throw new ArgumentException("Incorrect argument 
                     supplied"); 
            } 
        } 
    } 
} 

你会看到PlayerStatistics类现在完全负责计算球员的排名和击球率的统计数据。

你会看到我没有包括计算球员排名的实现。我在 GitHub 上简要评论了这个方法,说明了球员排名是如何确定的。这是一个相当复杂的计算,对于击球手和投球手是不同的。因此,我在这一章关于面向对象编程的目的上省略了它。

你的解决方案现在应该如下所示:

回到你的Player抽象类,从类中移除abstract public int CalculatePlayerRank();。在IBowler接口中,移除double BowlerStrikeRate { get; }属性。在IBatter接口中,移除double BatsmanBattingStrikeRate { get; }属性。

在Batsman类中,从类中移除public double BatsmanBattingStrikeRate和public override int CalculatePlayerRank()。现在Batsman类的代码如下:

using cricketScoreTrack.BaseClasses; 
using cricketScoreTrack.Interfaces; 

namespace cricketScoreTrack.Classes 
{ 
    public class Batsman : Player, IBatter 
    { 

        #region Player 
        public override string FirstName { get; set; } 
        public override string LastName { get; set; } 
        public override int Age { get; set; } 
        public override string Bio { get; set; } 
        #endregion 

        #region IBatsman 
        public int BatsmanRuns { get; set; } 
        public int BatsmanBallsFaced { get; set; } 
        public int BatsmanMatch4s { get; set; } 
        public int BatsmanMatch6s { get; set; } 
        #endregion 
    } 
} 

看看AllRounder类,移除public enum StrikeRate { Bowling = 0, Batting = 1 }枚举,以及public double BatsmanBattingStrikeRate和public double BowlerStrikeRate属性。

最后,移除private double CalculateStrikeRate(StrikeRate strikeRateType)和public override int CalculatePlayerRank()方法。现在AllRounder类的代码如下:

using cricketScoreTrack.BaseClasses; 
using cricketScoreTrack.Interfaces; 
using System; 

namespace cricketScoreTrack.Classes 
{ 
    public class AllRounder : Player, IBatter, IBowler 
    { 
        #region Player 
        public override string FirstName { get; set; } 
        public override string LastName { get; set; } 
        public override int Age { get; set; } 
        public override string Bio { get; set; } 
        #endregion 

        #region IBatsman 
        public int BatsmanRuns { get; set; } 
        public int BatsmanBallsFaced { get; set; } 
        public int BatsmanMatch4s { get; set; } 
        public int BatsmanMatch6s { get; set; } 
        #endregion 

        #region IBowler 
        public double BowlerSpeed { get; set; } 
        public string BowlerType { get; set; }  
        public int BowlerBallsBowled { get; set; } 
        public int BowlerMaidens { get; set; } 
        public int BowlerWickets { get; set; } 
        public double BowlerEconomy => BowlerRunsConceded / 
         BowlerOversBowled;  
        public int BowlerRunsConceded  { get; set; } 
        public int BowlerOversBowled { get; set; } 
        #endregion         
    } 
} 

回顾一下我们的AllRounder和Batsman类,代码显然更简化了。它肯定更灵活,开始看起来像一组构建良好的类。重新构建你的解决方案,确保一切正常运行。

开闭原则

之前,我们已经看过单一职责原则。与此相辅相成的是开闭原则。

Bertrand Meyer 说过,软件实体(类、模块、函数等):

  • 应该对扩展开放

  • 应该对修改关闭

这到底意味着什么?让我们以PlayerStatistics类为例。在这个类中,你知道我们有一个方法来计算特定球员的击球率。这是因为它继承自Statistics抽象类。这是正确的,但CalculateStrikeRate(Player player)方法为两种球员类型(全能选手和击球手)提供服务,这已经是一个问题的暗示。

假设我们引入了新的球员类型——不同的投球手类型(例如快速投球手和旋转投球手)。为了适应新的球员类型,我们必须改变CalculateStrikeRate()方法中的代码。

如果我们想要传递一组击球手来计算他们之间的平均击球率,我们需要再次修改CalculateStrikeRate()方法来适应这一点。随着时间的推移和复杂性的增加,为不同需要击球率计算的球员类型提供服务将变得非常困难。这意味着我们的CalculateStrikeRate()方法是对修改开放但对扩展关闭。这违反了之前列出的原则。

那么,我们该怎么做才能解决这个问题呢?事实上,我们已经走了一半的路。首先,在Classes文件夹中创建一个新的Bowler类:

using cricketScoreTrack.BaseClasses; 
using cricketScoreTrack.Interfaces; 

namespace cricketScoreTrack.Classes 
{ 
    public class Bowler : Player, IBowler 
    { 
        #region Player 
        public override string FirstName { get; set; } 
        public override string LastName { get; set; } 
        public override int Age { get; set; } 
        public override string Bio { get; set; } 
        #endregion 

        #region IBowler 
        public double BowlerSpeed { get; set; } 
        public string BowlerType { get; set; }  
        public int BowlerBallsBowled { get; set; } 
        public int BowlerMaidens { get; set; } 
        public int BowlerWickets { get; set; } 
        public double BowlerEconomy => BowlerRunsConceded / 
         BowlerOversBowled;  
        public int BowlerRunsConceded { get; set; } 
        public int BowlerOversBowled { get; set; } 
        #endregion 
    } 
} 

你可以看到构建新的球员类型有多么容易——我们只需要告诉类它需要继承Player抽象类并实现IBowler接口。

接下来,我们需要创建新的球员统计类,即BatsmanStatistics、BowlerStatistics和AllRounderStatistics。BatsmanStatistics类的代码如下:

using cricketScoreTrack.BaseClasses; 
using System; 

namespace cricketScoreTrack.Classes 
{ 
    public class BatsmanStatistics : Statistics 
    { 
        public override int CalculatePlayerRank(Player player) 
        { 
            return 1; 
        } 

        public override double CalculateStrikeRate(Player player) 
        { 
            if (player is Batsman batsman) 
            { 
                return (batsman.BatsmanRuns * 100) / 
                 batsman.BatsmanBallsFaced; 
            } 
            else 
                throw new ArgumentException("Incorrect argument 
                 supplied"); 
        } 
    } 
} 

接下来,我们添加AllRounderStatistics类:

using cricketScoreTrack.BaseClasses; 
using System; 

namespace cricketScoreTrack.Classes 
{ 
    public class AllRounderStatistics : Statistics 
    { 
        public override int CalculatePlayerRank(Player player) 
        { 
            return 1; 
        } 

        public override double CalculateStrikeRate(Player player) 
        { 
            if (player is AllRounder allrounder) 
            { 
                return (allrounder.BowlerBallsBowled / 
                 allrounder.BowlerWickets); 
            } 
            else 
                throw new ArgumentException("Incorrect argument 
                 supplied");             
        } 
    } 
} 

最后,我们添加了名为BowlerStatistics的新球员类型统计类:

using cricketScoreTrack.BaseClasses; 
using System; 

namespace cricketScoreTrack.Classes 
{ 
    public class BowlerStatistics : Statistics 
    { 
        public override int CalculatePlayerRank(Player player) 
        { 
            return 1; 
        } 

        public override double CalculateStrikeRate(Player player) 
        { 
            if (player is Bowler bowler) 
            { 
                return (bowler.BowlerBallsBowled / 
                 bowler.BowlerWickets); 
            } 
            else 
                throw new ArgumentException("Incorrect argument 
                 supplied"); 
        } 
    } 
} 

将计算所有球员击球率的责任从PlayerStatistics类中移开,使我们的代码更清晰、更健壮。事实上,PlayerStatistics类已经几乎过时了。

通过添加另一种球员类型,我们能够通过实现正确的接口轻松定义这个新球员的逻辑。我们的代码更小,更容易维护。通过比较我们之前编写的CalculateStrikeRate()的代码和新代码,我们可以看到这一点。

为了更清楚地说明,看一下下面的代码:

public override double CalculateStrikeRate(Player player) 
{             
    switch (player) 
    { 
        case AllRounder allrounder: 
            return (allrounder.BowlerBallsBowled / 
             allrounder.BowlerWickets); 

        case Batsman batsman: 
            return (batsman.BatsmanRuns * 100) / 
             batsman.BatsmanBallsFaced; 

        case Bowler bowler: 
            return (bowler.BowlerBallsBowled / bowler.BowlerWickets); 

        default: 
            throw new ArgumentException("Incorrect argument 
             supplied"); 
    } 
} 

前面的代码比下面的代码复杂得多,难以维护:

public override double CalculateStrikeRate(Player player) 
{ 
    if (player is Bowler bowler) 
    { 
        return (bowler.BowlerBallsBowled / bowler.BowlerWickets); 
    } 
    else 
        throw new ArgumentException("Incorrect argument supplied"); 
} 

例如,创建一个BowlerStatistics类的好处是,你知道在整个类中我们只处理球员,没有别的东西……一个单一的责任,可以在不修改代码的情况下进行扩展。

总结

虽然 SOLID 编程原则是很好的指导方针,但你遇到的很少有系统会在整个应用程序中实际实现它们。特别是如果你继承了一个已经投入生产多年的系统。

我必须承认,我遇到过一些以 SOLID 为设计理念的应用程序。这些应用程序非常容易操作,对团队中的其他开发人员设定了很高的代码质量标准。

同行代码审查和团队中每个开发人员对 SOLID 原则的深入理解,确保了保持相同水平的代码质量。

这一章内容非常丰富。除了为一个非常好的板球比分跟踪应用程序奠定基础外,我们还深入了解了面向对象编程的真正含义。

我们研究了抽象和封装之间的区别。我们讨论了多态性,并了解了运行时多态性与编译时多态性的区别。我们还研究了继承,即通过继承基类来创建派生类。

然后我们讨论了类、抽象类(不要与抽象混淆)和接口。希望清楚地解释了抽象类和接口之间的区别。记住,接口充当动词或行为,而抽象类充当名词,说明某物是什么。

在最后一节中,我们简要讨论了 SOLID 设计原则,并强调了单一责任和开闭原则。

在下一章中,我们将深入探讨使用.NET Core 进行跨平台开发。你会发现.NET Core 是一个非常重要的技能,它将伴随我们很长一段时间。随着.NET Core 和.NET 标准的发展,开发人员将有能力创造——好吧,我会留给你来想象。天空是极限。

第三章:跨平台.NET Core 系统信息管理器

在本章中,我们将创建一个简单的信息仪表板应用程序,显示我们正在运行的计算机的信息,以及该计算机位置的天气情况。这是使用 IP 地址完成的,虽然可能不是 100%准确(因为给我的位置是一个镇或者离这里有一段距离),但我想要证明的概念不是位置准确性。

关于我们正在创建的应用程序,我们将做以下事情:

  • 在 Windows 上设置应用程序

  • 查看Startup.cs文件并添加控制器和视图

  • 在 Windows 上运行应用程序

  • 在 macOS 上运行应用程序

  • 在 Linux 上设置和运行应用程序

本章主要介绍 ASP.NET Core 是什么。对于那些不知道的人,.NET Core 允许我们创建可以在 Windows、macOS 和 Linux 上运行的应用程序。

.NET Core 包括 ASP.NET Core 和 EF Core。

Microsoft 将 ASP.NET Core 定义如下:

"ASP.NET Core 是一个跨平台、高性能、开源框架,用于构建现代、基于云的、互联网连接的应用程序。"

是的,.NET Core 是开源的。您可以在 GitHub 上找到它 - github.com/dotnet/core。使用.NET Core 的好处列在文档网站上 - docs.microsoft.com/en-us/aspnet/core/。这些好处如下:

  • 构建 Web UI 和 Web API 的统一故事

  • 集成现代客户端框架和开发工作流

  • 云就绪,基于环境的配置系统

  • 内置依赖注入

  • 轻量级、高性能和模块化的 HTTP 请求管道

  • 能够在IIS(Internet Information Services)上托管或在自己的进程中进行自托管

  • 可以在.NET Core 上运行,支持真正的并行应用程序版本

  • 简化现代 Web 开发的工具

  • 能够在 Windows、macOS 和 Linux 上构建和运行

  • 开源和社区关注

我鼓励您查看 Microsoft 文档网站上关于这个主题的内容 - docs.microsoft.com/en-us/aspnet/core/。

实际上,ASP.NET Core 只包括适用于您的项目的 NuGet 包。这意味着应用程序更小、性能更好。在本章中,将会看到 NuGet 的用法。

所以,让我们开始吧。接下来,让我们创建我们的第一个跨平台 ASP.NET Core MVC 应用程序。

在 Windows 上设置项目

我们需要做的第一件事是在开发机器上设置.NET Core 2.0。出于本书的目的,我使用 Windows PC 来说明这一步骤,但实际上,您可以在 macOS 或 Linux 上设置.NET Core 应用程序。

我将在本章后面说明如何在 Linux 上设置.NET Core。对于 macOS,这个过程类似,但我发现在 Linux 上有点棘手。因此,我选择逐步为 Linux 展示这一步骤。

对于 macOS,我将向您展示如何在 Windows PC 上创建的应用程序上运行。这就是.NET Core 的真正之美。它是一种真正的跨平台技术,能够在任何三个平台(Windows、macOS 和 Linux)上完美运行:

  1. 将浏览器指向www.microsoft.com/net/core并下载.NET Core SDK:

安装也非常简单。如果您看一下这个屏幕,您会注意到这与 Linux 安装之间的相似之处。两者都有一个通知,告诉您在安装过程中运行一个命令来提高项目恢复速度:

安装完成后,您会找到一些资源、文档、教程和发布说明的链接:

  1. 启动 Visual Studio 并创建一个新的 ASP.NET Core Web 应用程序。同时,选择.NET Framework 4.6.2:

  1. 在下一个屏幕上,从模板中选择 Web 应用程序(模型-视图-控制器),并确保您已选择了 ASP.NET Core 2.0。准备好后,单击“确定”按钮创建 ASP.NET Core 项目:

创建项目后,您将在解决方案资源管理器中看到熟悉的 MVC 结构。模型-视图-控制器架构模式需要一点时间来适应,特别是如果您是从传统的 ASP.NET Web Forms 方法转变而来的 Web 开发人员。

我向您保证,使用 MVC 工作一段时间后,您将不想回到 ASP.NET Web Forms。使用 MVC 非常有趣,而且在许多方面更有益,特别是如果这对您来说仍然是全新的:

  1. 现在,您可以通过按住Ctrl + F5或在 Visual Studio 中点击调试按钮来运行应用程序。应用程序启动后,浏览器将显示 MVC 应用程序的标准视图:

  1. 停止调试会话,右键单击解决方案资源管理器中的项目。从弹出的上下文菜单中,单击 ManageNuGetPackages...,这将打开 NuGet 表单。

我们要添加的第一个 NuGet 包是Newtonsoft.Json。这是为了使我们能够在应用程序中使用 JSON。

  1. 单击安装按钮以将最新版本添加到您的应用程序中:

我们要添加的下一个 NuGet 包叫做DarkSkyCore。这是一个用于使用 Dark Sky API 的.NET Standard 库。

我已经看到有人对.NET Standard 库的说法产生了疑问。我们在这里处理的是.NET Core,对吧?那么,.NET Standard 是什么呢?

以下网站(.NET Core 教程)对此有很好的解释(dotnetcoretutorials.com/2017/01/13/net-standard-vs-net-core-whats-difference/):

"如果您编写一个希望在.net Core、UWP、Windows Phone 和.net Framework 上运行的库,您只需要使用所有这些平台上都可用的类。您如何知道哪些类在所有平台上都可用?.net Standard!"

.NET Standard 就是这样一个标准。如果您想要针对更多平台进行目标化,您需要针对较低版本的标准。如果您想要更多的 API 可用,您需要针对较高版本的标准。有一个 GitHub 存储库,github.com/dotnet/standard,您可以查看,并且有一个方便的图表显示每个平台版本实现了标准的哪个版本,可以转到github.com/dotnet/standard/blob/master/docs/versions.md查看。

  1. 返回到DarkSkyCore。单击安装按钮以获取最新版本:

现在我们已经安装了 NuGet 包,让我们更详细地查看项目。

项目详细信息

在我添加了所有必需的资源、控制器、视图和模型之后查看项目,您会注意到我添加了一些额外的文件夹。

我的解决方案将如下所示:

  • _docs(在下面的截图中标记为1):我个人的偏好是保留一个文件夹,我可以在其中做笔记并保存我发现对项目有用的相关链接

  • climacons(2): 这是包含将用作天气图标的 SVG 文件的文件夹

  • InformationController(3): 这是项目的控制器

  • InformationModel(4): 这是项目的模型

  • GetInfo(5): 这是与我的控制器上的GetInfo()方法对应的视图

除了Models,Views和Controllers文件夹,您可以根据需要放置其他文件夹。只需记住保持与解决方案相关的引用即可:

Climacons

Adam Whitcroft 为 Web 应用程序和 UI 设计师创建了 75 个气候分类的象形文字。我们需要下载它们以在我们的应用程序中使用:

  1. 前往adamwhitcroft.com/climacons/并下载该集合以将其包含在您的项目中。

始终记得要对你应用程序中使用的资源的创建者进行归因。

  1. 要将文件夹包含在项目中,只需将 SVG 文件放在项目中的一个文件夹中:

Startup.cs 文件

深入代码,让我们从Startup.cs文件开始。它应该已经默认创建了,但为了完整起见,我也在这里包含了它。

作为标准命名约定,名称Startup用于此文件。但实际上,您可以随意命名它。只需确保在Program.cs文件中也将其重命名。

Startup.cs文件中应包括以下using语句:

using Microsoft.AspNetCore.Builder; 
using Microsoft.AspNetCore.Hosting; 
using Microsoft.Extensions.Configuration; 
using Microsoft.Extensions.DependencyInjection; 

Startup文件中包含的代码对您来说将是相同的,并且在创建应用程序时默认生成。在本章中,我们不会修改此文件,但是通常情况下,如果您想要添加任何中间件,您会来到Configure()方法:

public class Startup 
{ 
    public Startup(IConfiguration configuration) 
    { 
        Configuration = configuration; 
    } 

    public IConfiguration Configuration { get; } 

    // This method gets called by the runtime. Use this method to add 
      services to the container. 
    public void ConfigureServices(IServiceCollection services) 
    { 
        services.AddMvc(); 
    } 

    // This method gets called by the runtime. Use this method
     to configure the HTTP request pipeline. 
    public void Configure(IApplicationBuilder app, IHostingEnvironment 
    env) 
    { 
        if (env.IsDevelopment()) 
        { 
            app.UseDeveloperExceptionPage(); 
            app.UseBrowserLink(); 
        } 
        else 
        { 
            app.UseExceptionHandler("/Home/Error"); 
        } 

        app.UseStaticFiles(); 

        app.UseMvc(routes => 
        { 
            routes.MapRoute( 
                name: "default", 
                template: "{controller=Home}/{action=Index}/{id?}"); 
        }); 
    } 
} 

InformationModel 类

该应用程序的模型非常简单。它将仅公开在我们的控制器中获取的值,并提供视图访问这些值的权限。要添加模型,请右键单击Models文件夹,然后添加一个名为InformationModel的新类:

public class InformationModel 
{         
    public string OperatingSystem { get; set; } 
    public string InfoTitle { get; set; } 
    public string FrameworkDescription { get; set; } 
    public string OSArchitecture { get; set; } 
    public string ProcessArchitecture { get; set; } 
    public string Memory { get; set; } 
    public string IPAddressString { get; set; } 
    public string WeatherBy { get; set; } 
    public string CurrentTemperature { get; set; } 
    public string CurrentIcon { get; set; } 
    public string DailySummary { get; set; } 
    public string CurrentCity { get; set; } 
    public string UnitOfMeasure { get; set; } 
} 

然后,按照前面的代码清单所示,添加属性。

InformationController 类

我们需要采取的下一步是为我们的应用程序添加控制器:

  1. 右键单击Controllers文件夹,选择“添加”,然后在上下文菜单中单击“Controller”:

  1. 通过从添加脚手架屏幕中选择 MVC Controller - Empty 来添加一个名为InformationController的新控制器。需要将以下using语句添加到控制器中:
using DarkSky.Models; 
using DarkSky.Services; 
using Microsoft.AspNetCore.Hosting; 
using Microsoft.AspNetCore.Mvc; 
using Newtonsoft.Json; 
using System.Globalization; 
using System.IO; 
using System.Net.Http; 
using System.Runtime.InteropServices; 
using System.Threading.Tasks; 
using static System.Math; 

微软文档中提到:

IHostingEnvironment 服务提供了与环境交互的核心抽象。这项服务由 ASP.NET 托管层提供,并可以通过依赖注入注入到启动逻辑中。

要了解更多信息,请浏览docs.microsoft.com/en-us/aspnet/core/fundamentals/environments并查看文档。

  1. 在我们的控制器的前面构造函数中添加以下属性。您会注意到我们已经将IHostingEnvironment接口添加到了类中:
public string PublicIP { get; set; } = "IP Lookup Failed"; 
public double Long { get; set; } 
public double Latt { get; set; } 
public string City { get; set; } 
public string CurrentWeatherIcon { get; set; } 
public string WeatherAttribution { get; set; } 
public string CurrentTemp { get; set; } = "undetermined"; 
public string DayWeatherSummary { get; set; } 
public string TempUnitOfMeasure { get; set; } 
private readonly IHostingEnvironment _hostEnv; 

public InformationController(IHostingEnvironment hostingEnvironment) 
{ 
    _hostEnv = hostingEnvironment; 
} 
  1. 创建一个名为GetInfo()的空方法。控制器(以及其中包含的方法)、视图和模型的命名是非常有意义的。如果遵循 MVC 设计模式的一组约定,将所有这些绑定在一起就会变得非常容易:
public IActionResult GetInfo() 
{ 

}
  1. 如果您还记得,Startup类在Configure()方法中定义了一个MapRoute调用:

代码的这一部分{controller=Home}/{action=Index}/{id?}被称为路由模板。MVC 应用程序使用标记化来提取路由值。

这意味着以下内容:

    • {controller=Home}定义了默认为Home的控制器的名称
  • {action=Index}定义了默认为Index的控制器的方法

  • 最后,{id?}被定义为可选的,通过?,可以用来传递参数

这意味着如果我不给应用程序指定路由(或 URL),它将使用在MapRoute调用中设置的默认值。

但是,如果我给应用程序一个http://localhost:50239/Information/GetInfo的路由,它将重定向到InformationController上的GetInfo()方法。

有关路由的更多信息,请访问docs.microsoft.com/en-us/aspnet/core/mvc/controllers/routing并阅读文档。

  1. 在我们的Controllers文件夹中,添加一个名为LocationInfo的类。我们将在调用位置信息 API 后使用它将 JSON 字符串绑定到它:
public class LocationInfo 
{ 
    public string ip { get; set; }  
    public string city { get; set; }  
    public string region { get; set; }  
    public string region_code { get; set; } 
    public string country { get; set; } 
    public string country_name { get; set; } 
    public string postal { get; set; } 
    public double latitude { get; set; } 
    public double longitude { get; set; }  
    public string timezone { get; set; }  
    public string asn { get; set; }  
    public string org { get; set; }          
} 

要获取位置信息,您可以使用许多位置 API 之一。我在ipapi.co上使用了一个 API 来为我提供位置信息。GetLocationInfo()方法只是调用 API 并将返回的 JSON 反序列化为刚刚创建的LocationInfo类。

就我个人而言,我认为ipapi这个名字真的很聪明。这是一个人不容易忘记的东西。他们还在他们的定价中提供了一个免费层,每天可以进行 1,000 次请求。这非常适合个人使用:

private async Task GetLocationInfo() 
{ 
    var httpClient = new HttpClient(); 
    string json = await 
     httpClient.GetStringAsync("https://ipapi.co/json"); 
    LocationInfo info = JsonConvert.DeserializeObject<LocationInfo>
    (json); 

    PublicIP = info.ip; 
    Long = info.longitude; 
    Latt = info.latitude; 
    City = info.city; 
}
  1. 我们将使用的下一个 API 是Dark Sky。您需要在darksky.net/dev注册帐户以获取您的 API 密钥:

我喜欢 Dark Sky 的一点是,他们的 API 还允许您每天进行 1,000 次免费 API 调用:

这使它非常适合个人使用。如果您有大量用户,即使选择按使用付费的选项也不贵。

请注意,如果您将 Dark Sky API 用于商业应用程序,您不能要求您应用程序的每个用户注册 Dark Sky API 密钥。您 Dark Sky 应用程序的所有用户必须使用您通过在线门户生成的特定 API 密钥。

对于那些感兴趣的人,常见问题解答提供了对此和许多其他重要问题的澄清:

“...您的最终用户不应该注册 Dark Sky API 密钥:API 密钥应与您的应用程序或服务关联,而不是与您的用户关联。

每天 1,000 次免费调用旨在促进个人使用和应用程序开发,而不是为您的应用程序提供免费天气数据。我们花费了大量资金来开发和维护支持 Dark Sky API 的基础设施。如果您的应用程序因受欢迎而增长,我们将不得不支付用于处理增加的流量所需资源的费用(这将使您的服务和用户受益),而没有财务手段来支持它。因此,我们的服务条款禁止要求用户注册 API 密钥的应用程序。”

跟踪 API 调用也非常容易,可以通过在线门户查看:

有了 Dark Sky API 注册,我们想要看看应用程序是否在使用公制系统或英制系统来测量单位的地区运行。

  1. 创建一个名为GetUnitOfMeasure()的方法,该方法返回一个DarkSkyService.OptionalParameters对象。这本质上只是使用RegionInfo类来检查当前地区是否为公制。

然后设置optParms变量并将其返回给调用类。我还趁机加入了TempUnitOfMeasure属性的摄氏度或华氏度符号:

private DarkSkyService.OptionalParameters GetUnitOfMeasure() 
{ 
    bool blnMetric = RegionInfo.CurrentRegion.IsMetric; 
    DarkSkyService.OptionalParameters optParms = new 
     DarkSkyService.OptionalParameters(); 
    if (blnMetric) 
    { 
        optParms.MeasurementUnits = "si"; 
        TempUnitOfMeasure = "C"; 
    } 
    else 
    { 
        optParms.MeasurementUnits = "us"; 
        TempUnitOfMeasure = "F"; 
    } 
    return optParms; 
} 
  1. 要添加的下一种方法称为GetCurrentWeatherIcon(),它将用于确定要在我们的网页上显示的 Dark Sky 图标。还有许多选择,但出于简洁起见,我选择只包括这几个图标名称。这些图标名称对应于我们解决方案中climacons文件夹中的 SVG 文件名的完整列表:
private string GetCurrentWeatherIcon(Icon ic) 
{ 
    string iconFilename = string.Empty; 

    switch (ic) 
    { 
        case Icon.ClearDay: 
            iconFilename = "Sun.svg"; 
            break; 

        case Icon.ClearNight: 
            iconFilename = "Moon.svg"; 
            break; 

        case Icon.Cloudy: 
            iconFilename = "Cloud.svg"; 
            break; 

        case Icon.Fog: 
            iconFilename = "Cloud-Fog.svg"; 
            break; 

        case Icon.PartlyCloudyDay: 
            iconFilename = "Cloud-Sun.svg"; 
            break; 

        case Icon.PartlyCloudyNight: 
            iconFilename = "Cloud-Moon.svg"; 
            break; 

        case Icon.Rain: 
            iconFilename = "Cloud-Rain.svg"; 
            break; 

        case Icon.Snow: 
            iconFilename = "Snowflake.svg"; 
            break; 

         case Icon.Wind: 
            iconFilename = "Wind.svg"; 
            break; 
         default: 
            iconFilename = "Thermometer.svg"; 
            break; 
    } 
    return iconFilename; 
} 
  1. 创建的下一个方法是GetWeatherInfo()方法。这只是调用DarkSkyService类并将之前在 Dark Sky 门户中生成的 API 密钥传递给它。您会注意到,代码实际上并不是什么高深莫测的东西。

该类中的步骤如下:

    1. 定义 Dark Sky 的 API 密钥。
  1. 使用 API 密钥实例化一个新的DarkSkyService对象。

  2. 获取确定度量单位的OptionalParameters对象。

  3. 然后,我们使用纬度和经度以及optParms来获取预报。

  4. 根据预报,我找到了适当的天气图标。

  5. 我使用Path.Combine来获取 SVG 文件的正确路径。

  6. 我读取了 SVG 文件中包含的所有文本。

  7. 最后,我设置了一些属性,用于将归因于 Dark Sky 的天气摘要和温度值四舍五入,使用静态Math类中的Round函数。在代码中,我不需要完全限定这一点,因为我之前已经导入了静态Math类。

因此,您的代码需要如下所示:

private async Task GetWeatherInfo() 
{ 
    string apiKey = "YOUR_API_KEY_HERE"; 
    DarkSkyService weather = new DarkSkyService(apiKey);             
    DarkSkyService.OptionalParameters optParms =
     GetUnitOfMeasure(); 
    var foreCast = await weather.GetForecast(Latt, Long, optParms); 

    string iconFilename = 
     GetCurrentWeatherIcon(foreCast.Response.Currently.Icon); 
    string svgFile = Path.Combine(_hostEnv.ContentRootPath, 
     "climacons", iconFilename); 
    CurrentWeatherIcon = System.IO.File.ReadAllText($"{svgFile}"); 

    WeatherAttribution = foreCast.AttributionLine; 
    DayWeatherSummary = foreCast.Response.Daily.Summary; 
    if (foreCast.Response.Currently.Temperature.HasValue) 
        CurrentTemp = 
     Round(foreCast.Response.Currently.Temperature.Value, 
      0).ToString(); 
} 
  1. 最后但同样重要的是,我们需要向GetInfo()方法添加适当的代码。该方法的第一部分涉及查找应用程序正在运行的计算机的系统信息。这显然会根据我们在其上运行.NET Core 应用程序的操作系统而改变:
public IActionResult GetInfo() 
{ 
    Models.InformationModel model = new Models.InformationModel(); 
    model.OperatingSystem = RuntimeInformation.OSDescription; 
    model.FrameworkDescription = 
     RuntimeInformation.FrameworkDescription; 
    model.OSArchitecture = 
     RuntimeInformation.OSArchitecture.ToString(); 
    model.ProcessArchitecture = 
     RuntimeInformation.ProcessArchitecture.ToString(); 

    string title = string.Empty; 
    string OSArchitecture = string.Empty; 

    if (model.OSArchitecture.ToUpper().Equals("X64")) { 
     OSArchitecture = "64-bit"; } else { OSArchitecture = 
     "32-bit"; } 

    if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) {
     title 
     = $"Windows {OSArchitecture}"; } 
    else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) { 
     title = $"OSX {OSArchitecture}"; } 
    else if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) { 
     title = $"Linux {OSArchitecture}"; } 

    GetLocationInfo().Wait(); 
    model.IPAddressString = PublicIP; 

    GetWeatherInfo().Wait(); 
    model.CurrentIcon = CurrentWeatherIcon; 
    model.WeatherBy = WeatherAttribution; 
    model.CurrentTemperature = CurrentTemp; 
    model.DailySummary = DayWeatherSummary; 
    model.CurrentCity = City; 
    model.UnitOfMeasure = TempUnitOfMeasure; 

    model.InfoTitle = title; 
    return View(model); 
}

GetInfo()方法的最后一部分涉及确定我们在前面步骤中制作的天气信息。

接下来的工作部分将涉及创建我们的视图。一旦我们完成了这一点,真正的乐趣就开始了。

GetInfo 视图

将视图放在一起非常简单。除了天气图标之外,我选择了非常简约的方式,但您可以在这里尽情发挥创意:

  1. 右键单击Views文件夹,然后添加一个名为Information的新文件夹。在Information文件夹中,通过右键单击文件夹并从上下文菜单中选择添加,然后单击 View...来添加一个名为GetInfo的新视图:

视图的命名也遵循了 MVC 中使用的命名约定。

请参阅本章前面的详细项目部分,以查看显示Views文件夹布局的 Visual Studio 解决方案的图像。

已创建的视图使用了 Razor 语法。Razor 是开发人员直接在网页内添加 C#代码(服务器代码)的一种方式。GetInfo.cshtml页面内的代码如下:

@model SystemInfo.Models.InformationModel 

@{ 
    ViewData["Title"] = "GetInfo"; 
} 

<h2> 
    System Information for: @Html.DisplayFor(model => model.InfoTitle)          
</h2> 

<div> 

    <hr /> 
    <dl class="dl-horizontal"> 
        <dt> 
            Operating System 
        </dt>         
        <dd> 
            @Html.DisplayFor(model => model.OperatingSystem)             
        </dd> 
        <dt> 
            Framework Description 
        </dt> 
        <dd> 
            @Html.DisplayFor(model => model.FrameworkDescription) 
        </dd> 
        <dt> 
            Process Architecture 
        </dt> 
        <dd> 
            @Html.DisplayFor(model => model.ProcessArchitecture) 
        </dd>         
        <dt> 
            Public IP 
        </dt> 
        <dd> 
            @Html.DisplayFor(model => model.IPAddressString) 
        </dd>        

    </dl> 
</div> 

<h2> 
    Current Location: @Html.DisplayFor(model => model.CurrentCity) 
</h2> 
<div> 
    <div style="float:left">@Html.Raw(Model.CurrentIcon)</div><div><h3>@Model.CurrentTemperature&deg;@Model.UnitOfMeasure</h3></div> 
</div> 

<div> 
    <h4>@Html.DisplayFor(model => model.DailySummary)</h4> 
</div> 
<div> 
    Weather Info: @Html.DisplayFor(model => model.WeatherBy) 
</div> 

正如您所看到的,MVC 将@model关键字添加到 Razor 的术语中。通过这样做,您允许视图指定视图的Model属性的类型。语法是@model class,包含在第一行中,@model SystemInfo.Models.InformationModel,将视图强类型化为InformationModel类。

有了这种灵活性,您可以直接将 C#表达式添加到客户端代码中。

  1. 您需要添加的最后一部分代码是在Views/Shared文件夹中的_Layout.cshtml文件中:

我们只是在这里的菜单中添加一个链接,以便导航到我们的InformationController类。您会注意到,代码遵循控制器和操作的约定,其中asp-controller指定InformationController类,asp-action指定该控制器内的GetInfo方法。

  1. 在这个阶段,应用程序应该已经准备好运行。构建它并确保您获得了一个干净的构建。运行应用程序,然后单击信息仪表板菜单项。

信息仪表板将显示它正在运行的计算机信息,以及当前位置的天气信息(或附近位置):

在本章的 Windows 部分,我使用了 Azure,因此服务器位于美国。这就是为什么显示的信息是基于美国的。

  1. 最后,让我们来看一下从我们的 Razor 视图生成的 HTML 代码。如果您使用内置的开发者工具(我使用的是 Chrome)并查看页面源代码,您会发现从 Razor 视图创建的 HTML 相当普通:
<h2> 
    System Information for: Windows 64-bit          
</h2> 

<div> 

    <hr /> 
    <dl class="dl-horizontal"> 
        <dt> 
            Operating System 
        </dt>         
        <dd> 
            Microsoft Windows 10.0.14393              
        </dd> 
        <dt> 
            Framework Description 
        </dt> 
        <dd> 
            .NET Core 4.6.00001.0 
        </dd> 
        <dt> 
            Process Architecture 
        </dt> 
        <dd> 
            X64 
        </dd>         
        <dt> 
            Public IP 
        </dt> 
        <dd> 
            13.90.213.135 
        </dd>        

    </dl> 
</div> 

归根结底,这只是 HTML。然而,值得注意的是,我们使用 Razor 来访问我们模型上的属性,并将它们直接放在我们视图的 HTML 中。

在 macOS 上运行应用程序

在本章的这一部分,我将假设您正在使用已安装.NET Core 1.1 的 Mac。如果您的 Mac 上没有安装.NET Core,请前往www.microsoft.com/net/core#macos并按照安装步骤进行安装(或跟随):

  1. 简而言之,从 Windows 的.NET Core 解决方案中,只需发布.NET Core 应用程序。然后,将发布的文件复制到您的 Mac 上。我只是把我的发布文件放在一个名为netCoreInfoDash的桌面文件夹中:

  1. 在您的 Mac 上打开终端,并将工作目录更改为netCoreInfoDash文件夹。输入命令dotnet SystemInfo.dll并按Enter:

因为该项目是为.NET Core 2.0 创建的,而我们的 Mac 只有.NET Core 1.1,所以我们将在终端中看到以下错误消息:

  1. 我们需要将 Mac 上的.NET Core 版本更新到 2.0 版。要做到这一点,前往www.microsoft.com/net/core#macos并安装.NET Core 2.0。

安装.NET Core SDK 非常简单:

在很短的时间内,.NET Core 2.0 就安装在您的 Mac 上了:

  1. 回到终端,输入dotnet SystemInfo.dll并按Enter。这次,您将在终端窗口中看到以下信息输出。您将看到指定了地址http://localhost:5000。列出的端口可能会改变,但5000通常是给定的端口:

  1. 在您的 Mac 上打开浏览器(可以是 Safari,但我使用 Chrome),并导航到—http://localhost:5000。您将看到熟悉的应用程序起始页显示出来。如果您点击“信息仪表板”菜单项,您将看到我们创建的页面与在 Windows 机器上显示的完全相同:

唯一的区别是 Mac 不在 Azure 上,实际上在南非的我的办公室。温度信息已更改为摄氏度,并且显示的机器信息是我的 Mac 的信息。南非这里是一个美好的春天傍晚。

在 Linux 上设置应用程序

每个人都在谈论.NET Core 跨平台运行的能力,甚至在 Linux 上也可以。因此,我决定试一试。我知道 Linux 可能不会吸引你们许多人,但能够使用强大的操作系统 Linux 确实有一种明显的满足感。

如果您正在开发.NET Core 应用程序,我建议您为测试目的设置一个 Linux 框。有许多方法可以做到这一点。如果您可以访问 Azure,可以在 Azure 上设置一个 Linux 虚拟机。

您还可以使用虚拟化软件在本地机器上提供一个完全功能的虚拟机。我选择的选项是使用VirtualBox,并测试了Parallels上的过程。这两种方法都非常简单,但 VirtualBox 是免费使用的,所以这将是一个不错的选择。您可以免费从www.virtualbox.org/wiki/Downloads下载最新版本的 VirtualBox。

你也可以通过从各种在线网站下载现成的 VirtualBox 镜像来节省设置时间。只要确保它们是值得信赖的网站,比如OS Boxes在www.osboxes.org/virtualbox-images/。

无论你选择哪种方式,本章的其余部分将假设你已经设置好了你的 Linux 环境,并且准备好设置你的.NET Core 应用程序。

所以让我们看看如何在 Linux 上安装.NET Core:

  1. 从www.microsoft.com/net/download/linux找到安装.NET Core 2.0 的特定 Linux 版本的说明:

  1. 我正在使用Ubuntu 16.04,点击sudo apt-get install dotnet-sdk-2.0.0链接将带我到安装步骤。

  2. 通过输入Ctrl + Alt + T在 Linux Ubuntu(或 Linux Mint)上打开终端窗口。

由于我正在运行全新的 Linux,我需要先安装cURL。这个工具允许我在服务器和本地机器之间传输数据。

  1. 运行以下命令获取 cURL:
sudo apt-get install curl
  1. 终端会要求输入密码。在屏幕上输入密码不会有任何显示,但继续输入并按Enter:

当你在 Linux 上工作时,屏幕上不显示你输入的密码是有意设计的。这是一个特性。

  1. 现在,我们需要注册受信任的 Microsoft 签名密钥。输入以下内容:
curl https://packages.microsoft.com/keys/microsoft.asc | gpg --dearmor > microsoft.gpg

  1. 当这个完成时,输入以下内容:
    sudo mv microsoft.gpg /etc/apt/trusted.gpg.d/microsoft.gpg
  1. 现在,我们需要为 Ubuntu 16.04 注册 Microsoft 产品源。要做到这一点,请输入以下内容:
sudo sh -c 'echo "deb [arch=amd64] https://packages.microsoft.com/repos/microsoft-ubuntu-xenial-prod xenial main" > /etc/apt/sources.list.d/dotnetdev.list'
  1. 然后,在那之后,输入以下内容:
    sudo apt-get update
  1. 现在,我们可以通过输入以下内容来安装.NET Core 2.0 SDK:
    sudo apt-get install dotnet-sdk-2.0.0
  1. 终端问我们是否要继续,我们要。所以,输入Y并按Enter:

当这个过程完成时,你会看到光标准备好在~$旁边输入:

  1. 要检查安装了哪个版本的.NET Core,请输入以下命令:
    dotnet --version  
  1. 这应该显示 2.0.0。我们现在在我们的 Linux 机器上安装了.NET Core 2.0。作为一个快速开始,创建一个名为testapp的新目录,并通过输入以下内容将你的工作目录更改为testapp目录:
    mkdir testapp
    cd testapp  

考虑以下的屏幕截图:

  1. 我们只是想看看.NET Core 是否在我们的 Linux 机器上工作,所以当你在testapp目录中时,输入以下内容:
    dotnet new razor

是的,就是这么简单。这刚刚在 Linux 上为我们创建了一个新的 MVC Web 项目:

  1. 就像我们在 Mac 上做的那样,输入以下命令:
    dotnet run  

看一下以下的屏幕截图:

  1. 在终端的输出中,你会注意到本地主机显示了相同的端口号。与 macOS 不同,在 Ubuntu 上我可以在终端窗口中点击http://localhost:5000。这将打开我们刚刚创建的应用程序:

现在我们知道.NET Core 2.0 在 Linux 上正常运行了,让我们把项目文件复制到我们的 Linux 机器上:

  1. 在桌面上创建一个文件夹;你可以随意命名。将.NET Core 应用程序的项目文件复制到该文件夹中(不要将发布的文件复制到此文件夹中):

你会记得在 macOS 上,我们只复制了发布的文件。但在 Linux 上是不同的。在这里,你需要复制所有的项目文件。

  1. 右键单击文件夹,选择在终端中打开。

现在我们在包含我们解决方案文件的文件夹中,输入以下内容:

    dotnet restore  

这个命令恢复了我们项目的依赖和工具:

  1. 因为我们正在处理解决方案文件,所以我需要向下导航一个文件夹并输入以下内容:
dotnet run

看一下以下的截图:

  1. 导航到http://localhost:50240在终端窗口中显示,将我带到我的应用程序的起始页面:

  1. 点击信息仪表板菜单项将带我们到我们创建的页面:

这就是全部。我们在 Windows PC 上使用 Visual Studio 2017 Enterprise 创建了一个 ASP.NET Core MVC 应用程序,该应用程序正在 Linux 机器上运行。最好的是,我们没有改变一行代码就让我们的应用程序在不同的平台上运行。

总结

回顾本章,我们看了一下在 Windows 上设置 ASP.NET Core 应用程序。我们看了添加视图和控制器,如果你熟悉 ASP.NET MVC,那么你会感到非常亲切。如果不熟悉,ASP.NET MVC 真的很容易。

最后,我们看了一下.NET Core 的强大之处,通过在 Windows、macOS 和 Linux 上运行相同的应用程序。

现在你应该明白.NET Core 的强大之处了。它允许开发人员使用.NET 编写真正的跨平台应用程序。这项技术是一个改变游戏规则的东西,每个开发人员都必须掌握。

接下来,你可能会想知道当我们想要将数据库连接到.NET Core 应用程序时,我们需要做什么。在下一章中,我们将看看如何在 ASP.NET Core MVC 应用程序上使用 MongoDB。

你可能会想为什么我们要使用 MongoDB?嗯,MongoDB 是免费的、开源的和灵活的。再说,我们为什么不想使用 MongoDB 呢?下一章见!

第四章:任务错误日志 ASP .NET Core MVC 应用程序

在这一章中,我们将通过创建一个任务/错误日志应用程序来看看如何在 ASP.NET Core MVC 中使用 MongoDB。个人任务管理器很有用,当你无法立即处理错误时,记录错误尤其方便。

在本章中,我们将涵盖以下主题:

  • 在本地机器上设置 MongoDB

  • 首次使用 MongoDB Compass

  • 创建一个 ASP.NET Core MVC 应用程序并集成 MongoDB

你可能会想知道为什么我们会选择 MongoDB。你需要问的问题是,你想要花多少精力来创建一个简单的应用程序?

使用 MongoDB 的好处是什么?

为了回答这个问题,让我们来看看使用 MongoDB 的好处。

使用 MongoDB 可以加快开发速度

这可能在你的开发过程中变得更清晰,但让我们说一下,我不喜欢开发过程中的一部分是不得不为各种表单和字段创建数据表。你有没有不得不创建一个表来存储地址字段信息?没错,你需要添加类似以下的内容:

  • 地址 1

  • 地址 2

  • 地址 3

  • 地址 4

  • 城市

  • 状态

  • 邮编

  • 国家

这个表显然可以变得非常庞大。这取决于你需要存储什么。使用 MongoDB,你只需要传递地址数组。MongoDB 会处理剩下的事情。不再需要费力地创建表语句。

提升职业技能

越来越多的职业网站将 MongoDB 列为一个受欢迎的技能。它在公司中被更频繁地使用,新开发人员被期望具有一些 MongoDB 的经验。在 LinkedIn 的职位门户上快速搜索 MongoDB 关键词,仅在美国就返回了 7800 个工作。拥有 MongoDB 经验是一个很好的职业助推器,特别是如果你习惯使用 SQL Server。

MongoDB 在行业中排名很高

为了进一步证明我的观点,MongoDB 在 DB-Engines 网站上排名第五(db-engines.com/en/ranking),在文档存储类别下排名第一(db-engines.com/en/ranking/document+store)。

这些统计数据在撰写时是正确的。事实上,MongoDB 的排名一直在稳步增长。

很明显,MongoDB 会一直存在,更重要的是,社区喜爱 MongoDB。这非常重要,因为它创造了一个健康的开发者社区,分享关于 MongoDB 的知识和文章。MongoDB 的广泛采用推动了技术的发展。

在本地机器上设置 MongoDB

前往www.mongodb.com/download-center#community并下载 Windows 的最新版本的 MongoDB Community Server。安装程序然后给你安装 MongoDB Compass 的选项。

你也可以从上述链接或直接导航到www.mongodb.com/download-center?jmp=nav#compass下载 Compass 作为单独的安装程序。

www.mongodb.com/download-center?jmp=nav#compass。

查看 MongoDB Compass 的网页,docs.mongodb.com/compass/master/,MongoDB Compass 的描述非常清晰:

"MongoDB Compass 旨在允许用户轻松分析和理解 MongoDB 中数据集合的内容,并执行查询,而无需了解 MongoDB 查询语法。

MongoDB Compass 通过随机抽样数据集合中的一部分文档,为用户提供了 MongoDB 模式的图形视图。抽样文档可以最小化对数据库的性能影响,并可以快速产生结果。"

如果这是你第一次使用 MongoDB,我建议你安装 MongoDB Compass 并试着玩一下。

安装 MongoDB 后,您将在C:\ProgramFiles\MongoDB下找到它。我现在想做的是将完整的安装路径保存在一个环境变量中。这样可以更容易地从 PowerShell 或命令提示符中访问。bin文件夹的完整安装路径是C:\Program\FilesMongoDBServer3.6bin。

为了设置它,我们执行以下步骤:

  1. 打开系统属性屏幕,然后单击“环境变量”按钮。

  2. 在“系统变量”组下,选择“Path”变量,然后单击“编辑”按钮。将完整的安装路径添加到“Path”系统变量中。

  3. 现在,我们需要去创建一个文件夹来存储 MongoDB 数据库。您可以在任何地方创建此文件夹,但无论您在哪里创建它,都需要在下一步中使用它。我在以下路径创建了我的 MongoDB 数据库文件夹:D:\MongoTask。

  4. 要使用 MongoDB,您必须首先启动 MongoDB 服务器。无论这是在远程机器上还是在本地机器上都无所谓。打开 PowerShell 并运行以下命令:

     mongod -dbpath D:MongoTask
  1. 运行上述命令后,按 Enter 键。您现在已经启动了 MongoDB 服务器。接下来,启动 MongoDB Compass。

  2. 您会发现您还没有任何数据库。单击“创建数据库”按钮,如下图所示:

  1. 打开“创建数据库”窗口,在“数据库名称”下指定数据库名称,在“集合名称”下指定集合名称。

  2. 最后,单击屏幕底部的“创建数据库”按钮,如下图所示:

  1. 您会看到一个名为TaskLogger的新数据库已经创建,如果展开TaskLogger数据库节点,您将看到列出的 TaskItem 文档,如下图所示:

在本章中,我们不会过多关注 MongoDB Compass。目前,我想向您展示使用 MongoDB Compass 可以以可视化的方式管理 MongoDB 数据库。您可以继续并删除刚刚创建的 TaskItem 文档。稍后,您将看到,当您第一次向 MongoDB 数据库中插入数据时,应用程序会自动为您创建一个文档。

将您的 ASP.NET Core MVC 应用程序连接到 MongoDB

谈到在应用程序中使用 MongoDB 时,人们想知道将这个功能添加到新的 ASP.NET Core MVC 应用程序有多容易。这个过程真的很简单。首先,创建一个新的 ASP.NET Core Web 应用程序,并将其命名为BugTracker:

  1. 在“新 ASP.NET Core Web 应用程序-BugTracker”屏幕上,确保您已从下拉列表中选择了 ASP.NET Core 2.0。

  2. 选择 Web 应用程序(模型-视图-控制器)。

  3. 取消选中启用 Docker 支持选项。最后,单击“确定”按钮。

  4. 您的新 ASP.NET Core MVC 应用程序将以基本形式创建,如下图所示:

  1. 在创建时可以轻松地为应用程序启用 Docker 支持。您还可以为现有应用程序启用 Docker 支持。

我将在后面的章节中介绍 Docker 以及如何使您的应用程序与 Docker 配合使用。目前,我们的应用程序不需要 Docker 支持。将其取消选中,并按照通常的方式创建您的应用程序。

添加 NuGet 包

由于本章主要讨论 MongoDB,我们需要将其添加到我们的项目中。最佳方法是通过添加 NuGet 包来实现。我们可以按照以下步骤进行:

  1. 右键单击您的项目,然后从上下文菜单中选择“管理 NuGet 包...”,如下图所示:

  1. 在 NuGet 屏幕上,您将选择“浏览”选项卡,并输入Mongodb.Driver作为搜索词。

  2. 选择 MongoDB.Driver by MongoDB 选项。

  3. 单击“安装”按钮将最新的稳定包添加到您的项目中。如下面的屏幕截图所示:

  1. 您可以在 Visual Studio 的输出窗口中查看进度。

  2. 在将 MongoDB 添加到项目后,您将看到 MongoDB.Driver(2.5.0)添加到项目的 NuGet 依赖项下,如下面的屏幕截图所示:

  1. 展开Controllers文件夹。您会看到,默认情况下,Visual Studio 已经创建了一个HomeController.cs文件。该文件中的代码应该类似于以下内容:
public class HomeController : Controller 
{ 
    public IActionResult Index() 
    { 
        return View(); 
    } 

    public IActionResult About() 
    { 
        ViewData["Message"] = "Your application description   
        page."; 

        return View(); 
    } 

    public IActionResult Contact() 
    { 
        ViewData["Message"] = "Your contact page."; 

        return View(); 
    } 

    public IActionResult Error() 
    { 
        return View(new ErrorViewModel { RequestId = 
         Activity.Current?.Id ?? HttpContext.TraceIdentifier }); 
    } 
} 

我们希望能够从这里连接到 MongoDB,因此让我们创建一些代码来连接到 Mongo 客户端。

您需要向您的类添加一个using语句,如下所示:

using MongoDB.Driver;

连接到 MongoDB 的步骤如下:

  1. 通过键入片段短代码ctor并按两次制表键,或者通过明确键入代码来创建构造函数。您的构造函数需要创建MongoClient的新实例。完成后,您的代码应如下所示:
public HomeController() 
{ 
    var mclient = new MongoClient(); 
} 
  1. 为了使MongoClient工作,我们需要为其提供一个连接字符串,以连接到我们创建的 MongoDB 实例。在“Bug Tracker”窗格的解决方案中打开appsettings.json文件,如下面的屏幕截图所示:

  1. 打开您的appsettings.json文件,它应该如下所示:
{ 
  "Logging": { 
    "IncludeScopes": false, 
    "LogLevel": { 
      "Default": "Warning" 
    } 
  } 
} 
  1. 修改文件并添加 MongoDB 连接详细信息,如下所示:
{ 
  "MongoConnection": { 
    "ConnectionString": "mongodb://localhost:27017", 
    "Database": "TaskLogger" 
  }, 
  "Logging": { 
    "IncludeScopes": false, 
    "LogLevel": { 
      "Default": "Warning" 
    } 
  } 
}
  1. 现在我们要在Models文件夹中创建一个Settings.cs文件,如下面的屏幕截图所示:

  1. 打开Settings.cs文件并将以下代码添加到其中:
public class Settings 
{ 
    public string ConnectionString { get; set; } 
    public string Database { get; set; } 
} 
  1. 现在我们需要打开Startup.cs文件并修改ConfigureServices方法,如下所示以注册服务:
public void ConfigureServices(IServiceCollection services) 
{ 
    services.AddMvc(); 

    services.Configure<Settings>(Options => 
    { 
        Options.ConnectionString = Configuration.GetSection
          ("MongoConnection:ConnectionString").Value; 
        Options.Database = Configuration.GetSection
         ("MongoConnection:Database").Value; 
    }); 

} 
  1. 返回HomeController.cs文件并修改构造函数以将连接字符串传递给MongoClient:
public HomeController(IOptions<Settings> settings) 
{             
    var mclient = new 
     MongoClient(settings.Value.ConnectionString);     
} 
  1. 此时,我想测试我的代码,以确保它实际访问我的 MongoDB 实例。为此,修改您的代码以返回集群描述:
IMongoDatabase _database; 

public HomeController(IOptions<Settings> settings) 
{             
    var mclient = new 
     MongoClient(settings.Value.ConnectionString);             
      _database = mclient.GetDatabase(settings.Value.Database); 
} 

public IActionResult Index() 
{ 
    return Json(_database.Client.Cluster.Description); 
}
  1. 运行您的 ASP.NET Core MVC 应用程序,并在浏览器中查看输出的信息,如下面的屏幕截图所示:

这一切都很好,但让我们看看如何将添加数据库连接的逻辑分离到自己的类中。

创建 MongoDbRepository 类

要创建MongoDbRepository类,我们需要执行以下步骤:

  1. 在您的解决方案中创建一个名为Data的新文件夹。在该文件夹中,创建一个名为MongoDBRepository的新类:

  1. 在这个类中,添加以下代码:
public class MongoDBRepository 
{ 
    public readonly IMongoDatabase Database; 

    public MongoDBRepository(IOptions<Settings> settings) 
    { 
        try 
        { 
            var mclient = new 
             MongoClient(settings.Value.ConnectionString); 
            Database = 
             mclient.GetDatabase(settings.Value.Database); 
        } 
        catch (Exception ex) 
        { 
            throw new Exception("There was a problem connecting 
             to the MongoDB database", ex); 
        } 
    } 
} 

如果代码看起来很熟悉,那是因为它与我们在HomeController.cs类中编写的相同代码,只是这次有一些错误处理,并且它在自己的类中。这意味着我们还需要修改HomeController类。

  1. 更改HomeController的构造函数中的代码以及Index操作。您的代码需要如下所示:
public MongoDBRepository mongoDb; 

public HomeController(IOptions<Settings> settings) 
{             
    mongoDb =  new MongoDBRepository(settings); 
} 
public IActionResult Index() 
{ 
    return Json(mongoDb.Database.Client.Cluster.Description); 
} 
  1. 再次运行您的应用程序,您将在浏览器中看到先前显示的相同信息,因此再次输出到浏览器窗口。

唯一的区别是现在代码已经适当分离并且易于重用。因此,如果以后发生任何更改,只需在此处更新即可。

读取和写入数据到 MongoDB

在本节中,我们将看一下如何从 MongoDB 数据库中读取工作项列表,以及如何将新的工作项插入到数据库中。我称它们为工作项,因为工作项可以是任务或错误。可以通过执行以下步骤来完成:

  1. 在 Models 文件夹中,创建一个名为WorkItem的新类,如下面的屏幕截图所示:

  1. 将以下代码添加到WorkItem类中。您会注意到Id的类型是ObjectId。这代表了在 MondoDB 文档中创建的唯一标识符。

您需要确保将以下using语句添加到您的WorkItem类using MongoDB.Bson;。

查看以下代码:

public class WorkItem 
{ 
    public ObjectId Id { get; set; } 
    public string Title { get; set; } 
    public string Description { get; set; } 
    public int Severity { get; set; } 
    public string WorkItemType { get; set; } 
    public string AssignedTo { get; set; } 
}
  1. 接下来,打开MongoDBRepository类并将以下属性添加到类中:
public IMongoCollection<WorkItem> WorkItems 
{ 
    get 
    { 
        return Database.GetCollection<WorkItem>("workitem"); 
    } 
} 
  1. 由于我们至少使用 C# 6,我们可以通过将WorkItem属性更改为表达式主体属性来进一步简化代码。为此,将代码更改为如下所示:
public IMongoCollection<WorkItem> WorkItems => Database.GetCollection<WorkItem>("workitem"); 
  1. 如果这看起来有点混乱,请查看以下屏幕截图:

花括号、get和return语句被=>lambda 运算符替换。被返回的对象(在这种情况下是WorkItem对象的集合)放在 lambda 运算符之后。这导致了表达式主体属性。

创建接口和 Work ItemService

接下来,我们需要创建一个接口。为此,我们需要执行以下步骤:

  1. 在解决方案中创建一个名为 Interfaces 的新文件夹,并在 Interfaces 文件夹中添加一个名为IWorkItemService的接口,如下面的屏幕截图所示:

  1. 将以下代码添加到IWorkItemService接口中:
public interface IWorkItemService 
{ 
    IEnumerable<WorkItem> GetAllWorkItems(); 
}
  1. 在您的Data文件夹中,添加另一个名为WorkItemService的类,并使其实现IWorkItemService接口。

确保添加using语句以引用您的接口。在我的示例中,这是using BugTracker.Interfaces;语句。

  1. 您会注意到 Visual Studio 提示您实现接口。要做到这一点,单击灯泡提示,然后单击上下文菜单中的 Implement interface,如下面的屏幕截图所示:

  1. 完成此操作后,您的WorkItemService类将如下所示:
public class WorkItemService : IWorkItemService 
{ 
    public IEnumerable<WorkItem> GetAllWorkItems() 
    { 
        throw new System.NotImplementedException(); 
    } 
}
  1. 接下来,添加一个构造函数并完成GetAllWorkItems方法,使您的类如下所示:
public class WorkItemService : IWorkItemService 
{ 
    private readonly MongoDBRepository repository; 

    public WorkItemService(IOptions<Settings> settings) 
    { 
        repository = new MongoDBRepository(settings); 
    } 

    public IEnumerable<WorkItem> GetAllWorkItems() 
    { 
        return repository.WorkItems.Find(x => true).ToList(); 
    } 
} 
  1. 现在,您需要打开Startup.cs文件并编辑ConfigureServices方法以添加以下代码行:
services.AddScoped<IWorkItemService, WorkItemService>(); 
  1. 您的ConfigureServices方法现在将如下所示:
public void ConfigureServices(IServiceCollection services) 
{ 
    services.AddMvc(); 

    services.Configure<Settings>(Options => 
    { 
        Options.ConnectionString = Configuration.GetSection("MongoConnection:ConnectionString").Value; 
        Options.Database = Configuration.GetSection("MongoConnection:Database").Value; 
    }); 

    services.AddScoped<IWorkItemService, WorkItemService>(); 
} 

您所做的是将IWorkItemService接口注册到依赖注入框架中。有关依赖注入的更多信息,请参阅以下文章:

docs.microsoft.com/en-us/aspnet/core/fundamentals/dependency-injection。

创建视图

当我们启动应用程序时,我们希望看到一个工作项列表。因此,我们需要为HomeController创建一个视图,以执行以下步骤显示工作项列表:

  1. 在 Views 文件夹中,展开 Home 子文件夹,如果有Index.cshtml文件,则删除它。

  2. 然后,右键单击 Home 文件夹,导航到上下文菜单中的 Add | View。将显示 Add MVC View 窗口。

  3. 将视图命名为Index,并选择 List 作为模板。从 Model 类的下拉列表中,选择 WorkItem(BugTracker.Models)。

  4. 将其余设置保持不变,然后单击添加按钮:

添加视图后,您的 Solution Explorer 将如下所示:

  1. 仔细观察视图,您会注意到它使用IEnumerable<BugTracker.Models.WorkItem>作为模型:
@model IEnumerable<BugTracker.Models.WorkItem> 

@{ 
    ViewData["Title"] = "Work Item Listings"; 
} 

这允许我们迭代返回的WorkItem对象集合并在列表中输出它们。还请注意,ViewData["Title"]已从Index更新为Work Item Listings。

修改 HomeController

在我们运行应用程序之前,我们需要做的最后一件事是修改HomeController类以与IWorkItemService一起使用:

  1. 修改构造函数和Index操作如下:
private readonly IWorkItemService _workItemService; 

public HomeController(IWorkItemService workItemService) 
{ 
    _workItemService = workItemService; 

} 

public IActionResult Index() 
{ 
    var workItems = _workItemService.GetAllWorkItems(); 
    return View(workItems); 
} 
  1. 我们正在从 MongoDB 数据库中获取所有工作项,并将它们传递给视图以供模型使用。

确保您已经通过mongod -dbpath <path>命令格式启动了 MongoDB 服务器,就像本章前面解释的那样。

  1. 完成后,运行您的应用程序,如下面的屏幕截图所示:

  1. 此时,数据库中没有工作项,所以我们在浏览器中看到了这个空列表。接下来,我们将添加代码将工作项插入到我们的 MongoDB 数据库中。

添加工作项

让我们通过以下步骤添加工作项:

  1. 要添加工作项,让我们首先在我们的 Models 文件夹中添加一个名为AddWorkItem的类,如下面的屏幕截图所示:

  1. 修改类中的代码,使其基本上看起来像WorkItem类:
public class AddWorkItem 
{ 
    public string Title { get; set; } 
    public string Description { get; set; } 
    public int Severity { get; set; } 
    public string WorkItemType { get; set; } 
    public string AssignedTo { get; set; } 
}
  1. 接下来,在 Views 文件夹下创建一个名为AddWorkItem的新文件夹。右键单击AddWorkItem文件夹,然后选择添加,然后在上下文菜单中单击“View”。

  2. 将显示“添加 MVC 视图”窗口。将视图命名为AddItem,并选择“模板”中的“创建”。

  3. 从 Model 类的下拉菜单中,选择 AddWorkItem(BugTracker.Models)。

  4. 将其余设置保持不变,然后点击“添加”按钮,如下面的屏幕截图所示:

  1. 打开AddItem.cshtml文件,查看表单操作。确保它设置为CreateWorkItem。以下代码片段显示了代码应该是什么样子的:
<div class="row"> 
  <div class="col-md-4"> 
     <form asp-action="CreateWorkItem"> 
         <div asp-validation-summary="ModelOnly" class="text-danger"></div> @*Rest of code omitted for brevity*@ 

您的Views文件夹现在应如下所示:

  1. 现在,我们需要对我们的IWorkItemService接口进行一些小修改。修改接口中的代码如下所示:
public interface IWorkItemService 
{ 
    IEnumerable<WorkItem> GetAllWorkItems(); 
    void InsertWorkItem(WorkItem workItem); 
} 

我们刚刚指定实现IWorkItemService接口的类必须具有一个名为InsertWorkItem的方法,该方法接受WorkItem类型的参数。这意味着我们需要转到WorkItemService并添加一个名为InsertWorkItem的方法。我们的WorkItemService接口中的代码将如下所示:

private readonly MongoDBRepository repository; 

public WorkItemService(IOptions<Settings> settings) 
{ 
    repository = new MongoDBRepository(settings); 
} 

public IEnumerable<WorkItem> GetAllWorkItems() 
{ 
    return repository.WorkItems.Find(x => true).ToList(); 
} 

public void InsertWorkItem(WorkItem workItem) 
{ 
    throw new System.NotImplementedException(); 
} 
  1. 更改InsertWorkItem方法以将WorkItem类型的单个对象添加到我们的 MongoDB 数据库中。更改代码如下所示:
public void InsertWorkItem(WorkItem workItem) 
{ 

} 
  1. 现在,我们需要稍微修改我们的WorkItem类。向类中添加两个构造函数,一个带有AddWorkItem对象作为参数,另一个不带任何参数:
public class WorkItem 
{ 
    public ObjectId Id { get; set; } 
    public string Title { get; set; } 
    public string Description { get; set; } 
    public int Severity { get; set; } 
    public string WorkItemType { get; set; } 
    public string AssignedTo { get; set; } 

    public WorkItem() 
    { 

    } 

    public WorkItem(AddWorkItem addWorkItem) 
    { 
        Title = addWorkItem.Title; 
        Description = addWorkItem.Description; 
        Severity = addWorkItem.Severity; 
        WorkItemType = addWorkItem.WorkItemType; 
        AssignedTo = addWorkItem.AssignedTo; 
    } 
} 

我们添加第二个不带参数的构造函数的原因是为了让 MongoDB 反序列化WorkItem。

如果您想进一步了解为什么为反序列化添加一个无参数构造函数,请查看以下网址:stackoverflow.com/questions/267724/why-xml-serializable-class-need-a-parameterless-constructor。

  1. 现在我们需要向我们的项目添加另一个控制器。右键单击 Controllers 文件夹,然后添加一个名为AddWorkItemController的新控制器。随意将其添加为空控制器。我们将在下面自己添加代码:

  1. 在 AddWorkItemController 控制器中,添加以下代码:
private readonly IWorkItemService _workItemService; 

public AddWorkItemController(IWorkItemService workItemService) 
{ 
    _workItemService = workItemService; 
} 

public ActionResult AddItem() 
{ 
    return View(); 
} 

[HttpPost] 
public ActionResult CreateWorkItem(AddWorkItem addWorkItem) 
{ 
    var workItem = new WorkItem(addWorkItem); 
    _workItemService.InsertWorkItem(workItem); 
    return RedirectToAction("Index", "Home"); 
} 

您会注意到HttpPost操作被称为CreateWorkItem。这就是AddItem.cshtml文件中的表单操作称为CreateWorkItem的原因。它告诉视图在单击创建按钮时要调用控制器上的哪个操作。

重定向到工作项列表

另一个有趣的事情要注意的是,在我们调用WorkItemService上的InsertWorkItem方法之后,我们将视图重定向到HomeController上的Index操作。正如我们已经知道的,这将带我们到工作项列表:

  1. 说到HomeController,修改那里的代码以添加另一个名为AddWorkItem的操作,该操作调用AddWorkItemController类上的AddItem操作:

public ActionResult AddWorkItem() 
{ 
    return RedirectToAction("AddItem", "AddWorkItem"); 
} 
Your HomeController code will now look as follows: 
private readonly IWorkItemService _workItemService; 

public HomeController(IWorkItemService workItemService) 
{ 
    _workItemService = workItemService;             
} 

public IActionResult Index() 
{ 
    var workItems = _workItemService.GetAllWorkItems(); 
    return View(workItems); 
} 

public ActionResult AddWorkItem() 
{ 
    return RedirectToAction("AddItem", "AddWorkItem"); 
} 
  1. 现在,让我们稍微修改Index.cshtml视图。为了使“Index”视图上的列表更直观,修改Index.cshtml文件。

  2. 添加一个if语句,以允许在列表为空时添加新的工作项。

  3. 添加一个ActionLink,在单击时调用HomeController上的AddWorkItem操作:

@if (Model.Count() == 0)
@if (Model.Count() == 0)
{
    <tr>
        <td colspan="6">There are no Work Items in BugTracker. @Html.ActionLink("Add your first Work Item", "AddWorkItem") now.</td>
    </tr>
}
else
{

    @foreach (var item in Model)
    {
        <tr>
            <td>
                @Html.DisplayFor(modelItem => item.Title)
            </td>
            <td>
                @Html.DisplayFor(modelItem => item.Description)
            </td>
            <td>
                @Html.DisplayFor(modelItem => item.Severity)
            </td>
            <td>
                @Html.DisplayFor(modelItem => item.WorkItemType)
            </td>
            <td>
                @Html.DisplayFor(modelItem => item.AssignedTo)
            </td>
            <td>
            @Html.ActionLink("Edit", "Edit", new { /* 
             id=item.PrimaryKey */ }) |
            @Html.ActionLink("Details", "Details", new { /* 
             id=item.PrimaryKey */ }) |
            @Html.ActionLink("Delete", "Delete", new { /* 
             id=item.PrimaryKey */ })
            </td>
        </tr>
   }
}

  1. 现在,将“Create New asp-action”包装在以下if语句中:
@if (Model.Count() > 0) 
{ 
<p> 
    <a asp-action="Create">Create New</a> 
</p> 
} 

我们稍后会看到这个。

在这一点上,我们将看到应用程序的逻辑,HomeController``Index操作列出了工作项。当我们单击“添加您的第一个工作项”链接时,我们调用了HomeController上的AddWorkItem操作。

HomeController上的AddWorkItem操作反过来调用AddWorkItemController上的AddItem操作。这只是返回AddItem视图,我们在其中输入工作项详细信息,然后单击“创建”按钮。

“创建”按钮反过来执行HttpPost,因为AddItem视图上的表单操作指向AddWorkItemController类上的CreateWorkItem操作,我们将工作项插入到我们的 MongoDB 数据库中,并通过执行RedirectToAction调用到HomeController上的Index操作重定向到工作项列表。

现在,在这一点上,如果您认为这是一个冗长的方式,将重定向回HomeController,然后重定向到AddWorkItemController上的AddItem操作,那么您是 100%正确的。我将向您展示一种快速的方法,当用户单击链接创建新工作项时,直接重定向到AddWorkItemController上的AddItem操作。现在,只需跟着我。我试图向您展示如何与控制器和操作进行交互。

现在,再次运行您的应用程序。

您将看到列表中的一个链接允许您添加您的第一个工作项。

这是将重定向回HomeController上的AddWorkItem操作的链接。要运行它,请执行以下操作:

  1. 单击链接,您将看到输出,如下截图所示:

  1. 这将带您到添加新工作项的视图。在字段中输入一些信息,然后单击“创建”按钮。

  1. “创建”按钮调用AddWorkItemController上的CreateWorkItem操作,并在HomeController的Index操作上重定向回工作项列表。

  1. 您可以看到“创建新”链接现在显示在列表顶部。让我们修改“Index.cshtml”视图,使该链接直接重定向到AddWorkItemController类上的AddItem操作。更改 Razor 如下:
@if (Model.Count() > 0) 
{ 
<p> 
    @Html.ActionLink("Create New", "AddWorkItem/AddItem") 
</p> 
} 

您可以看到我们可以指定应用程序必须采取的路由以到达正确的操作。在这种情况下,我们说当单击“创建新”链接时,我们必须调用AddWorkItemController类上的AddItem操作。

再次运行您的应用程序,然后单击“创建新链接”。您会看到被重定向到我们之前添加工作项的输入表单。

视图的默认样式看起来不错,但肯定不是最美丽的设计。至少,这使您作为开发人员有能力返回并使用 CSS 样式屏幕,根据您的需求“美化”它们。目前,这些沉闷的屏幕完全功能,并且足够满足我们的需求。

打开 MongoDB Compass,您会看到那里有一个工作项文档。查看该文档,您将看到我们刚刚从 ASP.NET Core MVC 应用程序中添加的信息。

总结

在本章中,我们看了一下:

  • 在本地机器上设置 MongoDB

  • 使用 MongoDB Compass

  • 创建连接到 MongoDB 的 ASP.NET Core MVC 应用程序

我们看到 MongoDB Compass 为开发人员提供了 MongoDB 数据的良好图形视图。因此,开发人员不需要了解任何 MongoDB 查询语法。但是,如果你想查看查询语法,请访问https://docs.mongodb.com/manual/tutorial/query-documents/。

在涉及 MongoDB 和 ASP.NET Core MVC 时,仍然有很多东西可以学习。单独一章几乎不足以涵盖所有内容。但可以肯定的是,MongoDB 非常强大,同时在应用程序中使用起来非常简单。MongoDB 有很好的文档,并且有一个蓬勃发展的社区可以在你的学习过程中提供帮助和指导。

在下一章中,我们将看一下 SignalR 以及如何创建实时聊天应用程序。

第五章:ASP.NET SignalR 聊天应用程序

想象一下,您有能力让服务器端代码实时推送数据到您的网页,而无需用户刷新页面。他们说,有很多种方法可以解决问题,但 ASP.NET SignalR 库为开发人员提供了一种简化的方法,可以向应用程序添加实时网络功能。

为了展示 SignalR 的功能,我们将构建一个简单的 ASP.NET Core SignalR 聊天应用程序。这将包括使用 NuGet 和Node Package Manager(npm)将所需的包文件添加到项目中。

在这一章中,我们将研究以下内容:

  • 整体项目布局

  • 设置项目

  • 添加 SignalR 库

  • 构建服务器

  • 创建客户端

  • 解决方案概述

  • 运行应用程序

让我们开始吧。

项目布局

对于这个项目,我们需要以下元素:

  • 聊天服务器:这将是我们的服务器端 C#代码,用于处理和指导从客户端发送的消息

  • 聊天客户端:客户端将包括用于向服务器发送消息和接收消息的 JavaScript 函数,以及用于显示的 HTML 元素

我们将从服务器端代码开始,然后转移到客户端,构建一个简单的引导布局,并从那里调用一些 JavaScript 函数。

作为奖励,我们将包括一种方法来将我们的对话历史存档到文本文件中。

设置项目

让我们设置这个项目:

  1. 使用 Visual Studio 2017,我们将创建一个 ASP.NET Core Web 应用程序。您可以随意命名应用程序,但我将其命名为Chapter5:

  1. 我们将使用一个空项目模板。确保从下拉菜单中选择 ASP.NET Core 2.0:

项目将被创建,并将如下所示:

添加 SignalR 库

接下来,我们需要将 SignalR 包文件添加到我们的项目中。

在撰写本文时,通过 NuGet 包管理器浏览时找不到 ASP.NET Core SignalR 的包,因此我们将使用包管理器控制台添加所需的包。

  1. 转到工具 | NuGet 包管理器 | 包管理器控制台:

  1. 在控制台窗口中输入以下命令并按回车键:
Install-Package Microsoft.AspnetCore.SignalR -Version 1.0.0-alpha2-final

您应该看到一些响应行,显示成功安装的项目。

我们还需要 SignalR 客户端 JavaScript 库。为此,我们将使用一个npm命令。

npm 是一个包管理器,类似于 NuGet,但用于 JavaScript。欢迎访问www.npmjs.com查看。

  1. 在控制台窗口中输入以下命令并按回车键:
npm install @aspnet/signalr-client

这将下载一堆 js 文件到项目根目录下的node_modules文件夹中。输出可能会显示一些警告,但不用担心。如果node_modules目录存在,您可以确认下载成功。

有了我们的包,我们可以(终于)开始编写一些代码了。

构建服务器

我们需要为我们的聊天程序构建一个服务器,其中包含我们想要从连接的客户端调用的方法。我们将使用 SignalR Hubs API,该 API 提供了连接的客户端与我们的聊天服务器通信所需的方法。

SignalR Hub 子类

现在我们需要创建 SignalR Hub。为此,请执行以下步骤:

  1. 在项目中添加一个类来处理聊天的服务器端。我们将其称为Chat:

这将需要是 SignalR Hub类的子类。确保添加Micosoft.AspNetCore.SignalR的使用指令。Visual Studio 的快速操作对此效果很好:

  1. 现在向类添加一个Task方法来处理消息的发送:
        public Task Send(string sender, string message) 
        { 
            return Clients.All.InvokeAsync("UpdateChat", sender, 
            message); 
        } 

这个方法将通过任何连接的客户端调用,并调用所有连接的客户端的Send函数,传递发送者和消息参数。

  1. 现在添加一个Task方法来处理存档功能:
        public Task ArchiveChat(string archivedBy, string path, 
         string messages) 
        { 
            string fileName = "ChatArchive" + 
             DateTime.Now.ToString("yyyy_MM_dd_HH_mm") + ".txt"; 
            System.IO.File.WriteAllText(path + "\" + fileName, 
             messages); 
            return Clients.All.InvokeAsync("Archived", "Chat 
             archived by "+ archivedBy); 
        } 

正如您所看到的,这个方法只是简单地获取消息字符串参数的值,将其写入一个名为ChatArchive_[date].txt的新文本文件中,保存到给定路径,并调用客户端的Archived函数。

为了使这两个任务真正起作用,我们需要做一些更多的脚手架工作。

配置更改

在Startup.cs文件中,我们需要将 SignalR 服务添加到容器中,并配置 HTTP 请求管道。

  1. 在ConfigureServices方法中,添加以下代码:
services.AddSignalR();
  1. 在Configure方法中,添加以下代码:
app.UseSignalR(routes => 
      { 
          routes.MapHub<Chat>("chat"); 
      });

您的代码窗口现在如下所示:

这就是我们的服务器完成了。

您会注意到我已经在Configure方法中添加了以下代码行,app.UseStaticFiles()。静态文件是 ASP.NET Core 应用程序直接提供给客户端的资产。静态文件的示例包括 HTML、CSS、JavaScript 和图像。

我们可以(也将)稍后扩展我们服务器的功能,但是现在,让我们前往我们的客户端。

创建客户端

如我们的项目布局中所述,客户端将包括用于向服务器发送消息和接收消息的 JavaScript 函数,以及用于显示的 HTML 元素。

  1. 在您的项目中,在wwwroot下添加一个新的文件夹,名为scripts:

还记得之前由我们的npm命令创建的node_modules目录吗?

  1. 转到node_modules目录中的以下路径:

\@aspnet\signalr-client\dist\browser

查看以下截图:

  1. 将signalr-client-1.0.0-alpha2-final.min.js文件复制到我们项目中刚创建的scripts文件夹中。我们将在我们的 HTML 文件中引用这个库,现在我们将创建这个文件。

  2. 在wwwroot文件夹中添加一个 HTML 页面。我把我的命名为index.html。我建议您也这样命名。稍后我会解释:

我们将保持客户端页面非常简单。我使用div标签作为面板,在页面上显示和隐藏不同的部分。我还使用 bootstrap 使其看起来漂亮,但您可以按自己的喜好设计它。我也不会让您对基础知识感到厌烦,比如在哪里指定页面标题。我们将坚持相关的元素。

让我展示整个 HTML 布局代码以及 JavaScript,然后我们将从那里开始分解:

<!DOCTYPE html> 
<html> 
<head> 
    <title>Chapter 5- Signal R</title> 
    <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css"> 
    <script src="img/jquery.min.js"></script> 
    <script src="img/bootstrap.min.js"></script> 
    <script src="img/signalr-client-1.0.0-alpha2-final.min.js"></script> 

    <script type="text/javascript"> 
        let connection = new signalR.HubConnection('/chat'); 
        connection.start(); 

        connection.on('UpdateChat', (user, message) => { 
            updateChat(user, message); 
        }); 
        connection.on('Archived', (message) => { 
            updateChat('system', message); 
        }); 

        function enterChat() { 
            $('#user').text($('#username').val()); 
            sendWelcomeMessage($('#username').val()); 
            $('#namePanel').hide(); 
            $('#chatPanel').show(); 
        }; 

        function sendMessage() { 
            let message = $('#message').val(); 
            let user = $('#user').text(); 
            $('#message').val(''); 
            connection.invoke('Send', user, message); 
        }; 

        function sendWelcomeMessage(user) { 
            connection.invoke('Send','system',user+' joined the 
            chat'); 
        }; 

        function updateChat(user, message) { 
            let chat = '<b>' + user + ':</b> ' + message + 
            '<br/>' 
            $('#chat').append(chat); 
            if ($('#chat')["0"].innerText.length > 0) { 
                $('#historyPanel').show(); 
                $('#archivePanel').show(); 
            } 
        }; 

        function archiveChat() { 
            let message = $('#chat')["0"].innerText; 
            let archivePath = $('#archivePath').val(); 
            let archivedBy = $('#username').val(); 
            connection.invoke('ArchiveChat', archivedBy, 
             archivePath, message); 
        }; 
    </script> 

</head> 
<body> 
    <div class="container col-md-10"> 
        <h1>Welcome to Signal R <label id="user"></label></h1> 
    </div> 
    <hr /> 
    <div id="namePanel" class="container"> 
        <div class="row"> 
            <div class="col-md-2"> 
                <label for="username" class="form-
                  label">Username:</label> 
            </div> 
            <div class="col-md-4"> 
                <input id="username" type="text" class="form-
                 control" /> 
            </div> 
            <div class="col-md-6"> 
                <button class="btn btn-default" 
                  onclick="enterChat()">Enter</button> 
            </div> 
        </div> 
    </div> 
    <div id="chatPanel" class="container" style="display: none"> 
        <div class="row"> 
            <div class="col-md-2"> 
                <label for="message" class="form-label">Message: 
                </label> 
            </div> 
            <div class="col-md-4"> 
                <input id="message" type="text" class="form-
                 control" /> 
            </div> 
            <div class="col-md-6"> 
                <button class="btn btn-info" 
                 onclick="sendMessage()">Send</button> 
            </div> 
        </div> 
        <div id="historyPanel" style="display:none;"> 
            <h3>Chat History</h3> 
            <div class="row"> 
                <div class="col-md-12"> 
                    <div id="chat" class="well well-lg"></div> 
                </div> 
            </div> 
        </div> 
    </div> 
    <div id="archivePanel" class="container" style="display:none;"> 
        <div class="row"> 
            <div class="col-md-2"> 
                <label for="archivePath" class="form-
                 label">Archive Path:</label> 
            </div> 
            <div class="col-md-4"> 
                <input id="archivePath" type="text" class="form-
                 control" /> 
            </div> 
            <div class="col-md-6"> 
                <button class="btn btn-success" 
                 onclick="archiveChat()">Archive Chat</button> 
            </div> 
        </div> 
    </div> 
</body></html> 

包括的库

添加link和script标签以包含所需的库:

<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/
bootstrap.min.css">
<script src="img/jquery.min.js">
</script>
<script src="img/bootstrap.min.js">
</script>
<script src="img/signalr-client-1.0.0-alpha2-final.min.js"> </script>

如果您不想使用 bootstrap 来进行外观和感觉,您就不需要 bootstrap JavaScript 库或 CSS,但请注意我们将在我们的脚本中使用 jQuery,所以请留下它。

命名部分

我们需要知道谁是我们的聊天室参与者。添加一个输入元素来捕获用户名,以及一个按钮来调用enterChat函数:

  • <input id="username" type="text" class="form-control" />

  • <button class="btn btn-default" onclick="enterChat()">Enter</button>

聊天输入

添加所需的元素,使我们的用户能够输入消息(输入)并将其发送到服务器(sendMessage的事件按钮):

  • <input id="message" type="text" class="form-control" />

  • <button class="btn btn-info" onclick="sendMessage()">Send</button>

对话面板

添加一个带有 ID"chat"的div标签。我们将使用这个作为我们对话的容器(聊天历史):

  • <div id="chat" class="well well-lg"></div>

存档功能

添加所需的元素,使我们的用户能够指定存档文件需要保存的路径(输入),并将消息发送到服务器(archiveChat的事件按钮):

  • <input id="archivePath" type="text" class="form-control" />

  • <button class="btn btn-info" onclick="archiveChat()">Archive Chat</button>

JavaScript 函数

我们的客户端需要一些代码来向服务器发送和接收消息。我尽量保持 JavaScript 尽可能简单,选择了 jQuery 代码以提高可读性:

  1. 为我们的 SignalR Hub 服务器创建一个变量(我命名为connection)并调用其 start 函数:
let connection = new signalR.HubConnection('/chat');
connection.start();

'/chat'参数用于signalR.HubConnection,指的是我们的Chat.cs类,它继承了 SignalR 的 Hub 接口。

  1. 添加UpdateChat和Archived方法,这些方法将由服务器调用:
connection.on('UpdateChat', (user, message) => {
updateChat(user, message);
});
connection.on('Archived', (message) => {
updateChat('system', message);
});

我们只是将从服务器获取的参数传递给我们的updateChat方法。我们稍后会定义这个方法。

  1. 定义enterChat函数:
function enterChat() {
$('#user').text($('#username').val());
sendWelcomeMessage($('#username').val());
$('#namePanel').hide();
$('#chatPanel').show();
};

我们从用户名输入元素的值中设置user标签的文本,将其传递给我们的sendWelcomeMessage方法(我们稍后会定义),并切换相关面板的显示。

  1. 定义sendMessage方法:
function sendMessage() {
let message = $('#message').val();
$('#message').val('');
let user = $('#user').text();
connection.invoke('Send', user, message);
};

我们从消息输入元素中设置message变量,然后清除它以便下一条消息使用,并从用户标签中设置user变量。然后我们使用connection.invoke方法调用服务器上的Send方法,并将我们的变量作为参数传递。

  1. 定义sendWelcomeMessage函数:
function sendWelcomeMessage(user) {
connection.invoke('Send','system',user+' joined the chat');
};

就像步骤 4 中描述的sendMessage函数一样,我们将使用connection.invoke函数调用服务器上的Send方法。不过这次我们将字符串'system'作为用户参数传递,以及有关刚刚加入的用户的一些信息性消息。

  1. 定义updateChat方法:
function updateChat(user, message) {
let chat = '<b>' + user + ':</b> ' + message + '<br/>'
$('#chat').append(chat);
if ($('#chat')["0"].innerText.length > 0) {
$('#historyPanel').show();
$('#archivePanel').show();
}
};

updateChat只是我们用来更新聊天历史面板的自定义函数。我们本可以在两个connection.on函数中内联执行这个操作,但这样就意味着我们会重复自己。在任何编码中,通常的规则是尽量避免重复代码。

在这个函数中,我们将chat变量设置为我们希望每条聊天历史记录的样式。在这种情况下,我们只是将我们的用户(带有冒号)加粗显示,然后消息不加样式,最后换行。几行聊天看起来会像这样:

  • John: 大家好

  • Sarah: 你好 John

  • server: Peter 加入了聊天

  • John: 你好 Sarah,你好 Peter

  • Peter: 大家好

我还检查了聊天 div 的innerText属性,以确定聊天历史和存档面板是否可见。

定义archiveChat函数:

function archiveChat() {
let message = $('#chat')["0"].innerText;
let archivePath = $('#archivePath').val();
connection.invoke('ArchiveChat', archivePath, message);
};

和其他一切一样,我尽量保持简单。我们获取聊天面板(div)的innerText和archivePath输入中指定的路径,然后将它们传递给服务器的ArchiveChat方法。

当然,这里我们有一个小错误的窗口:如果用户没有输入有效的文件保存路径,代码将抛出异常。我会留给你自己的创造力来解决这个问题。我只是在这里为了 SignalR 功能。

解决方案概述

现在你应该有一个完整的、可构建的解决方案。让我们快速查看一下解决方案资源管理器中的解决方案:

从头开始,让我列出我们对Chapter5项目所做的更改:

  1. 以下是我们通过 NuGet 添加的 SignalR Asp.NET Core 库:

Dependencies/NuGet/Microsoft.AspNetCore.SignalR (1.0.0-alpha2-final)

  1. 我们手动从node_modules文件夹中复制了这个 JavaScript 库,之后使用npm下载了它:

wwwroot/scripts/signalr-client-1.0.0-alpha2-final.min.js

  1. 我们的客户端页面包含了 HTML 标记、样式和 JavaScript:one.wwwroot/index.html

如果你要将这个应用程序作为基础并进行扩展,我建议将 JavaScript 代码移到一个单独的.js文件中。这样更容易管理,也是另一个良好的编码标准。

  1. Chat.cs:这是我们的聊天服务器代码,或者说是我们声明的任何自定义任务方法

  2. Startup.cs:这个文件在 Asp.NET Code web 应用程序中是标准的,但我们改变了配置以确保 SignalR 被添加为服务

  3. 让我们构建我们的项目。在 Visual Studio 的顶部菜单中,单击“构建”菜单按钮:

您可以选择构建整个解决方案,也可以选择单独的项目。鉴于我们的解决方案中只有一个项目,我们可以选择任何一个。您还可以使用键盘快捷键Ctrl + Shift + B。

您应该在输出窗口中看到一些(希望成功的)构建消息:

如果您遇到任何错误,请再次查看本章,看看您是否漏掉了什么。一个小刺可以引起很多不适。

展示和告知

是时候了。您已经创建了项目,添加了库,并编写了代码。现在让我们看看这个东西的表现。

运行应用程序

要运行应用程序,请按F5(或Ctrl + F5以无调试模式启动)。应用程序将在默认浏览器中打开,您应该看到这个:

等等。什么?我们一定是漏掉了什么。

现在我们只需通过将我们的 URL 更改为localhost:12709/index.html(只需检查您的端口号),我们就可以导航到 index.html 页面了。

相反,让我们将我们的index.html页面指定为默认启动页面。

在Startup.cs类的Configure方法中,在顶部添加这一行:

app.UseDefaultFiles();

有了这个小宝石,对wwwroot文件夹的任何请求(随时导航到您的网站)都将搜索以下之一:

  • default.htm

  • default.html

  • index.htm

  • index.html

找到的第一个文件将作为您的默认页面提供。太棒了!

现在让我们再次运行我们的应用程序:

即使我们的 URL 仍然不显示/index.html部分,我们的 Web 应用程序现在知道要提供哪个页面。现在我们可以开始聊天了。输入用户名并按Enter:

如您所见,我们的名称面板现在被隐藏,我们的聊天和存档面板正在显示。

我们的服务器还友好地通知我们加入了聊天,感谢我们的sendWelcomeMessage(user)函数。

每次我们发送消息,我们的聊天历史都会更新:

开始派对

只有多方参与,对话才是对话。所以让我们开始一个派对。

如果您在网络上发布应用程序,可以使用实际的网络客户端进行聊天,但我不在网络上(不是那个意思),所以我们使用另一个技巧。我们可以使用各种浏览器来代表我们不同的派对客人(网络客户端)。

复制您的应用程序 URL(再次检查端口号)并粘贴到其他几个浏览器中。

对于每个新客人(浏览器),您需要指定一个用户名。为了更容易跟踪,我将称我的其他客人为不同的浏览器名称。

当他们每个人进入聊天并开始发送消息时,您将看到我们的聊天历史增长:

您可以将浏览器平铺(或将它们移动到其他显示器,如果您有额外的显示器)以查看由一个人发送的消息立即传递给所有人的数量,这正是 SignalR 的全部意义所在。

我们从 Microsoft Edge 中的 John Doe 开始,所以我们将在那里继续:

Opera 是第一个加入派对的:

然后 Chrome 到达:

最后,Firefox 也加入了:

您还会注意到每个客人的聊天历史只有在他们加入聊天时才开始。这是有意设计的。我们不会在客户端加入时发送历史聊天记录。

存档聊天

要将聊天记录保存到文本文件中,请在archivePath输入元素中输入有效的本地文件夹路径,然后点击“存档聊天”按钮:

如前所述,我们尚未为我们的路径构建适当的验证,因此请确保使用有效路径进行测试。如果成功,您应该在聊天窗口中看到这样的消息:

system: Chat archived by John Doe

您还将在指定路径中找到新创建的文本文件,文件名采用ChatArchive_[date].txt的命名约定。

总结

正如本章所示,SignalR 非常容易实现。我们创建了一个聊天应用程序,但有许多应用程序可以从实时体验中受益。这些包括股票交易、社交媒体、多人游戏、拍卖、电子商务、财务报告和天气通知。

列表可以继续。即使实时数据的需求不是必需的,SignalR 仍然可以使任何应用程序受益,使节点之间的通信变得无缝。

浏览 Asp.NET SignalR 的 GitHub 页面(github.com/aspnet/SignalR),显然该库正在不断地进行改进和完善,这是个好消息。

随着对快速、相关和准确信息的需求变得更加关键,SignalR 是您团队中的重要成员。

第六章:使用 Entity Framework Core 的 Web 研究工具

“我对自己说的最大谎言是我不需要把它写下来,我会记住的。”

  • 未知

所以,你有几分钟时间来赶上你的动态。当你浏览时,你看到有人分享了一篇关于记住吉他和弦的新方法的文章。你真的想读它,但现在没有足够的时间。"我以后再读",你告诉自己,以后变成了永远。主要是因为你没有把它写下来。

现在有各种应用程序可以满足您保存链接以供以后使用的需求。但我们是开发人员。让我们写一些有趣的东西。

在本章中,我们将看到以下内容:

  • Entity Framework(EF)Core 历史

  • 代码优先与模型优先与数据库优先方法

  • 开发数据库设计

  • 设置项目

  • 安装 EF Core

  • 创建模型

  • 配置服务

  • 创建数据库

  • 使用测试数据填充数据库

  • 创建控制器

  • 运行应用程序

  • 部署应用程序

这是相当多的内容,但不要担心,我们会一步一步来。让我们散步一下。

Entity Framework(EF)Core 历史

开发应用程序时最令人沮丧的部分之一是尝试建立代码和数据库之间的通信层。

至少曾经是这样。

进入 Entity Framework

Entity Framework 是一个对象关系映射器(ORM)。它将您的.NET 代码对象映射到关系数据库实体。就是这么简单。现在,您不必担心为了处理普通的 CRUD 操作而搭建所需的数据访问代码。

当 Entity Framework 的第一个版本于 2008 年 8 月发布时,随着.NET 3.5 SP1 的发布,最初的反应并不是很好,以至于一群开发人员签署了一份关于该框架的不信任投票。幸运的是,大部分提出的问题得到了解决,随着 Entity Framework 4.0 的发布,以及.NET 4.0,许多关于框架稳定性的批评得到了解决。

微软随后决定使用.NET Core 使.NET 跨平台,这意味着 Entity Framework Core 进行了完全重写。显然,这有其利弊,因为 EF Core 和 EF6 之间的比较表明,虽然 EF Core 引入了新功能和改进,但它仍然是一个新的代码库,因此还没有 EF6 中的所有功能。

代码优先与模型优先与数据库优先方法

使用 Entity Framework,您可以选择三种实现方法,总是很好能够有选择。让我们快速看看它们之间的区别。

代码优先方法

对于硬核程序员来说,这是首选的方法,这种方法让您完全控制数据库,从代码开始。数据库被视为简单的存储位置,很可能不包含任何逻辑或业务规则。一切都由代码驱动,因此任何所需的更改也需要在代码中完成:

模型优先方法

如果您更喜欢绘画而不是诗歌,那么您可能更喜欢模型优先方法。在这种方法中,您创建或绘制您的模型,工作流将生成一个数据库脚本。如果有必要添加特定逻辑或业务规则,您还可以使用部分类扩展模型,但这可能会变得复杂,如果有太多具体内容,最好考虑代码优先方法:

数据库优先方法

数据库优先方法适用于需要从事设计和维护数据库的专职 DBA 的大型项目。Entity Framework 将根据数据库设计为您创建实体,并且您可以在数据库更改时运行模型更新:

开发数据库设计

在我们开始创建具有数据库、模型和控制器的解决方案之前,我们需要首先弄清楚我们想要如何设计数据库。

根据微软的 TechNet,有五个基本步骤可以遵循来规划数据库:

  1. 收集信息

  2. 识别对象

  3. 对对象建模

  4. 确定每个对象的信息类型

  5. 确定对象之间的关系

我们的要求非常简单。我们只需要保存一个网站链接以便以后导航,因此我们不会有多个对象之间的关系。

然而,我们需要澄清我们想要为对象(网站链接)保存的信息类型。显然,我们需要 URL,但我们还需要什么?确保您了解解决方案所需的信息以及如何使用它。

以日常术语来考虑——如果您为朋友的房子写地址,您可能希望除了街道之外还有一些东西,可能是您朋友的名字或某种备注。

在我们的解决方案中,我们想知道 URL 是什么,但我们还想知道我们何时保存它,并且有一个地方可以记录笔记,以便我们可以为条目添加更多个人细节。因此,我们的模型将包含以下内容:

  • URL

  • DateSaved

  • Notes

我们将在开始创建模型时详细介绍,但让我们不要急于行动。我们仍然需要创建我们的项目。

设置项目

使用 Visual Studio 2017,创建一个 ASP.NET Core Web 应用程序。请注意,我们将采用代码优先方法来进行此项目。

  1. 让我们将应用程序称为WebResearch。如下截图所示:

  1. 在下一个屏幕上,选择 Web 应用程序(模型-视图-控制器)作为项目模板。为了保持简单,将身份验证保持为无身份验证。参考以下截图:

  1. 创建的项目将如下所示:

安装所需的包

我们需要将三个 NuGet 包安装到我们的解决方案中,这将帮助我们完成我们的任务。这是通过包管理器控制台完成的。

转到工具 | NuGet 包管理器 | 包管理器控制台:

实体框架核心 SQL Server

EF Core 提供了各种数据库提供程序,包括 Microsoft SQL Server、PostgreSQL、SQLite 和 MySQL。我们将使用 SQL Server 作为数据库提供程序。

有关数据库提供程序的完整列表,请参阅官方微软文档:docs.microsoft.com/en-us/ef/core/providers/index。

在控制台窗口中,输入以下命令并按Enter:

    Install-Package Microsoft.EntityFrameworkCore.SqlServer  

您应该看到几行响应显示成功安装的项目。

实体框架核心工具

接下来,我们将安装一些实体框架核心工具,这些工具将帮助我们根据我们的模型创建数据库。

在控制台窗口中,输入以下命令并按Enter:

    Install-Package Microsoft.EntityFrameworkCore.Tools  

再次,您应该看到几行响应显示成功安装的项目。

代码生成设计

我们可以使用一些 ASP.Net Core 代码生成工具来帮助我们进行脚手架搭建,而不是自己编写所有代码。

接下来在控制台窗口中,输入以下命令并按Enter:

    Install-Package Microsoft.VisualStudio.Web.CodeGeneration.Design

像往常一样,检查一下是否获得了“成功安装”的项目。

如果安装任何 NuGet 包时出现问题,可能是访问控制问题。一般来说,我会将我的 Visual Studio 设置为以管理员身份运行,这样就可以解决大部分问题。

安装完成后,我们的解决方案将在“依赖项”部分反映出添加的 NuGet 包,如下所示:

创建模型

右键单击项目中的 Models 文件夹,添加一个名为ResearchModel.cs的类:

实际上,我们需要两个类——一个是Research类,它是我们entity对象的表示,另一个是ResearchContext,它是DbContext的子类。为了简化,我们可以将这两个类都放在我们的ResearchModel文件中。

这是代码:

using Microsoft.EntityFrameworkCore; 
using System; 

namespace WebResearch.Models 
{ 
    public class Research 
    { 
        public int Id { get; set; } 
        public string Url { get; set; } 
        public DateTime DateSaved { get; set; } 
        public string Note { get; set; } 
    } 

    public class ResearchContext : DbContext 
    { 
        public ResearchContext(DbContextOptions<ResearchContext> 
        options) : base(options) 
        { 
        } 

        public DbSet<Research> ResearchLinks { get; set; } 
    } 
} 

让我们分解如下:

首先,我们有我们的Research类,这是我们的entity对象表示。如前面的开发数据库设计部分所述,对于每个链接,我们将保存 URL、日期和备注。ID 字段是保存信息的数据库表的标准做法。

我们的第二个类ResearchContext是DbContext的子类。这个类将有一个以DbContextOptions为参数的空构造函数和一个用于我们数据集合的DbSet<TEntity>属性。

我可以在这里给您一个关于DbSet<Entity>的简要概述,但我宁愿让 Visual Studio 来帮助我们。如果您将鼠标悬停在DbSet上,您将得到一个信息弹出窗口,其中包含您需要了解的一切:

配置服务

在Startup.cs类中,在ConfigureServices方法中,添加以下代码的DbContext服务:

string connection = Configuration.GetConnectionString("LocalDBConnection"); 
services.AddDbContext<ResearchContext>(options => options.UseSqlServer(connection)); 

如您所见,我们从配置中设置了一个连接字符串变量,然后将其作为DbContext的SqlServer选项参数传递。

但是等等。LocalDBConnection是从哪里来的?我们还没有在配置中设置任何东西。现在还没有。让我们现在就搞定。

打开项目根目录中的appsettings.json文件:

默认情况下,您应该看到一个日志记录条目。在Logging部分之后添加您的ConnectionStrings部分,其中包含LocalDBConnection属性。

完整文件应该看起来像这样:

{ 
  "Logging": { 
    "IncludeScopes": false, 
    "LogLevel": { 
      "Default": "Warning" 
    } 
  }, 

  "ConnectionStrings": { 
    "LocalDBConnection": "Server=(localdb)\mssqllocaldb; 
     Database=WebResearch;  
     Trusted_Connection=True" 
  } 
} 

稍后,我们将看看如何连接到现有数据库,但现在我们只是连接到本地的db文件。

创建数据库

在任何应用程序的开发阶段,您的数据模型很有可能会发生变化。当这种情况发生时,您的 EF Core 模型与数据库架构不同,您必须删除过时的数据库,并根据更新后的模型创建一个新的数据库。

这都是一件有趣的事情,直到您完成了第一个实时实现,并且您的应用程序在生产环境中运行。那时,您不能去删除数据库来更改一些列。您必须确保在进行任何更改时,实时数据保持不变。

Entity Framework Core Migrations 是一个很棒的功能,它使我们能够对数据库架构进行更改,而不是重新创建数据库并丢失生产数据。Migrations具有很多功能和灵活性,这是一个值得花时间的话题,但现在我们只涵盖一些基础知识。

我们可以在Package Manager Console中使用 EF Core Migration 命令来设置、创建,并在需要时更新我们的数据库。

在Package Manager Console中,我们将执行以下两个命令:

  1. Add-Migration InitialCreate

  2. Update-Database

第一条命令将在项目的Migrations文件夹中生成用于创建数据库的代码。这些文件的命名约定是<timestamp>_InitialCreate.cs。

第二条命令将创建数据库并运行Migrations:

在InitialCreate类中有Note的两种方法,Up和Down。简单地说,Up方法代码在升级应用程序时执行,Down方法代码在降级应用程序时运行。

假设我们想要向我们的Research模型添加一个名为Read的布尔属性。为了持久化该值,我们显然需要将该列添加到我们的表中,但我们不希望删除表来添加字段。使用Migrations,我们可以更新表而不是重新创建它。

我们将从修改我们的模型开始。在Research类中,添加Read属性。我们的类将如下所示:

public class Research 
{ 
    public int Id { get; set; } 
    public string Url { get; set; } 
    public DateTime DateSaved { get; set; } 
    public string Note { get; set; } 
    public bool Read { get; set; } 
} 

接下来,我们将添加一个Migration。我们将使用Migration名称来指示我们正在做什么。在Package Manager Console中执行以下命令:

    Add-Migration AddReseachRead

您会注意到我们的Migrations文件夹中有一个新的类:

让我们来看看底层。您会看到我们的Up和Down方法并不像InitialCreate类中那样为空:

如前所述,Up方法在升级期间执行,Down方法在降级期间执行。现在我们可以看到代码,这个概念更清晰了。在Up方法中,我们正在添加Read列,在Down方法中,我们正在删除该列。

如果需要,我们可以对这段代码进行更改。例如,我们可以更改Read列的nullable属性,但更新代码如下所示:

protected override void Up(MigrationBuilder migrationBuilder) 
{ 
    migrationBuilder.AddColumn<bool>( 
        name: "Read", 
        table: "ResearchLinks", 
        nullable: true, 
        defaultValue: false); 
} 

我们还可以添加一个自定义的 SQL 查询,将所有现有条目更新为Read:

migrationBuilder.Sql( 
    @" 
        UPDATE Research 
        SET Read = 'true'; 
    "); 

我知道这不是一个很好的例子,因为你不希望每次更新数据库时都将所有的Research条目标记为Read,但希望你能理解这个概念。

但是,这段代码尚未执行。因此,当前时刻,我们的模型和数据库架构仍然不同步。

再次执行以下命令,我们就更新完毕了:

    Update-Database

用测试数据填充数据库

现在我们有一个空数据库,让我们用一些测试数据填充它。为此,我们需要创建一个在数据库创建后调用的方法:

  1. 在项目中创建一个名为Data的文件夹。在文件夹中,添加一个名为DbInitializer.cs的类:

该类有一个Initialize方法,该方法以我们的ResearchContext作为参数:

public static void Initialize(ResearchContext context) 
  1. 在Initialize方法中,我们首先调用Database.EnsureCreated方法,确保数据库存在并在不存在时创建它:
context.Database.EnsureCreated(); 
  1. 接下来,我们进行一个快速的Linq查询,检查ResearchLinks表是否有任何记录。论点是,如果表为空,我们希望添加一些测试数据:
if (!context.ResearchLinks.Any()) 
  1. 然后,我们创建一个Research模型的数组,并添加一些测试条目。URL 可以是任何你喜欢的东西。我只是选择了一些最常见的网站:
var researchLinks = new Research[] 
{ 
 new Research{Url="www.google.com", DateSaved=DateTime.Now, 
  Note="Generated Data", Read=false}, 
       new Research{Url="www.twitter.com", DateSaved=DateTime.Now,  
  Note="Generated Data", Read=false}, 
       new Research{Url="www.facebook.com", DateSaved=DateTime.Now, 
  Note="Generated Data", Read=false}, 
       new Research{Url="www.packtpub.com", DateSaved=DateTime.Now, 
  Note="Generated Data", Read=false}, 
       new Research{Url="www.linkedin.com", DateSaved=DateTime.Now,  
  Note="Generated Data", Read=false}, 
}; 
  1. 填充了我们的数组后,我们循环遍历它,并将条目添加到我们的上下文中,最后调用SaveChanges方法将数据持久化到数据库中:
foreach (Research research in researchLinks) 
{ 
 context.ResearchLinks.Add(research); 
} 
 context.SaveChanges();
  1. 将所有内容放在一起如下所示:
using System; 
using System.Linq; 
using WebResearch.Models; 

namespace WebResearch.Data 
{ 
    public static class DbInitializer 
    { 
        public static void Initialize(ResearchContext context) 
        { 
            context.Database.EnsureCreated(); 

            if (!context.ResearchLinks.Any()) 
            { 
                var researchLinks = new Research[] 
                { 
                    new Research{Url="www.google.com", 
                     DateSaved=DateTime.Now, Note="Generated Data", 
                      Read=false}, 
                    new Research{Url="www.twitter.com", 
                      DateSaved=DateTime.Now, Note="Generated
                      Data", 
                       Read=false}, 
                    new Research{Url="www.facebook.com", 
                     DateSaved=DateTime.Now, Note="Generated Data", 
                      Read=false}, 
                    new Research{Url="www.packtpub.com", 
                     DateSaved=DateTime.Now, Note="Generated Data", 
                      Read=false}, 
                    new Research{Url="www.linkedin.com", 
                     DateSaved=DateTime.Now, Note="Generated Data", 
                      Read=false}, 
                }; 
                foreach (Research research in researchLinks) 
                { 
                    context.ResearchLinks.Add(research); 
                } 
                context.SaveChanges(); 
            } 
        } 
    } 
} 

创建控制器

控制器是 ASP.NET Core MVC 应用程序构建的基本构件。控制器内的方法称为操作。因此,我们可以说控制器定义了一组操作。这些操作处理请求,这些请求通过路由映射到特定的操作。

要了解有关控制器和操作的更多信息,请参阅 Microsoft 文档:docs.microsoft.com/en-us/aspnet/core/mvc/controllers/actions。要了解有关路由的更多信息,请参阅 Microsoft 文档:docs.microsoft.com/en-us/aspnet/core/mvc/controllers/routing。按照以下步骤:

  1. 右键单击 Controllers 文件夹,然后选择添加|控制器。

  2. 在脚手架屏幕上,选择使用 Entity Framework 和单击添加的 MVC 控制器视图:

  1. 在下一个屏幕上,选择我们的 Research 模型作为Model类,ResearchContext 作为Data上下文类。你可以将其余部分保持不变,除非你想要更改控制器名称:

简要查看创建的控制器,我们现在已经有了基本的创建、读取、更新和删除(CRUD)任务。现在,是主要事件的时候了。

运行应用程序

在我们开始运行应用程序之前,让我们确保我们的新页面很容易访问。最简单的方法就是将它设置为默认主页:

  1. 看一下Startup.cs中的Configure方法。你会注意到默认路由被指定为Home控制器。

  2. 简单地将控制器更改为你的Research控制器如下:

app.UseMvc(routes => 
{ 
    routes.MapRoute( 
        name: "default", 
        template: "{controller=Researches}/{action=Index}/{id?}"); 
});
  1. 最后,确保你的Main方法如下所示:
public static void Main(string[] args)
{
  var host = BuildWebHost(args);
  using (var scope = host.Services.CreateScope())
  {
    var services = scope.ServiceProvider;
    try
    {
      var context = services.GetRequiredService<ResearchContext>();
      DbInitializer.Initialize(context);
    }
    catch (Exception ex)
    {
      var logger = services.GetRequiredService<ILogger<Program>>
       ();logger.LogError(ex, "An error occurred while seeding the 
        database.");
    }
  }host.Run();
}
  1. 现在,按下Ctrl + F5来运行应用程序,看看你的劳动成果:

  1. 如你所见,我们的测试条目可以供我们使用。让我们快速看一下可用的功能:
  • 点击“创建新”以查看我们链接的条目表单:

  1. 输入一些有趣的数据,然后点击“创建”按钮。你将被重定向回列表视图,并看到我们的新条目被添加到列表底部:

在每个项目旁边,你可以选择编辑、详情或删除。随便玩玩这些功能。有很多可以做来改善用户体验,比如自动填写日期字段。我将把改善用户体验的创意留给你自己来完成。

部署应用程序

一旦你的应用程序准备部署,你可以使用一些可用的选项:

  1. Microsoft Azure 应用服务

  2. 自定义目标(IIS、FTP)

  3. 文件系统

  4. 导入配置文件

在 Visual Studio 的“构建”菜单项下,点击“发布 WebResearch”(或者你决定给你的项目起的名字):

你将看到一个屏幕显示可用的发布选项。让我们仔细看一下。

Microsoft Azure 应用服务

Microsoft Azure 负责创建和维护 Web 应用程序所需的所有基础设施。这意味着我们开发人员不需要担心诸如服务器管理、负载平衡或安全性等问题。随着平台几乎每天都在改进和扩展,我们也可以相当有信心地认为我们将拥有最新和最好的功能。

我们不会详细介绍 Azure 应用服务,因为它本身可以成为一本书,但我们当然可以看一下将我们的 Web 应用程序发布到这个云平台所需的步骤:

  1. 选择 Microsoft Azure 应用服务作为你的发布目标。如果你有一个现有的站点需要发布,你可以选择“选择现有”。现在,我假设你需要“创建新”:

  1. 点击“确定”按钮后,Visual Studio 将使用你登录的 Microsoft 账户联系 Azure,然后 Azure 将检查你是否有 Azure 账户,并返回可用的服务详情。

我为这个蓝图创建了一个试用账户,没有事先设置具体细节,正如你从下面的截图中看到的,Azure 会为你推荐一个可用的应用名称和应用服务计划。

  1. 资源组是可选的,如果你没有指定任何内容,它将获得一个唯一的组名:

  1. 你可以在“更改类型”选项下更改要发布的应用程序类型。在我们的情况下,我们显然会选择 Web 应用程序:

  1. 点击左侧的“服务”以查看将与你的发布一起设置的服务。

第一个框显示了您的应用程序可能受益的任何推荐资源类型。在我们的情况下,推荐了一个 SQL 数据库,我们确实需要它,因此我们将通过单击添加(+)按钮来简单地添加它:

Azure 将负责 SQL 安装,但我们需要提供所需的信息,例如如果您已经在您的配置文件中有一个服务器,则使用哪个服务器,或者如果您还没有,则创建一个新的服务器。

  1. 在这种情况下,我们将配置一个新的 SQL 服务器。单击 SQL 服务器下拉菜单旁边的新按钮以打开配置 SQL 服务器表单。Azure 将为服务器提供一个推荐的名称。虽然您可以提供自己的名称,但服务器名称很可能不可用,因此我建议您只使用他们推荐的名称。

  2. 为服务器提供管理员用户名和管理员密码,然后点击确定:

  1. 这样做将带您回到配置 SQL 数据库表单,在那里您需要指定数据库名称以及连接字符串名称:

  1. 再次查看创建应用服务表单。您会注意到 SQL 数据库已添加到您选择和配置的资源部分:

  1. 现在我们可以返回到托管选项卡,它将向您显示单击创建按钮时会发生什么的概述。

  2. 如下图所示,将创建以下三个 Azure 资源:

  3. 应用服务

  4. 应用服务计划

  5. SQL 服务器

  1. 创建后,我们可以通过单击发布按钮将其发布到我们的新 Azure 配置文件。

  2. 您将在输出窗口中看到一些构建消息,并最终会得到以下结果:

   Publish Succeeded.
   Web App was published successfully 
   http://webresearch20180215095720.azurewebsites.net/
   ========== Build: 1 succeeded, 0 failed, 0 up-to-date, 0 skipped 
   ==========
   ========== Publish: 1 succeeded, 0 failed, 0 skipped ==========

  1. 您可以查看 Azure 门户上的仪表板(portal.azure.com),该仪表板将显示由于我们的服务创建而启用在您的帐户上的资源:

  1. 发布的应用程序将在浏览器中打开,您很可能会看到错误消息。默认情况下,您不会看到有关错误的详细信息,但至少 Azure 会通过将您的ASPNETCORE_ENVIRONMENT环境变量设置为Development并重新启动应用程序来提供一些指针以获取错误详细信息:

  1. 当您登录到 Azure 门户时,可以导航到您的应用服务,然后在应用程序设置中,添加值为Development的 ASPNETCORE_ENVIRONMENT 设置,并重新启动您的应用:

  1. 现在,我们可以刷新网站,我们应该看到关于底层错误的更多细节:

  1. 啊,是的!我们仍然指向我们的本地数据库,并且我们无法从发布环境访问它。让我们更新我们的appsettings.json指向我们的 Azure 数据库。

  2. 导航到 Azure 仪表板上的 SQL 服务器,然后到属性。在右侧窗格上,您应该会看到一个显示数据库连接字符串的选项:

  1. 复制 ADO.NET 连接字符串,返回到您的代码,并在appsettings.json文件中更新 CONNECTION STRINGS 条目。

  2. 重新发布应用程序,然后您应该可以开始了。

自定义目标

下一个发布选项通常称为自定义目标。

此选项基本上包括任何不是 Azure 或本地文件系统的内容。单击确定按钮后,您可以选择发布方法:

有四种发布方法或自定义目标,每种方法都有自己的要求:

  1. FTP

  2. Web 部署

  3. Web 部署包

  4. 文件系统

我们还有一个设置选项卡,适用于所有四种方法。让我们快速看看那里的选项:

配置选项可以设置为 Debug 或 Release。

使用 Debug,您生成的文件是可调试的,这意味着可以命中指定的断点。但这也意味着性能会下降。

使用 Release,您将无法实时调试,但由于应用程序已完全优化,性能将有所提高。

在我们的情况下,唯一可用的目标框架是netcoreapp2.0,但在标准.NET 应用程序中,这是您可以将目标设置为.NET 3.5 或.NET 4.5,或者其他可用的地方。

然后,您还可以指定目标运行时,选择让 Visual Studio 清理目标文件夹,并为运行时指定连接字符串。

如前所述,这些设置适用于所有四种发布方法,我们现在将看一下。

FTP

FTP 发布方法使您能够发布到托管的 FTP 位置。对于此选项,您需要提供以下内容:

  • 服务器 URL

  • 站点路径

  • 用户名

  • 密码

  • 目标 URL

它还允许您验证从输入的详细信息的连接:

Web Deploy

看看 Web Deploy 和 FTP 的形式,您可能会原谅自己认为它们是同一回事。嗯,两者都基本上会导致同样的结果,即直接发布到托管站点,但是使用 Web Deploy,您将获得一些额外的好处,包括以下内容:

  • Web Deploy 会将源与目标进行比较,并仅同步所需的更改,从而大大减少了与 FTP 相比的发布时间

  • 即使 FTP 也有其安全的表亲 SFTP 和 FTPS,Web Deploy 始终支持安全传输

  • 适当的数据库支持,使您能够在同步过程中应用 SQL 脚本

发布屏幕如下所示:

Web Deploy Package

Web Deploy Package 选项用于创建部署包,您可以在之后选择的任何位置安装您的应用程序。请参考以下屏幕截图:

文件系统

被全球老派开发人员使用,主要是因为我们仍然不太信任一些可用工具,此选项允许您发布到您选择的文件夹位置,然后手动将其复制到发布环境:

文件夹

只是为了向您展示开发人员仍然控制发布代码的流行程度,我们有两条路径最终都会发布到文件夹位置。

再次,只需指定文件夹位置,然后点击“确定”:

导入配置文件

导入配置文件方法不是实际的发布方法,而是一个简单的选项,用于导入先前保存的配置文件,可以是从备份中导入,也可以用于在开发团队之间共享发布配置文件:

总结

在本章中,我们在 Entity Framework Core 领域进行了一次引导式的导览。我们从博物馆开始,了解了 Entity Framework 的历史,然后访问学区,讨论了 Code-First、Model-First 和 Database-First 实现方法之间的一些区别。甚至还有 TechNet 的快速访问,提供了一些关于设计数据库的想法。

之后,我们花了一些时间构建自己的 EF Core 解决方案,并研究了部署应用程序的各种方式。我们还研究了如何用一些测试数据填充我们的新建筑,以查看一旦向公众开放,它将如何保持稳定。

导览结束时,我们参观了分发区,以了解可用的部署选项。

这次访问时间太短,无法涵盖 Entity Framework Core 世界中所有可用和可能的内容,因为它是一个拥有庞大社区不断努力改进和扩展其功能的框架。

了解开发社区不满足于任何平庸,不断努力改进和扩展功能,比如 Entity Framework,尽管它似乎已经非常成熟和广泛。

第七章:一个无服务器的电子邮件验证 Azure 函数

本章将带我们进入无服务器计算的领域。我听到你问无服务器计算到底是什么?事实上,一旦你理解了“无服务器计算”这个术语与缺乏服务器无关的概念,答案就非常简单了。事实上恰恰相反。

在本章中,我们将看一下:

  • 创建 Azure 函数

  • 在浏览器中测试您的 Azure 函数

  • 从 ASP.NET Core MVC 应用程序调用 Azure 函数

我们将创建一个简单的 Azure 函数,使用正则表达式来验证电子邮件地址。您需要记住 Azure 函数是云中的小代码片段。不要把它们看作复杂代码的大部分。越小越好。

从无服务器计算开始

传统上,公司花费时间和金钱来管理服务器的计算资源。这些代表了公司的固定和重复成本。无论服务器是空闲还是正在执行某种计算任务,都会产生费用。底线是,它只是因为存在而花费了金钱。

使用无服务器计算,计算资源是可扩展的云服务。这意味着它是一个事件驱动的应用程序设计。基本上,使用无服务器计算,您只支付您使用的部分。这对 Azure 函数也是如此。

Azure 函数是驻留在云中的小代码片段。您的应用程序可以根据需要简单地使用这些函数,您只需支付所使用的计算能力。无论是一个人还是一百万人访问您的应用程序都无所谓。Azure 函数将自动扩展以处理额外的负载。当您的应用程序的使用量下降时,Azure 函数会自动缩小规模。

无服务器计算的重要性

想象一下,您的应用程序使用频繁(但不是持续)出现峰值。因为处理来自您的应用程序的请求的服务器不是无服务器的,它需要升级(作为您或您的公司的成本)以处理额外的负载。在低使用率时,服务器并没有更少的资源。您升级它以处理特定的用户负载。它将始终以这个性能水平运行,正如您所知,性能是有代价的。

使用无服务器计算,资源会随着需求的增加和减少而自动扩展和缩小。这是一种更有效的使用服务器的方式,因为您不必为未充分利用的计算能力付费。

Azure 函数的特性

Azure 函数为开发人员提供了丰富的功能。请参考微软文档,了解更多关于 Azure 函数的信息-docs.microsoft.com/en-us/azure/azure-functions/。现在,我们将看一下其中的一些功能。

语言选择

Azure 函数的好处是您可以使用自己选择的语言创建它们。有关支持的语言列表,请浏览以下网址:

docs.microsoft.com/en-us/azure/azure-functions/supported-languages。

在本章中,我们将使用 C#编写 Azure 函数。

按使用付费

如前所述,您只需支付 Azure 函数运行的实际时间。按秒计费的消耗计划。微软在以下网址上有一份关于 Azure 函数定价的文档:

azure.microsoft.com/en-us/pricing/details/functions/。

灵活的开发

您可以直接在 Azure 门户中创建 Azure 函数。您还可以使用 Visual Studio Team Services 和 GitHub 设置持续集成。

我可以创建什么类型的 Azure 函数?

您可以使用 Azure 函数作为集成解决方案,处理数据,与物联网,API 和微服务一起工作。Azure 函数还可以很好地触发,因此您甚至可以安排任务。这些是提供给您的一些 Azure 函数模板:

  • HTTPTrigger

  • TimerTrigger

  • GitHub webhook

  • Generic webhook

  • BlobTrigger

  • CosmosDBTrigger

  • QueueTrigger

  • EventHubTrigger

  • ServiceBusQueueTrigger

  • ServiceBusTopicTrigger

要了解有关这些模板和 Azure 函数的更多信息,请阅读微软文档Azure 函数简介,网址如下:

docs.microsoft.com/en-us/azure/azure-functions/functions-overview。

创建 Azure 函数

让我们毫不拖延地创建我们自己的 Azure 函数。我们要创建的函数将使用正则表达式验证电子邮件地址。这是一个非常标准的开发任务。它也是一个将在许多应用程序中广泛使用的功能:

您需要拥有 Azure 帐户。如果没有,您可以在azure.microsoft.com/en-us/free/上设置免费试用帐户。

  1. 将浏览器指向portal.azure.com并登录到您的 Azure 门户。

  2. 登录后,寻找“创建资源”链接。单击该链接,然后在 Azure Marketplace 部分下查找“计算”链接。请参考以下屏幕截图:

  1. 在“特色”部分下方,您将看到“函数应用”作为一个选项。单击该链接:

  1. 现在,您将看到“函数应用设置”屏幕。需要输入以下选项:
  • 应用名称:这是您的 Azure 函数的全局唯一名称。

  • 订阅:这是您的函数将在其中创建的订阅。

  • 资源组:为您的函数创建一个新的资源组。

  • 操作系统:您可以选择 Windows 或 Linux。我选择了 Windows。

  • 托管计划:这将定义资源如何分配给您的函数。

  • 位置:最好选择地理位置最接近您的位置。

  • 存储:保持默认设置。

  1. 您还可以选择将应用程序洞察切换到打开或关闭状态。您还可以选择“固定到仪表板”选项。

我们称之为 Azure 函数核心邮件验证。

  1. 添加所有必需的设置后,单击“创建”按钮。

  1. 单击“创建”按钮后,您将看到一个“正在验证...”的消息。这可能需要几秒钟时间!

  2. 请注意 Azure 门户右上角的通知部分(小铃铛图标)。新通知将显示在那里,并以数字表示未读通知的数量!

  3. 如果单击通知,您将看到 Azure 正在部署您创建的 Azure 函数的进度!

  4. 当部署您的 Azure 函数时,您将在“通知”部分看到“部署成功”消息。从那里,您可以单击“固定到仪表板”或“转到资源”按钮。

将您的函数固定到仪表板只是为了以后更容易访问它。将经常使用的服务固定到仪表板是一个好主意。

  1. 要访问您的 Azure 函数,请单击“转到资源”按钮:

  1. 然后,您将进入 Azure 门户的“函数应用”部分。您将在“函数应用”部分下看到“核心邮件验证”函数:

  1. 在“core-email-validation”下,单击“函数”选项。然后,在右侧面板中单击“新建函数”选项。

  1. 现在,您将看到一系列可以帮助您入门的模板。向下滚动以查看所有可用的模板(不仅仅是以下截图中显示的四个):

  1. 我们不会浏览所有可用的模板。我们将保持简单,只选择“转到快速入门”选项,如下截图所示:

  1. 对于我们的目的,我们将简单地选择“Webhook + API”,并选择“C#”作为我们的语言。还有其他可供选择的语言,因此请选择您最熟悉的语言。

  2. 要创建该函数,请单击“创建此函数”按钮:

  1. 已创建 Azure 函数,并为您自动添加了一些样板代码,以便您了解函数内部代码的外观。所有这些代码所做的就是在查询字符串中查找名为name的变量,并在找到时在浏览器中显示它:
      using System.Net; 
      public static async Task<HttpResponseMessage> 
       Run(HttpRequestMessage req, TraceWriter log) 
      { 
        log.Info("C# HTTP trigger function processed a request."); 

        // parse query parameter 
        string name = req.GetQueryNameValuePairs() 
        .FirstOrDefault(q => string.Compare(q.Key, "name", true) == 0) 
        .Value; 

        if (name == null) 
        { 
          // Get request body 
          dynamic data = await req.Content.ReadAsAsync<object>(); 
          name = data?.name; 
        } 

        return name == null 
        ? req.CreateResponse(HttpStatusCode.BadRequest,
        "Please pass a name on the query string or in the request body") 
          : req.CreateResponse(HttpStatusCode.OK, "Hello " + name); 
      }  
  1. 查看屏幕右上角。您将看到一个“</>获取函数 URL”链接。单击以下链接:

  1. 这将显示一个弹出屏幕,其中包含访问您刚创建的 Azure 函数的 URL。单击“复制”按钮将 URL 复制到剪贴板:

  1. 您复制的 URL 将如下所示:
https://core-mail-validation.azurewebsites.net/api/HttpTriggerCSharp1?code=/IS4OJ3T46quiRzUJTxaGFenTeIVXyyOdtBFGasW9dUZ0snmoQfWoQ== 
  1. 要运行我们的函数,我们需要在 URL 的查询字符串中添加一个name参数。继续在 URL 中添加&name==[YOUR_NAME],其中[YOUR_NAME]是您自己的名字。在我的情况下,我在 URL 的末尾添加了&name=Dirk:
https://core-mail-validation.azurewebsites.net/api/HttpTriggerCSharp1?code=/IS4OJ3T46quiRzUJTxaGFenTeIVXyyOdtBFGasW9dUZ0snmoQfWoQ==&name=Dirk
  1. 将此 URL 粘贴到浏览器地址栏中,然后点击返回按钮。浏览器中将显示一条消息(在我的情况下)“Hello Dirk”:

请注意,在 Chrome 和 Firefox 中,您可能会看到消息“此 XML 文件似乎没有与其关联的任何样式信息”。要查看输出,请使用 Microsoft Edge。

  1. 回到 Azure 门户,在 Azure 函数屏幕的底部,您将看到“日志”窗口。如果没有显示,请单击“Λ”箭头展开面板。在这里,您将看到 Azure 触发器已成功运行:

恭喜,您刚刚运行了新的 Azure 函数。

修改 Azure 函数代码

虽然这一切都很令人兴奋(应该是的,这真是很酷的技术),但我们需要对 Azure 函数进行一些更改以满足我们的要求:

  1. 在 Azure 函数中找到return语句。它将如下所示:
      return name == null 
        ? req.CreateResponse(HttpStatusCode.BadRequest,
         "Please pass a name on the query string or in the request 
          body") 
        : req.CreateResponse(HttpStatusCode.OK, "Hello " + name); 

让我们简化一下代码,如果电子邮件地址不为空,只需返回true。将return语句替换为以下代码:

      if (email == null) 
      { 
        return req.CreateResponse(HttpStatusCode.BadRequest,
         "Please pass an email address on the query string or
          in the request body"); 
      } 
      else 
      { 
        bool blnValidEmail = false; 
        if (email.Length > 0) 
        { 
            blnValidEmail = true; 
        } 

        return req.CreateResponse(HttpStatusCode.OK,
         "Email status: " + blnValidEmail); 
      } 
  1. 您的 Azure 函数中的代码现在应该如下所示:
      using System.Net; 

      public static async Task<HttpResponseMessage>
       Run(HttpRequestMessage req, TraceWriter log) 
      { 
        log.Info("C# HTTP trigger function processed a new email 
         validation request."); 

        // parse query parameter 
        string email = req.GetQueryNameValuePairs() 
          .FirstOrDefault(q => string.Compare(q.Key, "email", true) == 
          0) 
          .Value; 

        if (email == null) 
        { 
          // Get request body 
          dynamic data = await req.Content.ReadAsAsync<object>(); 
          email = data?.email; 
        } 

        if (email == null) 
        { 
          return req.CreateResponse(HttpStatusCode.BadRequest,
           "Please pass an email address on the query string or
            in the request body"); 
        } 
        else 
        { 
          bool blnValidEmail = false; 
          if (email.Length > 0) 
          { 
            blnValidEmail = true; 
          } 

          return req.CreateResponse(HttpStatusCode.OK,
           "Email status: " + blnValidEmail); 
        }    

      }
  1. 确保单击“保存”按钮以保存对 Azure 函数的更改。然后,您将看到函数已编译,并在“日志”窗口中显示“编译成功”消息:

  1. 与以前一样,通过单击</>获取函数 URL 链接来复制 URL:
https://core-mail-validation.azurewebsites.net/api/HttpTriggerCSharp1?code=/IS4OJ3T46quiRzUJTxaGFenTeIVXyyOdtBFGasW9dUZ0snmoQfWoQ==

不过,这次我们要将其作为电子邮件地址传递。您可以看到参数名称已更改为email,并且值可以是您选择输入的任何电子邮件地址。因此,我在 URL 的末尾添加了&email=dirk@email.com:

https://core-mail-validation.azurewebsites.net/api/HttpTriggerCSharp1?code=/IS4OJ3T46quiRzUJTxaGFenTeIVXyyOdtBFGasW9dUZ0snmoQfWoQ==&email=dirk@email.com
  1. 将 URL 粘贴到浏览器中,然后点击返回按钮,以在浏览器中查看结果显示:

  1. 我们现在有信心 Azure Function 正在对我们的电子邮件地址进行基本验证(即使只是检查它是否存在)。然而,我们需要函数做更多的事情。为了验证电子邮件地址,我们将使用正则表达式。为此,将以下命名空间添加到 Azure Function 中:
      using System.Text.RegularExpressions; 

在进行验证的代码部分,输入代码来匹配电子邮件与正则表达式模式。

互联网上有成千上万种不同的正则表达式模式。正则表达式是一个完全不同的话题,超出了本书的范围。如果您的应用程序需要匹配文本模式,可以搜索一下,看看是否有可用的正则表达式模式。如果你真的很勇敢,你可以自己写。

  1. 正则表达式已经内置到.NET Framework 中,代码非常简单:
blnValidEmail = Regex.IsMatch(email, 
                @"^(?("")("".+?(?<!\)""@)|((0-9a-z)|[-!#$%&'*+/=?^`{}|~w])*)(?<=[0-9a-z])@))" + 
                @"(?([)([(d{1,3}.){3}d{1,3}])|(([0-9a-z][-0-9a-z]*[0-9a-z]*.)+[a-z0-9][-a-z0-9]{0,22}[a-z0-9]))$", 
                RegexOptions.IgnoreCase, TimeSpan.FromMilliseconds(250)); 
  1. 在添加了所有代码之后,您的 Azure Function 将如下所示:
      using System.Net; 
      using System.Text.RegularExpressions; 

      public static async Task<HttpResponseMessage>
       Run(HttpRequestMessage req, TraceWriter log) 
      { 
        log.Info("C# HTTP trigger function processed a new email 
         validation request."); 

        // parse query parameter 
        string email = req.GetQueryNameValuePairs() 
          .FirstOrDefault(q => string.Compare(q.Key, "email", true) == 
           0) 
          .Value; 

        if (email == null) 
        { 
          // Get request body 
          dynamic data = await req.Content.ReadAsAsync<object>(); 
          email = data?.email; 
        } 

        if (email == null) 
        { 
          return req.CreateResponse(HttpStatusCode.BadRequest,
          "Please pass an email address on the query string or in
           the request body"); 
        } 
        else 
        { 
          bool blnValidEmail = false; 

          blnValidEmail = Regex.IsMatch(email, 
                @"^(?("")("".+?(?<!\)""@)|((0-9a-z)|
                [-!#$%&'*+/=?^`{}|~w])*)(?<=[0-9a-z])@))" + 
                @"(?([)([(d{1,3}.){3}d{1,3}])|(([0-9a-z][-0-9a-z]*
                [0-9a-z]*.)+[a-z0-9][-a-z0-9]{0,22}[a-z0-9]))$", 
                RegexOptions.IgnoreCase, 
                TimeSpan.FromMilliseconds(250)); 

          return req.CreateResponse(HttpStatusCode.OK,
          "Email status: " + blnValidEmail); 
        }    

      } 
  1. 使用之前复制的相同 URL 粘贴到浏览器窗口中,然后点击返回或输入键:
https://core-mail-validation.azurewebsites.net/api/HttpTriggerCSharp1?code=/IS4OJ3T46quiRzUJTxaGFenTeIVXyyOdtBFGasW9dUZ0snmoQfWoQ==&email=dirk@email.com
  1. 电子邮件地址dirk@email.com已经验证,并且在浏览器中显示了消息“电子邮件状态:True”。这里发生的是电子邮件地址被传递给 Azure Function。然后函数从查询字符串中读取email参数的值,并将其传递给正则表达式。

电子邮件地址与正则表达式模式匹配,如果找到匹配,则认为电子邮件地址是有效的:

  1. 让我们将相同的 URL 输入到浏览器中,只是这次输入一个你知道将是无效的电子邮件地址。例如,电子邮件地址只能包含一个@符号。然后我添加到 URL 的参数如下:
https://core-mail-validation.azurewebsites.net/api/HttpTriggerCSharp1?code=/IS4OJ3T46quiRzUJTxaGFenTeIVXyyOdtBFGasW9dUZ0snmoQfWoQ==&email=dirk@@email.com

然后您可以看到,当我们点击返回或输入键时,无效的电子邮件地址dirk@@email.com被验证,并且不匹配正则表达式。因此在浏览器中显示文本“电子邮件状态:False”:

太棒了!我们已经看到我们创建的 Azure Function 使用了我们添加的正则表达式来验证它接收到的电子邮件地址。根据正则表达式验证的结果,函数返回 true 或 false。

最后,在继续之前,我们希望 Azure Function 返回一个单一的True或False值给调用应用程序。修改函数的return语句来实现这一点:

  return req.CreateResponse(HttpStatusCode.OK, blnValidEmail); 

我们已经看到了这个函数是如何工作的,通过逐步修改代码并直接从浏览器窗口运行。然而,除非我们可以从应用程序调用这个 Azure Function,否则这对我们没有任何好处。

让我们看看如何创建一个 ASP.NET Core MVC 应用程序,调用我们的 Azure Function 来验证在登录屏幕上输入的电子邮件地址。

从 ASP.NET Core MVC 应用程序调用 Azure Function

在上一节中,我们看了一下我们的 Azure Function 是如何工作的。现在,我们想创建一个 ASP.NET Core MVC 应用程序,将调用我们的 Azure Function 来验证应用程序登录屏幕中输入的电子邮件地址:

这个应用程序根本不进行任何身份验证。它所做的只是验证输入的电子邮件地址。ASP.NET Core MVC 身份验证是一个完全不同的话题,不是本章的重点。

  1. 在 Visual Studio 2017 中,创建一个新项目,并从项目模板中选择 ASP.NET Core Web 应用程序。单击“确定”按钮创建项目。如下截图所示:

  1. 在下一个屏幕上,确保从表单的下拉选项中选择.NET Core 和 ASP.NET Core 2.0。选择 Web 应用程序(模型-视图-控制器)作为要创建的应用程序类型。

不要费心进行任何身份验证或启用 Docker 支持。只需单击“确定”按钮创建项目:

  1. 创建项目后,您将在 Visual Studio 的解决方案资源管理器中看到熟悉的项目结构:

创建登录表单

在接下来的部分中,我们可以创建一个简单的普通登录表单。为了有点乐趣,让我们稍微调整一下。在互联网上寻找一些免费的登录表单模板:

  1. 我决定使用一个名为colorlib的网站,该网站在最近的博客文章中提供了 50 个免费的 HTML5 和 CSS3 登录表单。文章的网址是:colorlib.com/wp/html5-and-css3-login-forms/。

  2. 我决定使用Colorlib网站上的Login Form 1。将模板下载到您的计算机并解压缩 ZIP 文件。在解压缩的 ZIP 文件中,您将看到我们有几个文件夹。将此解压缩的 ZIP 文件中的所有文件夹复制(保留index.html文件,因为我们将在一分钟内使用它):

  1. 接下来,转到 Visual Studio 应用程序的解决方案。在wwwroot文件夹中,移动或删除内容,并将从解压缩的 ZIP 文件中的文件夹粘贴到 ASP.NET Core MVC 应用程序的wwwroot文件夹中。您的wwwroot文件夹现在应如下所示:

  1. 回到 Visual Studio,展开 CoreMailValidation 项目中的 wwwroot 节点时,您将看到文件夹。

  2. 我还想让您注意Index.cshtml和_Layout.cshtml文件。我们将修改这些文件:

  1. 打开Index.cshtml文件,并从该文件中删除所有标记(大括号中的部分除外)。将之前从 ZIP 文件中提取的index.html文件中的 HTML 标记粘贴到该文件中。

不要复制index.html文件中的所有标记。只复制<body></body>标记内的标记。

  1. 您的Index.cshtml文件现在应如下所示:
@{ 
    ViewData["Title"] = "Login Page";     
} 

<div class="limiter"> 
    <div class="container-login100"> 
        <div class="wrap-login100"> 
            <div class="login100-pic js-tilt" data-tilt> 
                <img src="img/img-01.png" alt="IMG"> 
            </div> 

            <form class="login100-form validate-form"> 
                <span class="login100-form-title"> 
                    Member Login 
                </span> 

                <div class="wrap-input100 validate-input" 
                 data-validate="Valid email is required: 
                  ex@abc.xyz"> 
                    <input class="input100" type="text" 
                     name="email" placeholder="Email"> 
                    <span class="focus-input100"></span> 
                    <span class="symbol-input100"> 
                        <i class="fa fa-envelope"
                         aria-hidden="true"></i> 
                    </span> 
                </div> 

                <div class="wrap-input100 validate-input" 
                 data-validate="Password is required"> 
                    <input class="input100" type="password" 
                     name="pass" 
                     placeholder="Password"> 
                    <span class="focus-input100"></span> 
                    <span class="symbol-input100"> 
                        <i class="fa fa-lock"
                         aria-hidden="true"></i> 
                    </span> 
                </div> 

                <div class="container-login100-form-btn"> 
                    <button class="login100-form-btn"> 
                        Login 
                    </button> 
                </div> 

                <div class="text-center p-t-12"> 
                    <span class="txt1"> 
                        Forgot 
                    </span> 
                    <a class="txt2" href="#"> 
                        Username / Password? 
                    </a> 
                </div> 

                <div class="text-center p-t-136"> 
                    <a class="txt2" href="#"> 
                        Create your Account 
                        <i class="fa fa-long-arrow-right m-l-5" 
                         aria-hidden="true"></i> 
                    </a> 
                </div> 
            </form> 
        </div> 
    </div> 
</div> 

本章的代码可在 GitHub 上的以下链接找到:

github.com/PacktPublishing/CSharp7-and-.NET-Core-2.0-Blueprints/tree/master/Serverless。

  1. 接下来,打开Layout.cshtml文件,并将我们之前复制到wwwroot文件夹中的所有链接添加到文件中。使用index.html文件作为参考。您将注意到_Layout.cshtml文件包含以下代码片段—@RenderBody()。这是一个占位符,指定了Index.cshtml文件内容应该注入的位置。如果您来自 ASP.NET Web Forms,请将_Layout.cshtml页面视为主页面。您的Layout.cshtml标记应如下所示:
<!DOCTYPE html>
<html>

<head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>@ViewData["Title"] - CoreMailValidation</title>
    <link rel="icon" type="image/png" href="~/images/icons/favicon.ico" />
    <link rel="stylesheet" type="text/css" href="~/vendor/bootstrap/css/bootstrap.min.css">
    <link rel="stylesheet" type="text/css" href="~/fonts/font-awesome-4.7.0/css/font-awesome.min.css">
    <link rel="stylesheet" type="text/css" href="~/vendor/animate/animate.css">
    <link rel="stylesheet" type="text/css" href="~/vendor/css-hamburgers/hamburgers.min.css">
    <link rel="stylesheet" type="text/css" href="~/vendor/select2/select2.min.css">
    <link rel="stylesheet" type="text/css" href="~/css/util.css">
    <link rel="stylesheet" type="text/css" href="~/css/main.css">
</head>

<body>
    <div class="container body-content">
        @RenderBody()
        <hr />
        <footer>
            <p>&copy; 2018 - CoreMailValidation</p>
        </footer>
    </div>
    <script src="img/jquery-3.2.1.min.js"></script>
    <script src="img/popper.js"></script>
    <script src="img/bootstrap.min.js"></script>
    <script src="img/select2.min.js"></script>
    <script src="img/tilt.jquery.min.js"></script>
    <script>
        $('.js-tilt').tilt({
            scale: 1.1
        })
    </script>
    <script src="img/main.js"></script>
    @RenderSection("Scripts", required: false)
</body>

</html>
  1. 如果一切顺利,当您运行 ASP.NET Core MVC 应用程序时,您将看到以下页面。登录表单显然是完全无效的:

但是,登录表单是完全响应的。如果您需要缩小浏览器窗口的大小,您会看到表单随着浏览器大小的减小而缩放。这就是您想要的。如果您想探索 Bootstrap 提供的响应式设计,请访问getbootstrap.com/并查看文档中的示例:

我们接下来要做的事情是将此登录表单连接到我们的控制器,并调用我们创建的 Azure 函数来验证我们输入的电子邮件地址。

让我们来看看下一步该怎么做。

连接所有内容

为了简化事情,我们将创建一个模型传递给我们的控制器:

  1. 在应用程序的Models文件夹中创建一个名为LoginModel的新类,并单击“添加”按钮:

  1. 您的项目现在应该如下所示。您将看到model添加到Models文件夹中:

  1. 接下来,我们要做的是在我们的model中添加一些代码,以表示登录表单上的字段。添加两个名为Email和Password的属性:
      namespace CoreMailValidation.Models 
      { 
        public class LoginModel 
        { 
          public string Email { get; set; } 
          public string Password { get; set; } 
        } 
      }
  1. 回到Index.cshtml视图,在页面顶部添加model声明。这使得model可以在我们的视图中使用。请务必指定model存在的正确命名空间:
      @model CoreMailValidation.Models.LoginModel 
      @{ 
        ViewData["Title"] = "Login Page"; 
      } 
  1. 接下来的代码部分需要在HomeController.cs文件中编写。目前,它应该只有一个名为Index()的操作:
      public IActionResult Index() 
      { 
        return View(); 
      } 
  1. 添加一个名为ValidateEmail的新的async函数,它将使用我们之前复制的 Azure Function URL 的基本 URL 和参数字符串,并使用 HTTP 请求调用它。我不会在这里详细介绍,因为我认为代码非常简单。我们所做的就是使用我们之前复制的 URL 调用 Azure Function 并读取返回的数据:
      private async Task<string> ValidateEmail(string emailToValidate) 
      { 
        string azureBaseUrl = "https://core-mail-
         validation.azurewebsites.net/api/HttpTriggerCSharp1"; 
        string urlQueryStringParams = $"?
         code=/IS4OJ3T46quiRzUJTxaGFenTeIVXyyOdtBFGasW9dUZ0snmoQfWoQ
          ==&email={emailToValidate}"; 

        using (HttpClient client = new HttpClient()) 
        { 
          using (HttpResponseMessage res = await client.GetAsync(
           $"{azureBaseUrl}{urlQueryStringParams}")) 
          { 
            using (HttpContent content = res.Content) 
            { 
              string data = await content.ReadAsStringAsync(); 
              if (data != null) 
              { 
                return data; 
              } 
              else 
                return ""; 
            } 
          } 
        } 
      }  
  1. 创建另一个名为ValidateLogin的public async操作。在操作内部,继续之前检查ModelState是否有效。

有关ModelState的详细解释,请参阅以下文章-www.exceptionnotfound.net/asp-net-mvc-demystified-modelstate/。

  1. 然后,我们在ValidateEmail函数上进行await,如果返回的数据包含单词false,则我们知道电子邮件验证失败。然后将失败消息传递给控制器上的TempData属性。

TempData属性是一个存储数据的地方,直到它被读取。它由 ASP.NET Core MVC 在控制器上公开。TempData属性默认使用基于 cookie 的提供程序在 ASP.NET Core 2.0 中存储数据。要在不删除的情况下检查TempData属性中的数据,可以使用Keep和Peek方法。要了解有关TempData的更多信息,请参阅 Microsoft 文档:docs.microsoft.com/en-us/aspnet/core/fundamentals/app-state?tabs=aspnetcore2x。

如果电子邮件验证通过,那么我们知道电子邮件地址是有效的,我们可以做其他事情。在这里,我们只是说用户已登录。实际上,我们将执行某种身份验证,然后路由到正确的控制器。

另一个有趣的事情是在控制器上的ValidateLogin操作上包含ValidateAntiForgeryToken属性。这确保了表单是从我们的站点提交的,并防止我们的站点受到跨站请求伪造攻击的欺骗。

如果我们必须检查应用程序运行时页面的呈现标记,我们将看到 ASP.NET Core 已自动生成了防伪标记。

通过浏览器的开发者工具检查标记。在 Chrome 中,按Ctrl + Shift + I或者如果您使用 Edge,则按F12。

  1. 您将看到 __RequestVerificationToken 和生成的值如下所示:

  1. HomeController上的完整ValidateLogin操作应如下所示:
      [HttpPost, ValidateAntiForgeryToken] 
      public async Task<IActionResult> ValidateLogin(LoginModel model) 
      { 
        if (ModelState.IsValid) 
        { 
          var email = model.Email; 
          string azFuncReturn = await ValidateEmail(model.Email); 

          if (azFuncReturn.Contains("false")) 
          { 
            TempData["message"] = "The email address entered is 
             incorrect. Please enter again."; 
            return RedirectToAction("Index", "Home"); 
          } 
          else 
          { 
            return Content("You are logged in now."); 
          }                 
        } 
        else 
        { 
          return View(); 
        } 

      } 

回到我们的Index.cshtml视图,仔细查看form标记。我们已经明确定义了使用asp-action(指定要调用的操作)和asp-controller(指定要去哪个控制器查找指定操作)来调用哪个控制器和操作:

<form class="login100-form validate-form" asp-action="ValidateLogin" asp-controller="Home"> 

这将ValidateLogin操作映射到HomeController类上,Index.cshtml表单将提交到该操作:

  1. 然后,稍微往下,确保您的按钮的type指定为submit:
      <div class="container-login100-form-btn"> 
        <button class="login100-form-btn" type="submit"> 
          Login 
        </button> 
      </div> 

我们的Index.cshtml视图几乎完成了。当输入的电子邮件无效时,我们希望得到某种通知。这就是 Bootstrap 派上用场的地方。添加以下标记以显示modal对话框,通知用户输入的电子邮件地址无效。

您将注意到页面末尾包含@section Scripts块。我们基本上是在说,如果TempData属性不为空,那么我们希望通过 jQuery 脚本显示模态对话框:

<div id="myModal" class="modal" role="dialog"> 
    <div class="modal-dialog"> 

        <!-- Modal content--> 
        <div class="modal-content"> 
            <div class="modal-header alert alert-danger"> 
                <button type="button" class="close"
                 data-dismiss="modal">&times;</button> 
                <h4 class="modal-title">Invalid Email</h4> 
            </div> 
            <div class="modal-body"> 
                <p>@TempData["message"].</p> 
            </div> 
            <div class="modal-footer"> 
                <button type="button" class="btn btn-default"
                 data-dismiss="modal">Close</button> 
            </div> 
        </div> 

    </div> 
</div> 

@section Scripts 
    { 
    @if (TempData["message"] != null) 
    { 
        <script> 
            $('#myModal').modal(); 
        </script> 
    } 
} 

运行您的应用程序,并在登录页面上输入一个无效的电子邮件地址。在我的示例中,我只是添加了一个包含两个@符号的电子邮件地址:

当按下登录按钮时,表单将回传到控制器,然后调用 Azure 函数,对输入的电子邮件地址进行验证。

结果是一个相当单调的模态对话框通知弹出,通知用户电子邮件地址不正确:

输入有效的电子邮件地址并单击登录按钮将导致对输入的电子邮件进行成功验证:

如前所述,电子邮件验证与身份验证不同。如果电子邮件经过验证,那么可以进行身份验证过程。如果此身份验证过程成功验证登录的用户,那么他们才会被重定向到已登录页面:

摘要

在本章中,我们看到了如何在 Azure 门户上创建 Azure 函数。我们了解到 Azure 函数是云中使用的应用程序的小代码片段。由于它们是按使用量付费的模式定价,因此您只需支付实际使用的计算能力。当您的 Web 应用程序的用户负载很高时,该函数会根据需要自动扩展以满足访问它的应用程序的需求。

我们通过手动将 URL 发布到浏览器来了解了在 Azure 函数中了解代码的过程。然后,我们创建了一个由单个登录页面组成的 ASP.NET Core MVC 应用程序。然后,我们看了如何使用 Azure 函数来验证登录屏幕上输入的电子邮件地址。 Azure 函数是一种令人兴奋的技术。还有很多东西要学习,这一章剩下的内容不足以讨论这种无服务器技术。如果您对此技术感兴趣,请探索其他可用的 Azure 服务模板。

在下一章中,我们将学习如何使用 ASP.NET Core MVC 应用程序和名为Tweetinvi的 C#库创建 Twitter 克隆。请继续关注,还有很多令人兴奋的内容等着您。

第八章:使用 OAuth 创建 Twitter 克隆

在本章中,我们将看看使用 ASP.NET Core MVC 创建一个基本的 Twitter 克隆是多么容易。我们将执行以下任务:

  • 在 Twitter 上使用 Twitter 的应用程序管理创建你的应用

  • 创建一个 ASP.NET Core MVC 应用程序

  • 阅读你的主页时间线

  • 发布一条推文

你可以想象,Twitter 功能在.NET(更不用说.NET Core)中并不是标准配置。

请注意,你需要创建一个 Twitter 账户才能在本章中执行任务。你可以通过访问twitter.com/进行注册。

幸运的是,有很多专注和热情的开发者愿意免费分享他们的代码。你通常会在 GitHub 上找到他们的代码,而这正是我们将要寻找一些代码集成到我们的 ASP.NET Core MVC 应用程序中,以赋予它 Twitter 的功能。这一章并不是对我们将要使用的特定 Twitter 库的认可。然而,这个库是我用过的最好的之一。而且(在撰写本文时)它还在不断更新。

让我们来看看 Tweetinvi。

使用 Tweetinvi

将你的浏览器指向github.com/linvi/tweetinvi。这个库的描述已经说明了一切:

Tweetinvi,最好的 Twitter C#库,适用于 REST 和 Stream API。它支持.NET、.NETCore、UAP 和便携式类库(Xamarin)...

换句话说,这个库正是我们创建 Twitter 克隆应用所需要的。Tweetinvi 文档非常完善,并且有一个支持它的活跃社区。

ASP.NET Core MVC Twitter 克隆应用程序

创建一个完整的 Twitter 克隆应用是一项艰巨的工作——比这一章节允许的工作还要多,恐怕我只能说明如何读取你主要的推文流(你在 Twitter 上关注的人的推文)。我还会向你展示如何从应用程序发布一条推文。

在这个应用程序中,我将放弃所有花哨的 UI 元素,而是给你一个绝佳的基础,让你继续开发一个完整的 Twitter 克隆。你可以考虑添加以下功能:

  • 删除推文

  • 转推

  • 关注某人

  • 取消关注某人

  • 发送私信

  • 搜索

  • 查看个人资料

你可以添加很多额外的功能;随意添加你想要看到的任何缺失功能。我个人希望有更好的方式来整理和保存我发现有趣的推文。

我知道你们中的一些人可能会想知道为什么点赞一条推文不够,这就是我的原因。点赞推文最近已经成为了一种简便的方式,让别人知道他们已经看到了这条推文。当你在一条推文中被提到时,这一点尤其正确。在不回复的情况下(尤其是对于反问),Twitter 用户只是简单地点赞推文。

点赞一条推文也不是一个整理工具。你点赞的一切都可以在你的点赞下找到。没有办法区分。啊哈!我听到你们中的一些人说,“那时时刻呢?”再次强调,时刻存在于 Twitter 上。

想象一下时刻,但是那些时刻是来到你身边的。无论如何,我们可以对这样一个自定义的 Twitter 克隆应用进行很多改进,真正让它成为你自己的。现在,让我们从基础开始。

在 Twitter 上创建你的应用程序

在我们开始创建 Twitter 克隆之前,我们需要在 Twitter 应用管理控制台上注册它。

要访问应用程序管理控制台,请将你的浏览器指向apps.twitter.com:

  1. 点击登录链接,如下截图所示:

  1. 在登录界面上使用你的 Twitter 凭据登录:

  1. 如果您以前创建过任何应用程序,您将看到它们列在下面。您创建的所有应用程序都列在 Twitter 应用程序部分下。点击“创建新应用”按钮:

  1. 现在您将看到创建应用程序表单。为您的应用程序提供一个合适的名称和描述。为您的应用程序提供一个网站,并最后提供一个回调 URL 值。我只是使用了http://localhost:50000/,稍后将向您展示如何在应用程序中配置此项。如下截图所示:

如果在回调期间 localhost 出现问题,请尝试改用127.0.0.1。

  1. 勾选您理解的 Twitter 开发者协议选项,然后点击创建 Twitter 应用程序:

  1. 接下来,您将看到刚刚创建的应用程序设置的摘要。在屏幕顶部,点击“密钥和访问令牌”选项卡:

  1. 这将带您到您的应用程序设置,其中提供了消费者密钥和消费者密钥。一定要记下这些密钥:

  1. 在页面底部,您将看到一个名为“创建我的访问令牌”的按钮。点击此按钮。这将创建一个令牌,使您能够进行 API 调用:

  1. 生成令牌后,将显示访问令牌和访问令牌密钥。也要记下这些:

这就是在 Twitter 的应用程序管理控制台上注册您的应用程序所需的全部内容。接下来我们需要做的是创建我们的 ASP.NET Core MVC 应用程序。

创建 ASP.NET Core MVC 应用程序并添加 NuGet 包

现在让我们开始创建 ASP.NET Core MVC 应用程序并向其添加 Twitter 功能:

  1. 在 Visual Studio 2017 中,创建一个新的 ASP.NET Core Web 应用程序。我只是在 Twitter 上注册时将我的应用程序命名为相同的名称。点击“确定”按钮:

  1. 在下一个屏幕上,确保您选择了 Web 应用程序(模型-视图-控制器)模板,并且您已从下拉菜单中选择了 ASP.NET Core 2.0。我特别提到这一点,因为我收到读者的反馈,他们从来没有选择过 ASP.NET Core 2.0。点击“确定”按钮:

创建项目后,它将如下所示:

  1. 现在我们要去获取 Tweetinvi NuGet 包,因此请右键单击项目,然后从上下文菜单中选择“管理 NuGet 包”,如下所示:

  1. 在“浏览”选项卡中,搜索tweetinvi,并选择开发人员 Linvi 的项目。点击“安装”按钮将其添加到您的应用程序:

  1. 不久后,进度将在 Visual Studio 的输出窗口中显示为已完成:

  1. 接下来要做的是将我们的 URL 设置为之前在 Twitter 应用程序管理控制台中设置的回调 URL。为此,请右键单击项目,然后从上下文菜单中单击“属性”:

  1. 选择“调试”选项卡,然后在“应用程序 URL”字段中输入回调 URL:

如果您在应用程序管理控制台中将回调 URL 的localhost部分设置为127.0.0.1,则在此处也需要将其设置为127.0.0.1。

  1. 保存您的设置并返回到代码窗口。

从设置的角度来看,这应该是您开始编写代码并连接一切所需的全部内容。让我们开始下一步。

让我们开始编码

此项目的所有代码都可以在 GitHub 上找到。将浏览器指向github.com/PacktPublishing/CSharp7-and-.NET-Core-2.0-Blueprints,并在阅读本章的其余部分时,获取代码并进行操作。

设置类和设置

我想要做的第一件事是创建一个将存储我的设置的类。为此,请执行以下步骤:

  1. 创建一个名为Classes的文件夹,在该文件夹中创建一个名为CoreTwitterSettings的类。然后,在Classes文件夹中添加一个名为TweetItem的第二个类(我们稍后将使用此类)。在此过程中,创建另一个名为css的文件夹,我们稍后将使用它。

  2. 完成后,您的项目将如下所示:

  1. 打开CoreTwitterSettings类,并向其中添加以下代码:
public class CoreTwitterConfiguration 
{ 
    public string ApplicationName { get; set; } 
    public int TweetFeedLimit { get; set; } = 1; 

    public TwitterSettings TwitterConfiguration { get; set; } = new 
    TwitterSettings(); 
} 

public class TwitterSettings 
{ 
    public string Consumer_Key { get; set; } 
    public string Consumer_Secret { get; set; } 
    public string Access_Token { get; set; } 
    public string Access_Secret { get; set; } 
} 
  1. 我们要做的下一件事是找到我们的appsettings.json文件。该文件将位于您的项目根目录中,如下截图所示:

  1. 双击appsettings.json文件以打开进行编辑。文件的默认内容应如下所示:
{ 
  "Logging": { 
    "IncludeScopes": false, 
    "LogLevel": { 
      "Default": "Warning" 
    } 
  } 
} 
  1. 修改文件以包含您想要存储的设置。appsettings.json文件的目的是存储您应用程序的所有设置。

  2. 将您的 Consumer Key 和 Consumer Secret 密钥添加到文件中。还要注意,我已经使用了一个基本 URL 的设置,这是之前设置的回调 URL。这在设置中有时很方便。我还创建了一个名为TweetFeedLimit的设置,以限制返回到主页时间线的推文。

您的 Consumer Key 和 Consumer Secret 肯定与我的示例中的值不同。因此,请确保相应地更改这些值。

  1. 修改您的appsettings.json文件后,它将如下所示:
{ 
  "Logging": { 
    "IncludeScopes": false, 
    "LogLevel": { 
      "Default": "Warning" 
    } 
  }, 

  "CoreTwitter": { 
    "ApplicationName": "Twitter Core Clone (local)", 
    "TweetFeedLimit": 10, 
    "BaseUrl": "http://localhost:50000/", 
    "TwitterConfiguration": { 
      "Consumer_Key": "[YOUR_CONSSUMER_KEY]", 
      "Consumer_Secret": "[YOUR_CONSUMER_SECRET]", 
      "Access_Token": "", 
      "Access_Secret": "" 
    } 
  } 
} 
  1. 如果您查看CoreTwitterSettings类,您会发现它与appsettings.json文件中的 JSON 略有相似。

  2. 在您的 Visual Studio 解决方案中,找到Startup.cs文件并打开进行编辑。您会看到 Visual Studio 2017 已经为您的这个类添加了很多样板代码。特别注意ConfigureServices方法。它应该看起来像这样:

public void ConfigureServices(IServiceCollection services) 
{ 
    services.AddMvc(); 
} 
  1. 自 ASP.NET Core 1.1 以来,我们已经能够使用Get<T>,它可以与整个部分一起使用。要使设置在我们的 ASP.NET Core MVC 应用程序中可用,请将此方法中的代码更改如下:
public void ConfigureServices(IServiceCollection services) 
{ 
    services.AddMvc(); 

    var section = Configuration.GetSection("CoreTwitter"); 
    services.Configure<CoreTwitterConfiguration>(section);             
} 

您会注意到我们正在获取appsettings.json文件中定义的CoreTwitter部分。

创建TweetItem类

TweetItem类只是简单地包含特定推文的 URL。它并不是一个非常复杂的类,但它的用处将在本章后面变得清晰。现在,只需向其中添加以下代码:

public class TweetItem 
{ 
    public string Url { get; set; } 
} 

它将存储的 URL 将是特定推文的 URL。

设置 CSS

为了在推文中使用<blockquote> HTML 标签,您将希望向您的CSS文件夹中添加一个 CSS 文件。在我们的示例中,我们将不使用它,但随着您进一步构建应用程序,您将希望使用此 CSS 来为您的<blockquote>推文设置样式。

如果您现在只是玩玩,完成本章后不打算进一步构建此应用程序,可以跳过添加 CSS 文件的部分。如果您想进一步使用此应用程序,请继续阅读:

  1. 右键单击解决方案中的css文件夹,并向其中添加一个新项。将文件命名为site.css,然后单击“添加”按钮,如下截图所示:

  1. 删除site.css文件的内容,并向其中添加以下css:
blockquote.twitter-tweet { 
    display: inline-block; 
    font-family: "Helvetica Neue", Roboto, "Segoe UI", Calibri,   
    sans-serif; 
    font-size: 12px; 
    font-weight: bold; 
    line-height: 16px; 
    border-color: #eee #ddd #bbb; 
    border-radius: 5px; 
    border-style: solid; 
    border-width: 1px; 
    box-shadow: 0 1px 3px rgba(0, 0, 0, 0.15); 
    margin: 10px 5px; 
    padding: 0 16px 16px 16px; 
    max-width: 468px; 
} 

blockquote.twitter-tweet p { 
    font-size: 16px; 
    font-weight: normal; 
    line-height: 20px; 
} 

blockquote.twitter-tweet a { 
    color: inherit; 
    font-weight: normal; 
    text-decoration: none; 
    outline: 0 none; 
} 

blockquote.twitter-tweet a:hover, 
blockquote.twitter-tweet a:focus { 
    text-decoration: underline; 
} 

为了补充这一部分,你可以阅读 Twitter 开发者文档dev.twitter.com/web/overview/css,并查看 CSS 概述。

添加控制器

现在我们需要开始添加我们的控制器。控制器负责响应应用程序发出的请求:

  1. 在Controllers文件夹中,添加另一个名为TwitterController的控制器。这个控制器将负责撰写新推文和发布新推文。稍后我们会回到这个控制器。现在,只需创建这个类。添加完后,你的解决方案应该如下所示:

  1. 默认情况下,当你创建 ASP.NET Core MVC 应用程序时,Visual Studio 会为你添加HomeController。打开HomeController并查看类的内容。确保在HomeController类中添加以下using语句:
using Tweetinvi; 
using Tweetinvi.Models; 
  1. 我想要做的第一件事是让我的应用程序设置存储在appsettings.json文件中在我的类中可用。你会记得我们修改了Startup.cs文件,在启动时注入了这些设置。

  2. 在HomeController类的顶部,添加以下代码行:

CoreTwitterConfiguration config;
  1. 在那行的下面,添加一个构造函数,将CoreTwitterConfiguration类引入我们控制器的范围内:
public HomeController(IOptions<CoreTwitterConfiguration> options) 
{ 
    config = options.Value; 
} 
  1. 现在我们将修改HomeController类的Index动作,检查我们是否有访问令牌或访问密钥。你会记得我们之前在appsettings.json文件中将它们留空。如果它们为空,那么用户就没有被认证,然后我们将重定向用户到HomeController的AuthenticateTwitter动作:
public IActionResult Index() 
{ 
    try 
    { 
        if (String.IsNullOrWhiteSpace(config.TwitterConfiguration.Access_Token)) throw new Tweetinvi.Exceptions.TwitterNullCredentialsException(); 
        if (String.IsNullOrWhiteSpace(config.TwitterConfiguration.Access_Secret)) throw new Tweetinvi.Exceptions.TwitterNullCredentialsException();                                 
    } 
    catch (Tweetinvi.Exceptions.TwitterNullCredentialsException ex) 
    { 
        return RedirectToAction("AuthenticateTwitter"); 
    } 
    catch (Exception ex) 
    { 
        // Redirect to your error page here 
    } 
    return View(); 
} 
  1. 现在让我们去创建AuthenticateTwitter动作。为此,我们需要消费者凭证,这些凭证我们之前从 Twitter 应用管理控制台复制并添加到我们的appsettings.json文件中。然后我们使这些设置在整个应用程序中可用;现在我们可以看到将设置存储在appsettings.json文件中的好处。

  2. 在AuthenticateTwitter动作中,我们只需将ConsumerCredentials对象传递给消费者密钥和消费者密钥。当我们验证通过时,我们将路由到ValidateOAuth动作,接下来我们将创建这个动作:

public IActionResult AuthenticateTwitter() 
{ 
    var coreTwitterCredentials = new ConsumerCredentials( 
        config.TwitterConfiguration.Consumer_Key 
        , config.TwitterConfiguration.Consumer_Secret); 
         var callbackURL = "http://" + Request.Host.Value + 
         "/Home/ValidateOAuth"; 
    var authenticationContext = 
    AuthFlow.InitAuthentication(coreTwitterCredentials,  
    callbackURL); 

    return new 
    RedirectResult(authenticationContext.AuthorizationURL); 
} 
  1. 在这一点上,我们已经被重定向到 Twitter 进行 OAuth 用户认证,并通过回调 URL 被重定向回我们的 ASP.NET Core 应用程序。代码非常简单。需要注意的一点是userCredentials.AccessToken和userCredentials.AccessTokenSecret是从userCredentials对象返回的。我只是把它们添加到了应用程序的配置设置中,但实际上,你可能希望将它们存储在其他地方(比如加密在数据库中)。这样就可以让你在不需要每次都进行身份验证的情况下使用应用程序:
public ActionResult ValidateOAuth() 
{ 
    if (Request.Query.ContainsKey("oauth_verifier") &&  
    Request.Query.ContainsKey("authorization_id")) 
    { 
        var oauthVerifier = Request.Query["oauth_verifier"]; 
        var authId = Request.Query["authorization_id"]; 

        var userCredentials =  
        AuthFlow.CreateCredentialsFromVerifierCode(oauthVerifier, 
        authId); 
        var twitterUser = 
        Tweetinvi.User.GetAuthenticatedUser(userCredentials); 

        config.TwitterConfiguration.Access_Token = 
        userCredentials.AccessToken; 
        config.TwitterConfiguration.Access_Secret = 
        userCredentials.AccessTokenSecret; 

        ViewBag.User = twitterUser; 
    } 

    return View(); 
} 

由于这个控制器动作被称为ValidateOAuth,让我们去创建一个同名的视图,这样我们就可以路由到一个页面,通知用户他们已经成功认证。

创建视图

视图和传统的 HTML 页面并不是同一回事。ASP.NET Core MVC 应用程序的页面由视图表示。正如我之前指出的,控制器接收请求并处理该请求。控制器可以将你重定向到另一个控制器动作,但也可以返回一个视图:

  1. 现在我们将继续创建应用程序的视图。展开Home文件夹,并在Home文件夹中添加一个名为ValidateOAuth的新视图。只需创建这些视图而不需要模型:

  1. 在Views文件夹中添加一个名为Twitter的文件夹,并在该文件夹中添加两个视图,分别为ComposeTweet和HomeTimeline。完成后,你的应用程序将如下所示:

  1. 打开ValidateOAuth视图,并向其添加以下标记:
@if (@ViewBag.User != null) 
{ 
    <h2>OAuth Authentication Succeeded!</h2> 
    <p>Welcome to the CoreTwitter Demo Application <b>@ViewBag.User.Name</b>. You have been successfully authenticated via Twitter.</p> 

    <div class="row"> 
        <div class="col-md-4"> 
            <h2>Go to your home feed</h2> 
            <p> 
                See what's new on your home feed. 
            </p> 
            <p> 
                <a class="btn btn-default" 
                 href="/Home/GetHomeTimeline">Home &raquo;</a> 
            </p> 
        </div> 
    </div> 
} 
else 
{ 
    <h2>OAuth Authentication failed!</h2> 
    <p>An error occurred during authentication. Try <a  
     href="/Home/TwitterAuth">authenticating</a> again.</p> 
} 

看一下标记,你会注意到它只是通知用户认证状态。如果经过认证,用户可以查看他们的主页动态,这是他们在 Twitter 上关注的人的所有推文。

我想在这里提醒你一下,我是如何在Home控制器上调用GetHomeTimeline动作的。你会在按钮链接中看到以下href存在:

href="/Home/GetHomeTimeline" 

这是将用户路由到控制器上的一个方法。稍后,我会向你展示另一种更好的方法来做到这一点。

因此,我们允许成功认证的用户通过点击Home链接查看他们关注的人的推文。这调用了一个名为GetHomeTimeline的动作。让我们去修改HomeController以添加这个动作。

修改 HomeController

回到HomeController,并添加另一个名为GetHomeTimeline的动作。然后,使用用户凭据查找经过认证用户的主页时间线推文。用户凭据包括以下内容:

  • 消费者密钥

  • 消费者密钥

  • 访问令牌

  • 访问密钥

你会注意到这些都来自CoreTwitterConfiguration对象。推特动态只包括在设置中设置的限制。我将我的设置为10,所以这应该只包含 10 条推文。对于动态中的每条推文,我提取推文的 URL 并将其添加到TweetItem类型的列表中(我们之前创建的类)。如果一切顺利,我就路由到HomeTimeline视图。

将以下代码添加到你的GetHomeTimeline动作中。

你应该在引用名为homeView的TwitterViewModel实例的代码上得到一个错误。我们接下来将纠正这个错误。

你的动作应该如下所示:

public IActionResult GetHomeTimeline() 
{ 
    TwitterViewModel homeView = new TwitterViewModel(); 

    try 
    { 
        if (config.TwitterConfiguration.Access_Token == null) throw new 
        Tweetinvi.Exceptions.TwitterNullCredentialsException(); 
        if (config.TwitterConfiguration.Access_Secret == null) throw 
        new Tweetinvi.Exceptions.TwitterNullCredentialsException(); 

        var userCredentials = Auth.CreateCredentials( 
            config.TwitterConfiguration.Consumer_Key 
            , config.TwitterConfiguration.Consumer_Secret 
            , config.TwitterConfiguration.Access_Token 
            , config.TwitterConfiguration.Access_Secret); 

        var authenticatedUser =  
        Tweetinvi.User.GetAuthenticatedUser(userCredentials); 

        IEnumerable<ITweet> twitterFeed = 
        authenticatedUser.GetHomeTimeline(config.TweetFeedLimit); 

        List<TweetItem> tweets = new List<TweetItem>(); 
        foreach(ITweet tweet in twitterFeed) 
        { 
            TweetItem tweetItem = new TweetItem();                     

            tweetItem.Url = tweet.Url; 
            tweets.Add(tweetItem); 
        } 

        homeView.HomeTimelineTweets = tweets;                 
    } 
    catch (Tweetinvi.Exceptions.TwitterNullCredentialsException ex) 
    { 
        return RedirectToAction("AuthenticateTwitter"); 
    } 
    catch (Exception ex) 
    { 

    } 

    return View("Views/Twitter/HomeTimeline.cshtml", homeView); 
} 

如前所述,你会看到一些错误。这是因为我们还没有一个名为TwitterViewModel的模型。让我们接下来创建它。

创建 TwitterViewModel 类

TwitterViewModel类只是一个非常简单的类,它将TweetItem的集合作为名为HomeTimelineTweets的属性。

让我们首先向我们的项目添加一个模型:

  1. 右键单击Models文件夹,然后在文件夹中添加一个名为TwitterViewModel的类。然后,将以下代码添加到该类中:
public class TwitterViewModel 
{ 
    public List<TweetItem> HomeTimelineTweets { get; set; } 
}
  1. 还要向类添加using语句using CoreTwitter.Classes;。

这就是所需要的一切。当你稍后扩展TweetItem类(如果你决定为这个应用添加功能),这个模型将负责将这些信息传递给我们的视图,以便在 Razor 中使用。

创建 HomeTimeline 视图

回想一下我们之前创建的HomeController动作GetHomeTimeline,你会记得我们路由到一个名为HomeTimeline的视图。我们已经创建了这个视图,但现在我们需要向它添加一些逻辑来呈现我们主页时间线中的推文。

因此,我们需要为我们的主页时间线添加一个视图,接下来我们将添加:

  1. 打开HomeTimeline.cshtml文件,并向视图添加以下标记:
@model TwitterViewModel 
@{ 
    ViewBag.Title = "What's happening?"; 
} 

<h2>Home - Timeline</h2> 

<div class="row"> 
    <div class="col-md-8"> 

        @foreach (var tweet in Model.HomeTimelineTweets) 
        { 
            <blockquote class="twitter-tweet"> 
                <p lang="en" dir="ltr"> 
                    <a href="@Html.DisplayFor(m => tweet.Url)"></a> 
            </blockquote> 
            <script async 
             src="img/widgets.js" 
             charset="utf-8"></script> 
        } 
    </div> 

    <div class="col-md-4"> 
        <h2>Tweet</h2> 
        <p>What's happening?</p> 
        <a class="btn btn-default" asp-controller="Twitter" asp-
         action="ComposeTweet">Tweet &raquo;</a>   
    </div> 

</div> 

你需要注意的第一件事是文件顶部的@model TwitterViewModel语句。这允许我们在视图中使用模型中存储的值。我们的视图循环遍历模型的HomeTimelineTweets属性中包含的推文集合,并构建一个要在页面上显示的推文列表。

我想要引起你的注意的另一件事是 Tweet 链接上的标签助手asp-controller和asp-action。这是一种更干净的方式,可以路由到特定控制器上的特定动作(而不是像我们之前在ValidateOAuth视图中看到的那样在href中进行路由)。

最后,你可能想知道widgets.js引用是做什么的。好吧,我不想自己设计我的推文样式,所以我决定让 Twitter 为我做。

  1. 要获取标记,请转到publish.twitter.com/#:

  1. 从下拉菜单中,选择“A Tweet”作为您要嵌入的内容的选项,如下所示:

  1. 然后您将获得一些示例代码供使用。 您只需单击复制代码按钮。 这只是我做的方式,但欢迎您在不经过此步骤的情况下自行前进:

  1. 您复制的代码可能看起来像以下内容:
<blockquote class="twitter-tweet"> 
        <p lang="en" dir="ltr">Sunsets don't get much better than 
         this one over <a href="https://twitter.com/GrandTetonNPS?
         ref_src=twsrc%5Etfw">@GrandTetonNPS</a>. 
        <a href="https://twitter.com/hashtag/nature?
         src=hash&amp;ref_src=twsrc%5Etfw">#nature</a> 
        <a href="https://twitter.com/hashtag/sunset?
         src=hash&amp;ref_src=twsrc%5Etfw">#sunset</a> 
    <a href="http://t.co/YuKy2rcjyU">pic.twitter.com/YuKy2rcjyU</a> 
            </p>&mdash; US Department of the Interior (@Interior) 
    <a href="https://twitter.com/Interior/status/463440424141459456?
     ref_src=twsrc%5Etfw">May 5, 2014</a> 
    </blockquote> 
    <script async src="img/widgets.js" 
     charset="utf-8"></script> 
  1. 将其修改为根据您的页面进行样式设置。 在循环中执行此操作,以便您可以单独输出所有推文。 您最终应该得到的代码只是:
<blockquote class="twitter-tweet"> 
    <p lang="en" dir="ltr"> 
        <a href="@Html.DisplayFor(m => tweet.Url)"></a> 
</blockquote> 
<script async src="img/widgets.js" charset="utf-8"></script> 

它只包含指向 Twitter URL 的链接。

修改 TwitterController 类

现在我们来到了允许用户发送推文的部分。

打开TwitterController类并添加名为ComposeTweet和PublishTweet的两个操作。 TwitterController类非常简单。 它只包含以下代码:

public class TwitterController : Controller 
{         
    public IActionResult ComposeTweet() 
    {             
        return View(); 
    } 

    public IActionResult PublishTweet(string tweetText) 
    { 
        var firstTweet = Tweet.PublishTweet(tweetText); 

        return RedirectToAction("GetHomeTimeline", "Home");  
    } 
} 

ComposeTweet操作只是简单地将用户返回到一个视图,他们可以在其中撰写推文。 您会记得我们之前创建了ComposeTweet视图。 PublishTweet操作同样简单。 它获取我要发推文的文本,并将其传递给Tweetinvi.Tweet类的PublishTweet方法。 之后,将重定向回主页时间线,我们期望在那里看到我们刚刚创建的推文。

我们需要完成的最后一个任务是修改ComposeTweet视图。 让我们接下来做这件事。

完成-ComposeTweet 视图

最后,我们使用ComposeTweet视图。

打开ComposeTweet视图并向视图添加以下标记:

@{ 
    ViewData["Title"] = "Tweet"; 
} 

<h2>Tweet</h2> 

<form method="post" asp-controller="Twitter" asp-action="PublishTweet"> 

    <div class="form-group"> 
        <label for="tweet">Tweet : </label> 
        <input type="text" class="form-control" name="tweetText" 
         id="tweetText" value="What's happening?" /> 
    </div> 

    <div class="form-group"> 
        <input type="submit" class="btn btn-success" /> 
    </div> 
</form> 

您会注意到,我再次使用标签助手来定义要调用的控制器和操作。 只是这一次,我是在<form>标签上这样做的。 在这一点上,您已经准备好首次运行应用程序了。 让我们看看它的表现如何。

运行 CoreTwitter 应用程序

对项目进行构建,以确保一切构建正确。 然后,开始调试您的应用程序。 因为您尚未经过身份验证,所以将被重定向到 Twitter 进行身份验证。

这是一个您肯定习惯看到的页面:

  1. 许多网络应用程序使用 OAuth 进行身份验证。 要继续,请点击授权应用程序按钮,如下所示:

  1. 然后您将看到一个重定向通知。 这可能需要一些时间来重定向您。 这完全取决于您的互联网连接速度:

  1. 一旦您被重定向到您的 CoreTwitter 应用程序,您将看到 OAuth 身份验证成功的消息。 之后,点击主页按钮转到“主页时间线”:

  1. HomeController开始执行,因为调用GetHomeTimeline操作并将您重定向到HomeTimeline视图。 您将在页面中看到加载的推文:

  1. 当您滚动浏览推文时(记住,我只返回了 10 条),您将看到包含视频的推文,当您单击播放按钮时将播放:

  1. 富媒体推文还会为您提供文章预览,并且您还将在时间轴中看到普通的文本推文。 所有链接都是完全活动的,您可以单击它们以查看文章:

  1. 如果您向右滚动到时间轴的底部(这应该在顶部,但我告诉过您我不打算在 UI 周围做太多事情),您将看到“推文”按钮。 单击它以撰写新推文:

  1. 在ComposeTweet视图上,您可以在推文字段中输入任何内容,然后单击“提交查询”按钮:

  1. 你的推文随后会发布在 Twitter 上,然后你会被重定向到主页时间轴,你会在那里看到你新发布的推文:

而且,仅仅为了这个缘故,你可以通过访问以下 URL 来查看特定的推文:twitter.com/DirkStrauss/status/973002561979547650。

是的,现在真的是凌晨 3:07。#就是这样。

总结

回顾这一章,我们确实做了很多。我鼓励你去 GitHub 上查看代码,以及在github.com/linvi/tweetinvi上可用的 Tweetinvi 文档。在这一章中,我们看到了如何在 Twitter 的应用程序管理控制台上注册我们的应用程序。我们看到我们可以通过使用一个叫做 Tweetinvi 的 NuGet 包,轻松地为我们的 ASP.NET Core MVC 应用程序添加 Twitter 功能。我们看了一下路由,以及控制器、模型、视图,以及将设置存储在appsetting.json文件中。

我们能够通过 OAuth 进行身份验证,并从我们的主页时间轴中读取最后 10 条推文。最后,我们能够发布一条推文,并在我们的主页时间轴中查看它。

在我们的 Twitter 克隆应用程序中仍然有很多工作可以做。我希望你觉得这是一个有趣的章节,并希望你继续努力改进它,以适应你特定的工作流程,并使其成为你自己的。

在下一章中,我们将看一下 Docker 以及作为软件开发人员对你意味着什么。我们还将看到如何在 Docker 容器中运行我们的 ASP.NET Core MVC 应用程序。

第九章:使用 Docker 和 ASP.NET Core

在本章中,我们将看看 Docker 是如何工作的。你可能以前听说过 Docker,但还没有机会去尝试。特别是,我们将看以下内容:

  • Docker 是什么

  • 图像和容器

  • Docker 如何使 web 开发人员受益

  • 在 Windows 10 Pro 上安装 Docker

  • 运行 Docker 并选择一些共享驱动器

  • 在 Windows 防火墙似乎是问题时排除共享驱动器的故障

  • Visual Studio 2017 如何与 Docker 集成

  • 创建一个 ASP.NET Core MVC 应用程序并在容器内运行它

  • 使用 Docker Hub 与 GitHub 并设置自动构建

Docker 将为你打开一个全新的世界。

Docker 是什么?

在我们开始使用 Docker 之前,让我们来看看 Docker 到底是什么。如果你访问 www.docker.com 并查看 What is Docker? 页面,你会看到他们说 Docker 是一个容器化平台。从第一眼看来,这并没有太多意义。不过,深入挖掘一下,你会发现 Docker 简化了应用构建过程,并允许你在不同的环境中运行和部署这些应用。这些不同的环境可能是开发、测试、用户验收测试和生产环境。

Docker 使用图像和容器,如果你看一下 Docker 的标志,你会看到他们的标志中代表着容器的概念:

货物规划员经常需要非常小心地堆放货船上的集装箱。他们在规划集装箱在船上的位置时需要考虑集装箱的目的地。

例如,前往中东的集装箱不能被装载在前往日本东京的集装箱下面。这意味着他们必须先移除顶部的集装箱,然后卸载底部的集装箱,然后再重新装载顶部的集装箱。集装箱的位置必须非常小心地规划,以优化货运物流的效率。

Docker 在使用容器方面类似。因此,让我们进一步澄清 容器 和 图像 这两个术语。

图像和容器

Docker 镜像只是用来创建 Docker 容器的文件。把它想象成 Docker 需要创建运行容器的蓝图。图像是只读模板,可以理解为创建容器实例所使用的共享文件的分层文件系统。

另一方面,容器是从这些图像创建的实例。容器是隔离和安全的,可以启动、停止、移动或删除。

Docker 运行在哪里?

如前所述,使用货船的类比,货船代表你的开发环境、测试环境或生产环境。

Docker 可以原生运行在以下系统上:

  • Linux

  • Windows Server 2016

  • Windows 10

Docker 也可以在以下云上运行:

  • Amazon EC2

  • Google Compute Engine

  • Azure

  • Rackspace

从前面的观点可以看出,Docker 是非常灵活的,使用 Docker 可以为开发人员提供巨大的好处。让我们看看 Docker 如何特别有益于 web 开发人员。

Docker 如何使 web 开发人员受益

Docker 为 web 开发人员提供了几个好处。如果你在开发人员、测试人员、设计师等混合环境中工作,你可能希望他们使用实际的应用程序而不是原型。你可以在服务器上设置应用程序,并将其连接到 SQL 数据库,然后管理每个用户从服务器访问站点所需的权限。另一方面,Docker 允许我们创建可以轻松在各个开发人员或设计师的机器上运行的容器。

我之前提到过,Docker 容器是隔离和安全的。因此,容器消除了应用程序冲突。我相信如果你已经开发了一段时间,你一定会遇到这样的情况:应用程序部署在生产服务器上。如果你想(或需要)升级应用程序的框架(例如),你可能会因升级而遇到其他应用程序冲突。有了 Docker,隔离的容器可以在不影响环境中的其他系统的情况下进行升级。

你有多少次听到开发人员说,“但是我的系统上应用程序运行良好”,而部署的应用程序失败了?这是因为开发人员的计算机、暂存服务器或生产服务器的设置可能存在差异。有了 Docker,你只需将镜像从一个环境移动到另一个环境,并让容器运行起来。这意味着,如果你的应用程序在开发机器上的容器内运行良好,它肯定也应该在暂存或生产机器上运行良好。

由于 Docker 容器的可预测性和稳定性,你能够比以前更快地发布代码。这将提高生产率。

在 Windows 10 专业版上安装 Docker

对于 Windows 10 专业版和 Windows 10 企业版,Docker Community Edition (CE)是免费提供的。

你可以从www.docker.com/docker-windows下载 Docker CE。

Docker CE 需要 Hyper-V,因此你需要运行 Windows 10 专业版或更高版本。要查看你的 Windows 版本,以管理员身份打开命令提示符,并在提示符处输入以下命令:

systeminfo

你会看到以下信息显示:

要检查 Hyper-V 是否已启用,请向下滚动一小段距离:

早期版本的 Windows 没有 Hyper-V,因此 Docker CE 无法运行。根据 Docker 文档(docs.docker.com/v17.09/docker-for-windows/faqs/#questions-about-stable-and-edge-channels),Windows 10 家庭版也不受支持。

对于较旧的 Mac 和 Windows 系统,可以安装 Docker Toolbox。它使用免费的 Oracle VM VirtualBox。有关更多信息,请查看docs.docker.com/toolbox/toolbox_install_windows/。

如前所述,Docker CE 可在 Windows 10 专业版和 Windows 10 企业版上下载。你可以从 Docker 商店下载安装程序:store.docker.com/editions/community/docker-ce-desktop-windows。

在撰写本文时,商店上的下载页面如下所示:

点击“获取 Docker”按钮,将 Docker 安装程序下载到你的计算机上。

安装程序将要求你注销 Windows 以完成安装。它不会自动执行此操作,但在执行安装之前,最好关闭其他正在运行的应用程序。

实际上,这个安装程序是我近年来见过的最友好的安装程序之一。而且安装起来也非常简单:

通常情况下,我总是以管理员身份运行安装程序。Docker 的安装过程非常简单。安装完成后,它会提示您注销 Windows:

重新登录 Windows 后,你可能会看到一条消息,要求你打开 Hyper-V 以使用 Docker 容器。选择打开 Hyper-V 选项。此时,你的计算机可能会再次重启。计算机重新启动后,你将看到 Docker 正在运行的通知:

您已成功安装了 Docker。我告诉过你,这真的很容易。

理解 Docker

要开始使用 Docker,请查找 Docker for Windows 桌面应用程序:

这将在您的计算机上启动 Docker。当 Docker 运行时,您将在任务栏中看到它:

默认情况下,安装后应启动 Docker,因此首先从任务栏检查它是否正在运行。让我们看看 Docker 提供给我们的各种设置。右键单击任务栏中的 Docker 图标,然后从上下文菜单中选择“设置”。打开屏幕后,单击“共享驱动器”选项卡:

重要的是您选择要提供给容器的本地驱动器。检查共享驱动器可以支持卷。卷是 Docker 容器生成的数据持久存在的机制。您可以在官方 Docker 文档中阅读有关卷的更多信息docs.docker.com/engine/admin/volumes/volumes/。

但是,我想指出文档中的以下要点:

  • 卷可以很容易地进行备份

  • 卷在 Linux 和 Windows 容器上工作

  • 您可以在多个容器之间共享卷

  • 您可以使用卷驱动程序将卷存储在远程计算机或云中

  • 您可以加密卷的内容

因为卷存在于容器之外,所以它是持久保存数据的首选选择。Docker 还需要端口445打开以在主机机器和容器之间共享驱动器。如果 Docker 检测到端口445关闭,您将看到以下屏幕:

您可以单击链接阅读有关此错误的文档。

有关共享驱动器的更多信息,请参阅 Docker 文档docs.docker.com/docker-for-windows/#shared-drives。

有一些在线推荐的方法可以解决此问题。首先是卸载并重新安装“Microsoft 网络的文件和打印机共享”。

  1. 要做到这一点,请从 Windows 设置中打开“网络和共享中心”。然后单击“vEthernet(DockerNAT)”连接:

  1. 在 vEthernet(DocketNAT)状态窗口中,单击“属性”按钮:

  1. 在这里,您将看到“文件和打印机共享”用于 Microsoft 网络。您的第一步是单击“卸载”按钮。这将从列表中删除条目。接下来,您需要单击“安装”按钮:

  1. 在“选择网络功能类型”屏幕上,单击“服务功能”,然后单击“添加”按钮:

  1. 在“选择网络服务”屏幕上,选择 Microsoft 作为制造商,然后单击“Microsoft 网络的文件和打印机共享”服务:

  1. 单击“确定”并关闭所有屏幕后,通过右键单击任务栏中的图标并单击“退出 Docker”来停止 Docker。然后,您可以通过再次单击“Windows 应用程序”来重新启动 Docker。

此时,您应该能够从设置屏幕中选择要与 Docker 一起使用的共享驱动器。如果仍然看到防火墙检测到的消息,则很可能是您的防病毒软件正在阻止它。

在我的情况下,是 ESET Endpoint Security 阻止了通信。您可能使用不同的防病毒软件,因此请查看它最近阻止的特定应用程序列表。在我的情况下,我启动了 ESET Endpoint Security 并选择了 SETUP,然后选择了 Network:

  1. 接下来,我选择了“最近阻止的应用程序或设备”列表:

在浏览列表时,我发现 ESET 阻止了10.0.75.2。根据 Docker 文档,这是要通过防火墙允许的 IP 地址:

“要共享驱动器,请允许 Windows 主机机器和 Windows 防火墙或第三方防火墙软件之间的连接。您不需要在任何其他网络上打开 445 端口。默认情况下,允许从 10.0.75.2(虚拟机)到 10.0.75.1 端口 445(Windows 主机)的连接。如果防火墙规则似乎是开放的,请考虑在虚拟网络适配器上重新安装文件和打印共享服务。”

  1. 单击“解除阻止”按钮会显示一个确认屏幕:

当您这样做时,您已解除了10.0.75.2的阻止:

  1. 最后,单击“完成”,返回到 Docker 设置,并选择要共享的驱动器。

现在,您应该能够选择要供 Docker 使用的共享驱动器。如果您仍然无法共享驱动器,请查看以下 Stack Overflow 文章,获取额外的故障排除提示:stackoverflow.com/questions/42203488/settings-to-windows-firewall-to-allow-docker-for-windows-to-share-drive/43904051#43904051。

接下来,我们将看看 Docker 如何集成到 Visual Studio 2017 中,以及您可以为 ASP.NET Core 应用程序启用 Docker 支持的方法。我们还将看看如何为现有的 ASP.NET Core 应用程序添加 Docker 支持(或 Dockerize)。

Docker 拥有一个庞大的开发者社区,也有大量的帮助文档可用。花些时间浏览这些文档,并研究您可能遇到的任何问题。

在 Visual Studio 2017 中在 Docker 内运行 ASP.NET Core 应用程序

那么,这一切对我们意味着什么呢?我们已经看过如何在 Windows 10 上设置 Docker,以及如何解决围绕这一设置的一些问题。现在让我们看看如何创建一个 ASP.NET Core 应用程序并为新应用程序添加 Docker 支持。

  1. 在 Visual Studio 2017 中创建一个新的 ASP.NET Core Web 应用程序,然后单击“确定”:

  1. 在下一个屏幕上,选择 Web 应用程序(模型-视图-控制器)或您喜欢的任何类型,同时确保从下拉列表中选择了 ASP.NET Core 2.0。然后勾选“启用 Docker 支持”复选框。这将启用操作系统下拉列表。在这里选择 Windows,然后单击“确定”按钮:

如果您看到以下消息,您需要切换到 Windows 容器。这是因为您可能已将 Docker 的默认容器设置为 Linux:

如果您在任务栏中的 Docker 图标上右键单击,您将看到您有一个选项在那里启用 Windows 容器。您可以通过单击任务栏中的 Docker 图标上的“切换到 Windows 容器”选项切换到 Windows 容器:

切换到 Windows 容器可能需要几分钟的时间才能完成,这取决于您的线路速度和 PC 的硬件配置。

但是,如果您没有单击此选项,当选择操作系统平台为 Windows 时,Visual Studio 将要求您切换到 Windows 容器。

我选择 Windows 容器作为目标操作系统是有充分理由的。在本章中,当使用 Docker Hub 和自动构建时,这个理由将变得更加清晰。

创建完 ASP.NET Core 应用程序后,您将在解决方案资源管理器中看到以下项目设置:

添加到 Visual Studio 的 Docker 支持不仅以 Dockerfile 的形式添加,还以 Docker 配置信息的形式添加。这些信息包含在解决方案级别的全局 docker-compose.yml 文件中:

  1. 单击“解决方案资源管理器”中的 Dockerfile,您会发现它看起来一点也不复杂。请记住,Dockerfile 是创建图像的文件。图像是一个只读模板,概述了如何创建 Docker 容器。因此,Dockerfile 包含生成图像并运行它所需的步骤。Dockerfile 中的指令在图像中创建层。这意味着如果 Dockerfile 中有任何更改,只有更改的层在重新构建图像时才会被重建。Dockerfile 如下所示:
FROM microsoft/aspnetcore:2.0-nanoserver-1709 AS base 
WORKDIR /app 
EXPOSE 80 

FROM microsoft/aspnetcore-build:2.0-nanoserver-1709 AS build 
WORKDIR /src 
COPY *.sln ./ 
COPY DockerApp/DockerApp.csproj DockerApp/ 
RUN dotnet restore 
COPY . . 
WORKDIR /src/DockerApp 
RUN dotnet build -c Release -o /app 

FROM build AS publish 
RUN dotnet publish -c Release -o /app 

FROM base AS final 
WORKDIR /app 
COPY --from=publish /app . 
ENTRYPOINT ["dotnet", "DockerApp.dll"] 

当您查看 Visual Studio 2017 中的菜单时,您会注意到“运行”按钮已更改为“Docker”:

  1. 单击“Docker”按钮调试 ASP.NET Core 应用程序时,您会注意到“输出”窗口中会弹出一些内容。特别感兴趣的是最后的 IP 地址。在我的情况下,它显示 Launching http://172.24.12.112 (您的将不同):

启动浏览器后,您将看到 ASP.NET Core 应用程序正在以前在“输出”窗口中列出的 IP 地址上运行。您的 ASP.NET Core 应用程序现在正在 Windows Docker 容器中运行:

这非常好,而且非常容易上手。但是,要将现有的 ASP.NET Core 应用程序 Docker 化,您需要做些什么呢?事实证明,这并不像您想象的那么困难。

为现有的 ASP.NET Core 应用程序添加 Docker 支持

假设您有一个不支持 Docker 的 ASP.NET Core 应用程序。要为此现有应用程序添加 Docker 支持,只需从上下文菜单中添加即可:

要为现有的 ASP.NET Core 应用程序添加 Docker 支持,需要执行以下操作:

  1. 在“解决方案资源管理器”中右键单击项目

  2. 点击“添加”菜单项

  3. 单击“Docker 支持”菜单中的“Docker 支持”:

  1. Visual Studio 2017 现在会询问您的目标操作系统是什么。在我们的情况下,我们将以 Windows 为目标:

  1. 单击“确定”按钮后,Visual Studio 2017 将开始为您的项目添加 Docker 支持:

创建具有 Docker 支持的 ASP.NET Core 应用程序非常容易,甚至可以更轻松地为现有的 ASP.NET Core 应用程序添加 Docker 支持。

最后,如果遇到任何问题,如文件访问问题,请确保您的防病毒软件已将 Dockerfile 排除在扫描范围之外。还要确保以管理员身份运行 Visual Studio。

使用 GitHub 的 Docker Hub

接下来的部分将说明如何设置 Docker Hub 以从 GitHub 存储库中的项目进行自动构建。

在此示例中,我不会介绍如何将代码检入 GitHub。

  1. 使用前几节创建的 DockerApp 项目,将其检入新的 GitHub 存储库。检入代码后,转到 Docker Hub hub.docker.com/ 并登录,如果还没有帐户,可以创建一个:

  1. 注册过程非常快速简单。您只需确认您的电子邮件地址,然后就可以使用了。确认电子邮件地址后,您将被提示再次登录。这将带您到 Docker Hub 仪表板。

在此页面上,您有几个选项可用。您可以创建存储库和组织,并浏览存储库:

  1. 要开始使用 GitHub,我们首先需要将 Docker Hub 与 GitHub 链接起来。点击页面右上角选择的用户名,然后点击“设置”菜单选项:

  1. 在设置下,找到“链接的帐户和服务”选项卡,然后点击它。现在您需要点击“链接 Github”选项继续:

  1. 为了简单起见(也是建议的),我直接点击了“公共和私有访问”设置:

  1. Docker Hub 现在会重定向您到授权页面,以允许 Docker Hub 访问您的 GitHub 存储库。在这里,您需要使用您的 GitHub 凭据登录:

请注意,如果您启用了双因素身份验证,您需要输入智能手机应用生成的身份验证代码。所以请保持手机附近。

  1. 要授权 Docker Hub,点击“授权 docker”按钮:

现在您将被带回“链接的帐户和服务”页面,在那里您将看到您 Docker Hub 配置文件上的已链接帐户:

  1. 接下来,我们需要去创建一个自动构建。从菜单中,点击“创建”菜单项,然后从下面的选项中选择“创建自动构建”:

  1. 然后,您需要点击“创建自动构建 Github”选项:

这将显示您 GitHub 帐户中所有可用存储库的列表。我之前将 DockerApp 项目检入到了我的 GitHub 帐户,所以这就是我们要选择的:

  1. 现在您可以根据需要在这里定义其他信息,或者保持默认设置。由您决定。完成后,点击“创建”按钮:

我们的自动构建现在已经创建并准备就绪。那么,这个自动构建是如何工作的呢?每当您将代码提交到 GitHub 存储库时,Docker Hub 都会构建您的项目:

  1. 要测试这一点,打开 Visual Studio 2017 中的 ASP.NET Core 应用程序并进行一些更改。然后将这些更改提交到您的 GitHub 存储库。然后点击 Docker Hub 中的“构建详情”链接。您会看到构建已排队,并将在几分钟内完成。要查看构建结果,稍等一会后刷新此页面:

  1. 刷新页面后,您会看到发生了错误。Docker Hub 将为您显示构建结果,您可以点击构建结果查看失败的详细信息。

我将举例说明一些常见的自动构建错误。我还将展示我发现的解决方法。我不确定这些方法在此期间是否有变化,但在撰写本文时,这些问题确实存在。

当我们查看失败的原因时,我们发现 Docker Hub 找不到项目根目录中的 Dockerfile。为什么会出现这个问题,我不知道。我本来期望 Docker Hub 会递归地遍历项目的树形结构,以找到 Dockerfile 的位置。不过,这很容易解决:

我只是复制了我的 Dockerfile 并将其复制到解决方案的根目录。然后我再次将我的代码检入 GitHub:

  1. 如果刷新您的自动构建页面,您会看到它正在重新构建项目:

  1. 这一次,又出现了另一个错误。再次点击错误条目会带您到错误详细信息:

这次,它显示错误的原因是我们的项目针对的是 Windows OS 而不是 Linux:

错误列如下:

Build failed: image operating system "windows" cannot be used on this platform 
  1. 要解决此问题,我们需要修改 Dockerfile。 Windows 的 Dockerfile 如下所示:
FROM microsoft/aspnetcore:2.0-nanoserver-1709 AS base 
WORKDIR /app 
EXPOSE 80 

FROM microsoft/aspnetcore-build:2.0-nanoserver-1709 AS build 
WORKDIR /src 
COPY *.sln ./ 
COPY DockerApp/DockerApp.csproj DockerApp/ 
RUN dotnet restore 
COPY . . 
WORKDIR /src/DockerApp 
RUN dotnet build -c Release -o /app 

FROM build AS publish 
RUN dotnet publish -c Release -o /app 

FROM base AS final 
WORKDIR /app 
COPY --from=publish /app . 
ENTRYPOINT ["dotnet", "DockerApp.dll"] 
  1. 将其修改为使用aspnetcore:2.0而不是aspnetcore:2.0-nanoserver:
FROM microsoft/aspnetcore:2.0 AS base 
WORKDIR /app 
EXPOSE 80 

FROM microsoft/aspnetcore-build:2.0 AS build 
WORKDIR /src 
COPY *.sln ./ 
COPY DockerApp/DockerApp.csproj DockerApp/ 
RUN dotnet restore 
COPY . . 
WORKDIR /src/DockerApp 
RUN dotnet build -c Release -o /app 

FROM build AS publish 
RUN dotnet publish -c Release -o /app 

FROM base AS final 
WORKDIR /app 
COPY --from=publish /app . 
ENTRYPOINT ["dotnet", "DockerApp.dll"]
  1. 再次将代码提交到 GitHub 以启动自动构建:

这一次,您将看到构建成功。

有关.NET 容器要针对哪个操作系统的更多信息,请参阅以下 Microsoft 文档:docs.microsoft.com/en-us/dotnet/standard/microservices-architecture/net-core-net-framework-containers/net-container-os-targets。

  1. 我们现在成功自动构建了 GitHub 项目。切换回 Repo Info 选项卡并记下 Docker pull 命令:

您的图像的 Docker 存储库位于dirkstrauss/dockerapp,Docker pull 命令为docker pull dirkstrauss/dockerapp。

  1. 以管理员身份运行 Windows 命令提示符,输入 Docker pull 命令,然后按Enter键:

您将看到您将开始将图像下载到本地计算机。

如果在拉取 Docker 镜像时收到错误消息 Error response from daemon: Get https://registry-1.docker.io/v2/: net/http: request canceled while waiting for connection (Client.Timeout exceeded while awaiting headers),只需通过右键单击任务栏中的 Docker 图标,单击设置,然后单击重置,然后重新启动 Docker。如果收到类似于 Image operating system "linux" cannot be used on this platform 的错误,您需要切换回 Linux 容器。有关更多信息,请参阅以下 URL:github.com/docker/kitematic/issues/2828。

  1. 现在,我们需要通过输入docker run -d -p 5000:80 [image-repository]来运行容器,这将将容器绑定到端口5000:

  1. 如果要查看容器是否已启动,请运行以下命令:
Docker container ls 

现在您可以看到容器 ID,以及有关正在运行的容器的其他信息:

  1. 我们现在要做的是在浏览器中运行我们在 GitHub 中检查的 ASP.NET Core 应用程序。为此,我们需要找到 IP 地址。在 Windows 10 上,我们需要查找 DockerNAT 的 IP 地址,为此我们需要运行以下命令:
ipconfig 

您将看到定义的 IP 地址是10.0.75.1,这是我们的容器将运行的 IP 地址:

  1. 打开浏览器,输入 IP 地址和端口号10.0.75.1:5000,然后点击Enter。您的 ASP.NET Core 应用程序将在浏览器窗口中以其全部荣耀出现:

使用 Docker Hub 设置 GitHub 以执行自动构建可能一开始看起来有点麻烦,但对开发团队来说好处多多。它允许您始终使用项目的最新构建。

摘要

在本章中,我们看了如何在 Windows 10 Pro 机器上安装 Docker。我们还了解了 Docker 是什么,以及对开发人员的好处。然后,我们看了一下当防火墙似乎是阻碍问题时,如何解决在本地机器上安装 Docker 的设置。然后,我们使用 Docker 创建了一个从头开始就具有 Docker 支持的 ASP.NET Core MVC 应用程序。我们还看了如何将 Docker 支持添加到现有应用程序中。最后,我们设置了 Docker 与 GitHub 集成并执行自动构建。我们还看了如何从 Docker Hub 拉取容器并在本地机器上运行它。

Docker 容器和 Docker Hub 是开发人员可以使用的工具,可以使他们的工作更加轻松。与 GitHub 和 Docker 等流行平台合作的力量将带来增加生产力和盈利能力的好处。Docker 消除了在多台机器上部署应用程序时的所有兼容性问题。

关于 Docker 还有很多要学习的,远远超过一个章节可以说明的。继续探索 Docker 的力量。

posted @ 2024-05-17 17:50  绝不原创的飞龙  阅读(101)  评论(0)    收藏  举报