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

通过 ASP.NET Web API 2... !

starIconstarIconstarIconstarIconstarIcon

5.00/5 (3投票s)

2015年11月21日

CPOL

38分钟阅读

viewsIcon

23901

设计和开发一个简单的 ASP.NET Web API。

引言

我已经很久没有在社区网站上写东西了。这句话并不是说我以前是常客。但我以前习惯把学到的有趣的东西写下来,以便更好地巩固所学,并从那些文章的评论中获得更多知识。

这次我要分享一些关于 ASP.NET Web API 的细节。我知道关于这个主题的文章已经很多了,但我仍然想写一些关于构建一个简单 Web API 的简单内容。

意图

使用以下技术开发一个用于组织 Pot Luck(自带食物聚会)活动的 Web API:

  • ASP.NET Web API。
  • 使用 Strecturemap IoC 容器进行依赖注入。
  • Entity Framework 6。
  • 存储库模式 &
  • Web API 帮助和简单的测试 NuGet。

当你读完本文的最后一行时,你应该能够使用上述内容开发你自己的 Web API。

什么是 Pot Luck?

(别在这里浪费时间:https://github.com/kuttikrishnankodoth/PotLuck/archive/master.zip)。这只是一个简单的案例研究,不如 NerdDinner 那么优雅 :)

对于不知道的人来说,pot luck 是一种与食物有关的聚会活动,由朋友或一群人组织。每个人都会带食物,而不是由一个人为所有人做饭或买食物,然后食物会在人们之间分享。这样,每个人都有机会品尝不同的美食。

组织一次 pot luck 活动有些繁琐,包括协调人员、选择每个人带来的食物,或者分摊任何额外开销等。你会在过程中了解更多。

我将如何进行

我将从没有任何上述花哨的东西开始,但随着我们进展,我会更多地使用它。

开始吧

我的计划是从头开始,这样 Web API 的新手也可以使用它。所以,如果你已经了解了项目设置的基础知识,请跳过本节。

我将使用 Git 作为我的源代码仓库。如果有人计划与少于五人的团队开发项目,Git 和 TFS 在 Visual Studio 中都可以免费使用。此外,我将使用 Visual Studio 2013 Community Edition 而不是 Web Express 或其他付费版本。首先,让我们通过在此中添加一个空的 Web API 项目来设置一个 Visual Studio 解决方案。

下一步,选择一个空的 Web API 项目模板,如下所示,并确保只勾选了 Web API 复选框。反正,我现在不打算添加任何单元测试项目。等我们完成至少一个终结点(操作方法)的开发后,再添加它。

点击“确定”按钮后,你可以在解决方案资源管理器中看到一个出色的项目,其文件夹结构如下。

现在我们有了一个包含空 Web API 项目的解决方案。(顺便说一句,我知道你们在到目前为止的这些步骤中已经很熟悉了,甚至已经精通了,但对于新手,我不想让他们去看别的文章。这是一站式服务。感谢你们的耐心 :)

设置层

像所有典型的应用程序开发一样,我们也使用“n”层架构。“n”层是因为我自己也不确定要创建多少层。我将创建的所有层都将是类库项目。我将解释如何创建一个层,其余的层你可以通过遵循相同的过程来创建。

如你所知,右键单击解决方案并添加新项目,如下所示。我正在创建的是用于数据访问和保存数据库实体的,因此我称之为“WebApi.Kod.PotLuck.DbEntities”。

正如我所示,你需要创建以下提到的层。我创建了大约七层。你可能会想,为什么这个简单的应用程序需要七层?无论应用程序是简单的还是复杂的,我们都应该为应用程序的可扩展性留一扇门。请始终记住,应用程序总有一天会发展壮大。所以你想确保你有一个坚实的基础,为美好的未来 :)

看看我创建的层,我们在使用它们时会逐一解释。创建完这些项目后,从所有项目中删除默认的 Class1.cs 文件,因为“我不喜欢它”。

现在我们的应用程序的骨架已经搭建好了。准备好开始编码了吗?还没?嗯!好吧,我们可以做一些其他的设置。

设置 Entity Framework 6

在我们开始编写任何应用程序之前,我敢肯定,每个开发者都会想知道数据从哪里来,又存储在哪里。对于这个应用程序,我将使用 **SQL Local Db** 而不是 **SQL server**。这样,使用本文档的开发者就能轻松地下载和配置应用程序,而不会出现任何问题,而且如果你也在使用 SQL Server DB,也可以很容易地替换它。

下一个问题:我们将如何访问数据?我们将使用 *Entity Framework 6.0* 进行数据访问。Entity Framework 可以作为 NuGet 包使用。我们只需下载它并安装到我们刚刚创建的所有项目中。我说的是所有项目,因为在某种程度上,我们将在几乎所有项目中使用 Entity Framework 的一些组件。

所以,让我们继续下载并从 NuGet 安装它。

点击管理 NuGet 包,会打开一个窗口,如下所示,然后在搜索框中搜索 Entity Framework。确保你已经选择了所有用红色框标记的项。

点击安装按钮,它会提示你选择要安装框架的项目。由于我只想在存储库层安装它,所以我只勾选了存储库项目。

点击确定并接受许可协议后,Entity Framework NuGet 包将安装到你的项目中。

同样,我们需要为 **Local DB** 添加另一个 NuGet 包,但我们只需要将 Local DB 放在主 Web API 项目中。

你可能会想,安装 NuGet 包是什么意思,对吧?对于不熟悉 NuGet 的人来说,它只是从在线 NuGet 服务器下载包,添加所需的 DLL,并对项目配置文件进行必要的修改。如果该包需要,它还会添加一些文件和文件夹。NuGet 包管理器会为我们处理所有这些繁琐的任务,而不是我们手动完成所有复杂的、耗时的步骤。是不是很棒?

我们已经完成了编码的基本需求。

创建 Code First 数据库实体

我们将为这个应用程序使用 Code First 方法。要创建 Code First 模型,首先我们需要决定数据库表的外观和所需的列。然后,我们需要为这些已识别的表和列创建相应的数据库类。所有数据库模型都将添加到 **WebApi.Kod.PotLuck.DbEntities** 项目中。现在你可能会想,“你要添加数据库模型,为什么项目名称是 DbEntities?”对吧?我倒是觉得,你可以把项目命名为 DbEntities 或 DbModels,这都取决于你。

起初,我将为我们的目的创建两个表,然后我们可以根据需要随时添加功能。我的两个表将是

  1. PotLuckEvent
  2. 成员

一个事件可以有许多成员,一个成员可以加入许多事件。表之间的关系将是 **多对多**(稍后会讨论)。所以,我将在 DbEntities 项目中添加这些类以及所需的属性(列)。我们还需要做的一件事是确定我们期望的两个表中的列。然后创建一个抽象类,并将所有公共属性放在抽象类中。

将此抽象类作为我们要创建的所有实体的基类。我创建了一个抽象类 `BaseEntity`,它具有以下显示的属性(列)。

如上所示,**Id** 字段将是表的**主键**,其余的属性顾名思义(可以称为审计字段)。我也为另外两个表创建了实体。

如图片 11 和 12 所示,我已经继承了 `BaseEntity` 抽象类。现在实体已经准备好了。下一步是创建一个数据上下文。

创建数据上下文

什么是数据上下文?MSDN 对数据上下文的定义是:

“负责将数据作为对象进行交互的主要类是 **System.Data.Entity.DbContext**(通常称为上下文)。上下文类在运行时管理实体对象,包括将数据从数据库填充到对象、更改跟踪以及将数据持久化到数据库。”

https://msdn.microsoft.com/en-us/data/jj729737.aspx

这个定义非常简单明了,易于理解。

正如 MSDN 链接所示,我们也将在 DbEntities 项目中创建一个上下文类,其中包含我们的两个实体。

我创建的上下文不是使用 **Dbset** 的具体实现,而是使用 **IDbset**。我更喜欢针对接口而不是具体类进行编码。创建一个数据上下文非常简单,我只是在我们的 DbEntities 项目中添加了一个继承自 **System.Data.Entity.DbContext** 的类,并添加了所需的 Db 设置。我做的另一件事是,我在项目中添加了一个额外的文件夹来保存 Db 上下文,以便于组织。上下文看起来如下(图 13)。

设置关系和配置属性

在 Entity Framework 中,关系和映射/配置可以通过两种方式完成。第一种是使用 Fluent API,另一种是使用数据注解。

这里我将使用 Fluent API 类型,因为在复杂场景下(例如,你尝试使用 Code First 处理现有数据库)Fluent API 可以解决比数据注解更多的问题。为了配置,我将为我们创建的每个模型添加一个配置类。

使用 Fluent API 设置复合键和各种类型的键非常容易,创建和管理索引也不复杂。

对于配置,我在同一个 DbEntities 项目中创建了一个名为 `DataBaseConfiguration` 的文件夹。我将在其中添加两个类;一个用于每个模型,如我所述。我将在这些文件中编写所需的配置;配置类将继承自 Entity Framework 的 `System.Data.Entity.ModelConfiguration.EntityTypeConfiguration<T>` 类。我的配置类看起来如下面的图片。

如果你看 **图片 14**,你会看到我将主键设置为了数据库生成的标识列,第二行 **HasKey** 将表明 **Id** 字段是主键。在 Entity Framework 中,不必显式指定主键。默认情况下,Entity Framework 会将名为 **Id/ID** 的列/属性或后跟 Id 的表名识别为主键。

尝试为另一个实体执行相同类型的配置。希望你成功了。

下一步是配置表之间的关系。我已经提到我们将采用 **多对多** 关系。例如,一个 Potluck 活动可以包含任意数量的成员,而一个成员可以加入任意数量的 Potluck 活动。:)

为了存储相关实体,我们必须在两个实体中都具有导航属性。我们必须将导航属性声明为虚拟属性。

在 Member 模型中,你可以像下面这样为 Potluck Event 创建 `ICollection` 类型的属性,反之亦然,在 Member 模型中。代码看起来如下。

public virtual ICollection<PotLuckEvent> MyEvents { get; set; }

重要事项!不要忘记在模型构造函数中实例化导航属性。现在两个模型看起来是这样的。

看看图片 15 中的构造函数;你可以看到我使用 HashSet 实例化了导航属性。(**为什么是 HashSet?**)我们已经基本完成了模型。接下来是设置关系。我将在之前创建的实体配置文件中进行此操作。由于我们计划采用多对多关系,所以在其中一个实体配置文件中设置关系是可以的。这里我出于某种原因倾向于在 *PotLuckEventConfig* 文件中进行设置。

正如我们所知,对于 **多对多** 关系,我们需要一个 **连接表/联接表** 来映射多对多关系。让我们看看我们将如何做到这一点。

看看高亮显示的 **HasMany** 部分,那是我们连接关系的部分。试着读一下。你能明白什么吗?只是简单的英语,对吧?:)

引用

这样读:*PotLuckEvent 有多个 EventMembers,与多个 MyEvents(PotLuckEvent 类型)关联。*

很简单,对吧?就是这样。

让我们看看第二部分 **Map**。这是创建 **连接表** 的部分。代码实际上要实现的是将关系映射到一个名为“EventMember”的连接表,并带有两个列:PotluckEventId 和 MemberId。你不必把列命名成我这样。你可以使用任何你心仪的命名约定。

在这里,左边的列将与 PotLuckEvent 的 PK 映射,因为我们在 PotLuckEvent 的配置文件中创建了关系。要了解更多关于 EF 关系的信息,你可以阅读下面的 MSDN 链接:https://msdn.microsoft.com/en-us/data/jj591620.aspx

所以,模型和关系都已完成。现在我们尝试创建具有相同数据的数据库。

一件小而重要的事情我们忘记了:我们创建了所有的配置等等,但我们忘记告诉 **DbContext** 我们创建了配置文件,并且你必须在接下来的模型中使用这些文件。嗯。要实现这一点,我们必须重写上下文的一个方法,并在该方法中编写管道代码。

完成!

使用测试数据设置数据库(Seeding)

实体、上下文和关系都已完成。在继续之前,我们需要确保数据库正在按预期创建。为此,让我们创建一个数据库初始化器。

引用

什么是数据库初始化器?我们只需在应用程序加载时根据条件用数据初始化数据库。

同样,我在同一个 DbEntities 项目中创建了一个名为 `DbInitializer` 的附加文件夹,并在其中添加了一个名为 `PotLuckDbInitializer` 的文件。

正如你下面看到的,这个初始化器可以继承自两种不同类型的 **System.Data.Entity.DbContext**。这取决于何时(条件)我们必须初始化数据库。

图 18 显示了两个类。一个将帮助我们创建一个数据库初始化器,它会在每次运行应用程序时删除并重新创建数据库。另一个只会在模型发生变化时(如列名更改、数据类型更改等)删除并重新创建数据库。更简洁地说,无论何时发生模式或类型更改。现在我们可以使用第二个。只需在模型发生更改时更新并创建数据库。一个重要的事情是,我们必须将我们的数据上下文作为类型参数传递给基类。

下一步,我们需要在子类中重写基类的一个名为 `Seed` 的虚拟方法。在该方法中,我们必须编写代码来填充一些样本数据。

namespace WebApi.Kod.PotLuck.DbEntities.DbInitializer
{
    public class PotLuckDbInitializer : DropCreateDatabaseIfModelChanges<PotLuckContext>
    {
        protected override void Seed(PotLuckContext context)
        {
            var members1 = new List<Member>
            {
                new Member{AddressLineOne="Test Address 1",City="Test City",ContactPhoneNumber="123-777-7656",CreatedBy=1,CreatedTime=DateTime.Now,Email="test@gmail.com",FirstName="Test",HouseNumber="1234",LastName="Test LastName",LastUpdatedBy=1,LastUpdatedTime=DateTime.Now,NickName="Test Nick",State="CT",Zip="17112"}
            };
            var members2 = new List<Member>
            {
                new Member{AddressLineOne="Test Address 2",City="Test City",ContactPhoneNumber="123-777-7656",CreatedBy=1,CreatedTime=DateTime.Now,Email="test@gmail.com",FirstName="Test",HouseNumber="1234",LastName="Test LastName",LastUpdatedBy=1,LastUpdatedTime=DateTime.Now,NickName="Test Nick",State="CT",Zip="17112"}
            };
            var plEvent = new List<PotLuckEvent>
            {
                new PotLuckEvent{CreatedBy=1,CreatedTime=DateTime.Now,EventAddressLineOne="Main Street",EventCity="Maryland Heights",EventDateTimeFrom=DateTime.Now,EventDateTimeTill=DateTime.Now,EventDescription="Test",EventName="Test Event 1",EventState="MO",EventZip="63043",LastUpdatedBy=1,LastUpdatedTime=DateTime.Now,EventMembers=members1},
                new PotLuckEvent{CreatedBy=1,CreatedTime=DateTime.Now,EventAddressLineOne="Main Street",EventCity="Maryland Heights",EventDateTimeFrom=DateTime.Now,EventDateTimeTill=DateTime.Now,EventDescription="Test",EventName="Test Event 2",EventState="MO",EventZip="63043",LastUpdatedBy=1,LastUpdatedTime=DateTime.Now,EventMembers=members2},

            };

            plEvent.ForEach(x => context.PLEvent.Add(x));
            context.SaveChanges();
        }
    }
}

上面是我用来填充样本数据的源代码。我重写了 `DropCreateDatabaseAlways` 类中的 `Seed` 方法,还创建了两个成员和一个事件列表。如果你仔细看 PotLuckEvent 列表,你会发现我将 Member 列表分配给了 Event 列表的 EventMembers 导航属性。我之所以这样做,是因为我已经正确设置了所有关系,所以提供的数据应该会填充 Member 表。总之,让我们看看会发生什么。

你还可以在代码最底部看到,我正在遍历事件列表并将数据保存到上下文中。

配置 Local DB

我在本文档开头提到过,我将使用 Local Db 作为此应用程序的数据库。让我们看看如何配置我们的应用程序以使用 Local DB。为此,我们需要下载 Local DB Provider 的 NuGet 包。你可以从 NuGet 包中找到该提供程序,如下所示。

然后安装它。

默认情况下,它会在你的 Web Config 文件中添加一个默认连接字符串。如果你想更改连接字符串的名称,请在 Web Config 文件中进行更改,并且不要忘记从 DB Context 中更改连接字符串的名称。

NuGet 包创建的默认连接字符串将如下图 20 所示。在这里,我将使用连接字符串名称 **PotLuckConnection**。并且我在 Db Context 类和 Web.config 文件中都进行了更改。现在我的文件看起来像图 21。

就是这样

一个问题:如果我想使用 SQL Server 或 SQL Server Express 而不是 Local DB,我需要做哪些更改?希望你明白了!

测试数据库创建和关系

为了测试我们到目前为止所做的设置是否正常工作并按预期创建具有表的数据库,我将在 WebAPI.Kod.PotLuck 项目中创建一个默认控制器,并尝试从数据库中获取所有记录。通过这样做,我的期望是:

  • 应用程序应创建一个 Local Db 实例。
  • 应用程序应在数据库中创建三个表,如下所示。
    • PotLuckEvent
    • Member &
    • EventMember
      • PotLuckEventId(列 1)
      • MemberId(列 2)
  • 应用程序应使用我们在 seed 中提供的数据填充所有三个表。
  • 应返回数据库中的记录。

为了实现这一点,我将不遵循任何分层方法来调用 db context,我将直接从控制器调用 Potluck Context,如下所示。

构建并尝试运行应用程序。我希望你能毫无错误地构建应用程序。好的!很好。我将使用 **Fiddler** 来访问 Web API。(我们有其他 REST API 测试工具,例如 Google Chrome 插件 Postman。)让你的 Visual Studio 应用程序保持运行状态,然后使用 Fiddler 访问 API,如下所示。

好的!只看我使用的 URL https://:55523/api/Default,并将请求类型设置为 **GET**。它让我等待了几秒钟,然后返回了一个异常,如下所示。

异常是关于对象序列化的,说“**检测到自引用循环**”。这是因为 **JSON** 无法序列化带有循环引用的对象。

什么是循环引用?

是的。这是我们的多对多关系导致的问题,例如成员有事件,事件有成员,所以在尝试序列化时会混淆序列化引擎。因此,我们必须在 `WebApiConfig.cs` 文件中添加两行代码来修复它。我添加了以下代码行。

json = config.Formatters.JsonFormatter;
json.SerializerSettings.ReferenceLoopHandling = Newtonsoft.Json.ReferenceLoopHandling.Ignor;

现在我的代码看起来像下面显示的图片,图 24。

现在尝试运行应用程序。是的。问题解决了,我得到了一些预期的响应。这意味着数据库正在正确创建并填充数据。并且正在按预期返回它。

你可以看到在抽象类中定义的 Id 和 Created datetime 等也在这里创建了。但是,我们必须检查数据库的创建位置和表的名称,对吧?当然!让我们看看表的样子。为此,我们需要进入 Visual Studio 解决方案资源管理器,点击“显示所有文件”图标,然后打开 `App_Data` 文件夹。你会看到一个 **.mdf** 文件被创建了。双击它,它将在右侧打开一个**服务器资源管理器**窗口,其中包含数据库和表。所以你可以看到服务器资源管理器中的所有表,如下图所示,图 26。

希望你到目前为止都享受到了学习的乐趣!现在到了有趣的部分。:)

创建模型/业务实体

到存储库的通信应该是业务实体,也可以称为模型或业务模型。所有业务操作都将在业务逻辑层的模型上执行。

为什么引入模型?

主要目的是避免数据库相关实体无意中泄露到业务层。数据库相关项泄露到 BLL 会使其与当前数据库存储库紧密耦合。我这里的计划是使其尽可能独立。所以,一个数据对象在离开控制器/Web API 之前会被转换为三种对象。

因此,根据上图,我们已经有了数据库实体/实体,我们一开始创建的。接下来,我们需要创建业务模型。你可以使用的业务模型/实体可以与数据库实体相同,但我仍然倾向于为业务模型设置一个单独的部分。我上面解释的一个原因是,有时你要使用的数据库的命名约定可能不是你想要的,或者出于特定的业务原因,你可能需要为数据库实体的一个字段使用不同的名称,或者你可能不想将数据库中的所有字段都暴露给业务模型等等。原因有很多。作为最佳实践和可扩展的设计,将业务层与存储库分开拥有不同的模型总是更好的。

引用

你的应用程序越复杂,中间类型的用处就越大。

如果你想修改存储库,或者要引入一个新的存储库和具有新命名的数据库实体,你所要做的就是将新存储库的输出插入到业务模型中。只需更改映射即可。你根本不需要触及业务层。

所以,模型就完成了。

从上面的快照中,你可以看到我已经删除了最后的更新和创建字段等审计字段。我还通过在 ID 字段前面附加模型名称来重命名它们。

存储库

什么是存储库? 

我认为存储库是应用程序进行数据访问的几种方式/模式之一。它解决了关注点分离、可测试性、缓存等问题。它隐藏了其他接口层的数据访问逻辑。

我知道我们大多数人都熟悉数据访问层,那么数据访问层和存储库层有什么区别呢?我想说,“由于我在 DAL 中实现了存储库模式来进行数据访问,所以我称我的 DAL 为存储库层。” 更精确的答案是:实现存储库模式的数据访问层称为存储库层。最终,DAL 和存储库都在努力实现同一个目标,即“**关注点分离**”。我在存储库模式中注意到的一个优点是,它使得单元测试更容易,因为我们可以使用一些模拟框架轻松地模拟存储库。

我还听说存储库是领域驱动设计(DDD)的一部分。由于我对该方法不熟悉,所以无法就存储库和 DDD 之间的联系发表任何看法。抱歉。

**请注意:**我上面所说的一切都严格基于我个人的理解和经验。

你可以在这里阅读更多关于存储库的信息:https://msdn.microsoft.com/en-us/library/ff649690.aspx

首先创建通用存储库接口

要创建存储库,我们首先要识别我们需要针对一个实体执行的所有操作。非常基本的数据访问操作如下所示,其余的将是这些操作的组合。

  • 创建
  • 读取
  • 更新 &
  • 删除

也称为 **CRUD** 操作。在我们的存储库中,我们可以遵循相同的模式。既然我们已经确定了操作,现在我们就需要创建存储库接口。为什么是接口?

  • 针对接口编码比针对实现/具体类编码要好。
  • 将使 IOC 和 Mocking 更容易

这里我们现在将创建两个接口

  • IPotLuckEventRepository
  • IMemberRepository。

所以我已经完成了我们的存储库接口,其中包含一些基本方法签名。我创建的方法如下所示。

  • GetAsync
  • InsertAsync
  • UpdateAsync
  • 删除

你可能已经注意到所有方法末尾的 Async。为什么?因为我们将使用 **Task**,async 和 await 来提高应用程序性能和响应能力。

好的,看看上面的图片。我高亮了 `GetAsync` 方法,这是一个非常强大且可重用的方法,因为它使用表达式作为输入参数,并且返回类型是 `IEnumerable`。

  • 什么是表达式?
  • 使用 IQueryable 的好处是什么?

让我们来实现它

我创建了泛型接口,但我将创建具体的存储库。为什么?好问题。因为我只是想避免另一个映射层。而且,通过拥有一个通用存储库,我可能可以避免从数据库中选择不必要的字段。最后,避免数据库实体泄露到 BLL。

让我们继续在我们的 Member 存储库中实现它。

public class MemberRepository : IRepository<MemberMdl>

我高亮显示的部分是从 DbEntities 映射到业务模型的映射区域。因此,我们将只查询所需的字段,数据库服务器将只返回那么多数据。

图 **32** 可以重构为 **32-A**。我提取了方法选择语句的投影,并将其创建为一个 **Expression AsMemberMdl**,并将其用作 Member Dbset 的选择器。顺便说一句,我们可以避免存储库方法的一些笨拙之处,并且 **AsMemberMdl** 表达式可以在同一个类中用于其他方法。

我也为 Event 存储库实现了同样的功能。让我们继续在业务层实现 Manager。

编码 BLL

每当我们开始编写任何新的层实现时,我们需要找到一个地方来存放所有常用和频繁使用的变量和功能。

这里我们也需要编写一个通用的基类,用于存放 Manager 层的存储库。我说通用基类是因为,对于一个 `EventManager` 类,我们希望存储库属性保存 event 存储库,而对于 MemberManager,我希望存储库属性保存 Member 存储库。

我不想有一个包含所有存储库的属性,并向不必要的管理器类公开存储库。例如,我不想在 EventManager 中看到 MemberRepository,反之亦然。

一言以蔽之:“**抽象**”。只公开必要行为。

让我们看看这样的基类应该是什么样子。

这是一个非常简单的基类,仅用于存放存储库并添加一层抽象。此外,我希望所有管理器类都遵循使用 **Repository** 变量的标准,而不是使用许多其他不同的命名约定。将来,如果你想存放缓存对象或属性,你可以将这些东西存放在基类中。

引用

你的脑海中可能会出现一个问题:如果我想在我的管理器类中使用多个存储库怎么办?我的回答是:*只要你使用一个具有正确关系设置的数据库,你很少会遇到在管理器类中使用多个存储库的情况。如果出现这种情况,你只需将其作为构造函数注入,并将其分配给管理器中的另一个存储库属性。*就是这样。

实现基管理器类

我刚刚通过实现我们刚刚创建的 BaseManager 来创建管理器类。我还从它提取了一个名为 `IMemberManager` 的接口,以便我们可以在控制器中使用 `IMemberManager` 接口进行编码。

我总是先创建具体实现,然后从中提取接口,因为 Visual Studio 让这变得很容易。我们不必手动编码接口。点击类名,然后按 **Ctrl+R+I** IDE 会为你创建一个接口(这只是个人偏好)。

在图 34 中,你可以看到我还实现了 `GetEvents` 方法,该方法内部使用了基类中的 Repository 属性。

同样,我希望你能创建一个 Event 的管理器类。下一步是在 Controller 中调用管理器方法。

Controller:编排者

与 MVC 控制器不同,Web API 控制器继承自 ApiController Base 类型。每当你创建一个 Web API 项目时,都应该将其创建为一个独立的,不包含 MVC 组件的项目,如 **图 3** 所示。

我将去创建一个 API 控制器和 Member 的操作方法。在创建控制器时,请始终尽量保持其精简。尽量不要在其中包含任何业务逻辑,因为测试控制器有点困难。我成功创建了一个控制器,它看起来是这样的。

你可以看到 Members 控制器的构造函数接受 Manager 的依赖项,然后存储库从 Manager 类注入到 Manager 中。

经验法则:你不得在 Web API 项目中添加存储库的引用。不要将任何存储库相关对象泄露到 Web API 层。一切都应该在 BLL 结束。因此,Controller 可以重写为。

就我们目前的业务逻辑层而言,这里没有太多内容。如果我们有什么内容,所有操作,如数据清理和整理,都应该在 BLL 中进行,而不是在控制器中。

控制器的作用只是传递请求,并将管理器返回的 Model 转换为 DTO 作为响应,然后通过网络发送。

试运行

让我们运行此应用程序,看看它是否按预期工作。我已经完成了 Events 和 Member 控制器的创建。让我们运行应用程序。这里我尝试使用 POSTMAN 来运行 Events 控制器。Postman 是一个 Google Chrome 扩展。你可以从 Google Chrome 扩展商店下载它。它是一个非常易于使用的工具。

正如我所说,我得到了一个精彩的异常。上面图片中高亮显示的部分。错误如下。

“ExceptionMessage”:“尝试创建类型为‘EventsController’的控制器时出错。请确保控制器有一个无参数的公共构造函数。”

**尤里卡!** 我明白了!但是我们还没有解决存储库的依赖关系,所以现在让我们继续解决所有控制器的依赖关系。我们解耦了所有东西,但忘记了解决依赖关系。嗯……真 sad。

使用 StructureMap 进行依赖解析

Structure map 是一款高性能且易于使用的 **IoC** 容器。Structure map for Web API 可以作为 NuGet 包提供。NuGet 包使配置 Structure Map 变得简单易行。转到 **工具 -> NuGet -> 包管理器 -> 包管理器控制台**

PM> Install-Package StructureMap.WebApi2.

这将把 structure map 安装到我们的 web API 项目中。我们只需要将 structure map Web API 2 安装到 Web API 项目中。请确保将默认项目选为 Web API 项目。这只是另一种安装 NuGet 包的方式,超出了我之前提到的范围。安装包后,如果你查看解决方案,你会看到一些文件和文件夹被添加进来,如下所示。

这些都是 Structure map 的支持文件。在这些文件中,我们最感兴趣的是 `IoC.cs`。IoC 文件是来自以下层的依赖解析类的汇聚点。你很快就会明白这句话。下一步,我们需要将不同版本的 structure map 安装到业务逻辑层和存储库中。在下面的图片中,你可以看到 NuGet 包的详细信息。

现在,在两个项目中添加一个名为 `DependencyResolution` 的文件夹,并在其中添加一个类文件。你可以在业务项目中将类文件命名为 `BusinessRegistry`,在存储库项目中命名为 `RepositoryRegistry`。

所以,我们必须在这些文件中写几行代码。让我们先来看存储库注册。在该文件中,我们必须告诉 structure map:*每当管理器类请求特定类型的存储库(接口)时,请为 Manager 提供实现了所请求接口类型的该特定存储库。* 语法如下。

从上图可以看到我创建的文件夹和文件。需要注意的一个重要点是,新类应该继承自你刚刚安装的 structuremap NuGet 包中的 **Registry** 类。另外,请确保你使用的是两个已高亮显示的命名空间。

 using StructureMap.Configuration.DSL;

好的。我们完成了这个存储库注册,现在让我们以同样的方式修改业务注册中的文件。但是你不需要添加 **For<>, Instead**,你必须添加 **Scan**。

Structure map 的扫描足够智能,可以找出实现调用者期望的接口依赖的类。

那么,为什么我们在存储库注册中显式添加 `For<>()Use`?因为 structure map 不够智能,无法捕捉我们像 `IRepository<EventMdl>` 这样的依赖关系。

StructureMap Scan 遵循命名约定

在 BLL 中,图 38-C 中显示的扫描将解决我们的依赖识别问题,并且我们不必显式地指定 `For<>()Use`。但是,我们必须在 BusinessRegistry 中添加一行额外的代码,而不是上面提到的代码。那就是在 BusinessRegistry 中包含 RepositoryRegistery。

你需要使用一些额外的命名空间才能让 Structure Map 进行扫描,如下所示。

using StructureMap.Configuration.DSL;
using StructureMap.Graph;

最后一步

现在我们必须告诉 Web API Structure map,我们在其他层还有其他依赖解析注册。

在这种情况下,我们不显式地将 Web API 依赖项解析器告知存储库注册,但我们只需提及业务注册,因为我们已经通过图 38-C 中的 include 语句告知了业务注册有关存储库注册的信息。他会处理这些,从而避免了在 Web API 层引用存储库项目,甚至是为了提及依赖项解析器注册。

我们需要编写这个代码来告诉 WebAPI 依赖项解析器的地方是 `IoC.cs` 文件,该文件位于 WebApi 项目的 DependencyResolution 文件夹中,如下所示(38-D)。

好的,我们完成了。

让我们再试一次运行

太棒了。对我来说运行得很顺利。你呢?

数据传输对象 (DTO)

我们的 Web API 现在运行正常,但我仍然觉得我们遗漏了什么。是的。看看我们创建响应对象的地方。我们正在通过网络发送 Model。嗯。我们不应该通过网络传输业务模型,只传输 DTO。

引用

那么 DTO 是什么?根据维基百科:DTO 是 **数据传输对象**(**DTO**)是传递数据在进程之间。

那么它的特殊之处是什么?

根据维基百科:数据传输对象与 **业务对象** 或 **数据访问对象** 之间的区别在于,DTO 除了存储和检索自身数据(**访问器** 和 **修改器**)之外,没有任何行为。DTO 是简单的对象,不应包含任何需要测试的业务逻辑。

让我们去写一些 DTO 吧。在我们的例子中,我们可以使用相同的业务模型,但根据我们的理论,DTO 不应包含任何需要测试的业务逻辑。

我在 DTO 项目中创建了两个类,名为 `EventDTO` 和 `MemberDTO`。然后我将相同的属性从 Model 复制到相应的文件,但我没有复制集合属性。下一步,我们需要编写一些扩展方法来将业务模型转换为 DTO。

我们将把这些扩展方法放在 Extension 项目中。

你可能会想,“我为什么要为编写扩展方法创建一个单独的项目?”原因是我只想在控制器中看到模型的扩展方法。此外,它不应该与业务模型绑定。因为 DTO 可能因 API 而异。这取决于你想通过网络传输什么,以及你的客户端想看到什么。

如果你将扩展与业务模型绑定,它将影响业务层的可重用性。就像你为想使用同一 BLL 的其他应用程序提供了不必要的方法。另一方面,如果其他应用程序想使用相同的 DTO,它们可以简单地引用 FTO 项目,并使用 DTO 和扩展项目以及业务逻辑层。我们只是将它分开打包,以便可以单独使用。

希望你现在已经明白了。

AutoMapper

在将数据从一种形式转换为另一种形式时,我们需要编写大量的代码。为了避免这些繁琐的手写代码,我们必须再次借助 NuGet。NuGet 有一个名为 AutoMapper 的包。我们必须像往常一样下载并安装它。

Install-Package AutoMapper

只将此包安装到扩展项目(你也可以将其放在存储库中)。好的,我已成功安装。让我们看看现在的情况。

进入 `Models.Extensions` 项目,为 `EventExtensions` 和 `MemberExtensions` 创建类文件。让我们先看 Event Extensions...

我写了两个扩展方法,一个用于 `EventMdl`,另一个用于 `IEnumerable<EventMdl>`。看看这些方法,映射语法多么简单。有关 AutoMapper 的更多阅读,请访问以下链接:https://github.com/AutoMapper/AutoMapper/wiki/Getting-started。有一件事我想指出的是,代码量减少的代价可能是性能下降。我不确定。

Yield return 是什么?

让我们在控制器中使用它。

引用

请记住,在将业务模型通过网络发送之前,将其转换为 DTO。

因此,最佳转换位置是在控制器中。

让我们运行应用程序,看看它是否有效。别等我,自己动手吧。

对我来说有效。我收到一个 DTO 格式的响应。你呢?好的,我希望你也得到了相同的结果。太棒了!

尽管如此,我还是感觉有点奇怪。看看返回 `IHttpActionResult` 的控制器操作,我无法判断它通过网络发送的数据类型。我认为我们必须解决这个可读性问题。<sh4>怎么解决?

更改操作方法的返回类型是一种方法。另一种标准方法是用 `System.Web.Http.Description` 命名空间中的 `ResponseType` 属性来装饰操作方法。我更喜欢第二种方法。

我装饰了操作方法,现在它看起来是这样的。

好的,现在我一眼就能轻松看出这个操作的返回类型是什么,很有趣,对吧?我也为 Member 应用了同样的方法。两个控制器对我来说都工作得很好,并吐出 DTO。

引用

DTO 和业务模型是解决序列化循环引用问题的最佳良药!

现在你可以尝试删除我在 **图 24** 中提到的代码,然后尝试运行应用程序。应用程序将正常运行。

救命!救命!拨打 911

现在我们已经成功完成了 Web API,只需考虑消耗应用程序。没有适当的文档,它们如何能够消耗?它们需要一些关于 API 的信息,对吧?某种文档或 Wiki 类?嗯。再次,准备 API 文档是一项繁琐的任务。

这里 NuGet 再次可以帮助我们。有一个名为 Web API help 的 NuGet 插件包。魔语是什么?

PM> Install-Package Microsoft.AspNet.WebApi.HelpPage.

让我们继续安装这个包,看看这个家伙如何为我们提供帮助文档(确保你只在 Web API 项目中安装)。

安装后,我在 Web API 项目的 `Area` 文件夹下看到了一堆文件被创建了。

MVC 中的 Area 是什么?

别害怕,你不需要做任何事情,除了简单的调整。

  1. 我们要做的第一件事是去 `Global.asax` 文件中注册 NuGet 包创建的 Area。
    public class WebApiApplication : System.Web.HttpApplication
        {
            protected void Application_Start()
            {
                GlobalConfiguration.Configure(WebApiConfig.Register);
                AreaRegistration.RegisterAllAreas();
            }
        }
  2. 在 `App_Data` 文件夹中添加一个名为 `XMLDocument.xml` 的 XML 文档(名称随意)。
  3. 如下图所示配置 Web 应用程序属性中的 XML 文档。

     

  4. 取消注释下面显示的这行代码。

     

     //// Uncomment the following to use the documentation from XML documentation file.
    config.SetDocumentationProvider(new XmlDocumentationProvider(HttpContext.Current.Server.MapPath("~/App_Data/XmlDocument.xml")));

现在运行应用程序。

使用 URL https://:55523/help。我得到了一个精彩的异常,如下所示。如果你运气好的话,你不会遇到这个异常。

看起来这与依赖注入有关。在 Google 上搜索了很长时间,我找不到这个问题的解决方案。你只需要在 StructureMap 默认注册文件中添加一行代码,如下所示。

现在运行应用程序。万岁!我的帮助页面开始显示了。很好,但现在我默认没有跳转到帮助页面,它把我带到了一个崩溃般的页面。

我现在必须输入 https://:55523/help 才能看到帮助页面。我不喜欢这样。我想在运行应用程序时将其设为默认。为此,我们需要做一些小的调整。

WebApi.Kod.PotLuck\Areas\HelpPage\HelpPageAreaRegistration.cs--> 打开它

 context.MapRoute(
            "Help Area",
            "",
            new { controller = "Help", action = "Index" }
            );

你必须在 `HelpPageAreaRegistration.cs` 中添加框起来的代码。现在运行应用程序。你将直接跳转到帮助页面。你的帮助页面看起来是这样的。

你右侧看到的描述来自你在 `Action` 方法顶部提供的摘要。

现在我们可以直接将 API 的 URL 提供给任何人,而无需任何额外的文档。

我们现在有了一个完整的 API,但如果我们想测试 API,我们就必须通过 POST Man 或 Fiddler 或其他 API 测试工具。值得庆幸的是,我们还有一个来自 NuGet 的插件用于测试 Web API。是的。

简单测试

同样,这是一个 NuGet 包。只需从 NuGet 安装并使用它。魔语是什么?

PM> Install-Package WebApiTestClient

只需为 Web API 项目安装此包。

在使用包管理器控制台安装包时,我遇到了一个异常。安装时,请确保你以管理员模式运行 Visual Studio,否则你会遇到异常。只需为 Web API 项目安装此包。

我安装了它,安装后,打开了一个文本文件,要求我在 `Areas\HelpPage\Views\Help\Api.cshtml` 中添加两行代码。

@Html.DisplayForModel("TestClientDialogs")
@Html.DisplayForModel("TestClientReferences")

我只是复制了上面的两行并粘贴到 `Api.cshtml` 文件中,如下所示,然后运行了应用程序。它再次将我带到 API 登录页面,但我在那里没有发现任何新的东西。

点击链接后,它会打开一个浏览器窗口。在浏览器窗口的**右下角**,你会看到一个标有**测试 API** 的按钮。

点击按钮。会弹出一个窗口。那是你要测试的特定终结点(操作方法)的测试目的地。

太棒了!

到目前为止,我们创建的所有方法都没有输入参数。只是为了向你展示测试窗口如何处理带有输入的终结点,我将编写一个简单的终结点,它接受一个 `int`。

我像往常一样执行了 API,它将我带到了登录帮助页面。从那里,我点击了我需要测试的终结点(在这种情况下)接受 `int` 作为输入的那个。测试弹出窗口和结果如下所示。

点击发送后,我们将在另一个弹出窗口中收到响应。

参考文献

结论

WEB API 真是太棒了。谢谢!我试图尽可能多地涵盖一个初学者开始编写可扩展的 Web API 所需的内容。

非常感谢你阅读这篇文章。如果你发现任何错误或可以改进的地方,请在下面的文章论坛中留下。

另外,我计划在下一篇文章中介绍 Web API 的异常处理和安全性。我将在接下来的部分中尝试包含 POST 操作。

再次感谢你的阅读。

© . All rights reserved.