四、控制器、动作和模型

服务的入口点和实体是控制器。虽然处理程序是 ASP.NET Core 管道中的初始类之一,但一旦请求通过 ASP.NET 并找到合适的路由,它将被定向到控制器。

现在,您可以控制要作为响应发送的数据。控制器可以包含许多方法。尽管这些可能是公共方法,但并非所有方法都可用。在这些方法上启用 HTTP 操作将使这些方法变成操作。

在本章中,您将更好地了解控制器以及它们如何与 ASP.NET 管道相结合。我们将创建一些控制器以及这些控制器的操作。

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

  • 控制器简介
  • 行动
  • 使用模型创建控制器

控制器简介

当您向项目中添加新控制器时,ASP.NET 会自动为您加载该控制器并使其准备好使用。以下是一些您可能想知道的指针,以便您不会陷入困境,或者如果您想创建一个新项目并将所有控制器都放在其中:

  • 您的控制器需要以单词Controller结尾。
  • 确保你的班级是public;不用说,接口和抽象类将无法工作。从 Microsoft 控制器类继承它们。
  • 不同名称空间中不能有相同的控制器名称。ASP.NET 允许同一控制器使用多个名称空间,但不会解析两个控制器。最佳做法是为控制器指定唯一的名称。

行动

web API 有很多动作,其中一些动作在第 2 章理解 HTTP 和 REST中介绍,并附有示例。作为复习,我们将再次讨论它们,因为我们希望在创建控制器时使用这些操作。Action属性将用于装饰一个方法。

每一个行动都应该从消费者的角度来考虑;例如,对于Post,客户机正在发布一些内容。

如果我们已经创建了ShoesController,那么其路径如下:

    [Route("api/[controller]")] 

邮递

当我们想要创建一些东西时,使用此操作。邮件正文将包含需要保存到数据存储中的数据:

    [Route("")] 
    [HttpPost]  
    public IActionResult CreateShoes([FromBody] ShoeModel model) 

第一行是路由,应该始终声明路由。它们让正在阅读代码或调试代码的人更好地理解正在发生的事情以及流程是如何进行的。

在第二行中,我们陈述了Action属性;在这种情况下,它是Post,您不需要一直设置它。宣布行动是一种良好的做法。

收到

Get用于检索数据。在大多数情况下,Get未明确说明:

    [Route("")] 
    [HttpGet]  
    public IHttpActionResult GetShoes() 

这可以声明如下:

    [Route("")] 
    public IHttpActionResult Get() 

注意动作的省略。

Put用于更新数据或创建一些不存在的数据:

    [Route("{id}")] 
    [HttpPut]  
    public IHttpActionResult Update(ShowModel model) 

您会注意到 ID 是路由的一部分,它意味着调用者知道他们想要更新哪个实体。我们所做的就是创建名为Update的方法;名称可以是您想要的名称。

色斑

PatchPut类似,区别在于您发送的只是改变了增量的数据,而不是整个模型:

    [Route("{id}")] 
    [HttpPatch]  
    public IHttpActionResult PatchUpdate(ShowModel model) 

删去

Delete用于删除数据。您所需要的只是 ID:

    [Route("{id}")] 
    [HttpDelete]  
    public IHttpActionResult Delete(string id) 

这些是我们将在控制器中使用的操作。我已经将Actions属性与控制器分开进行了研究,因此我们可以在不担心实现的情况下对其进行一些关注。

控制器

我启动了 VS2017 社区版并创建了一个新的 web 项目。请注意,您可以选择要针对哪个模板。我选择了 ASP.NET Core Web 应用。创建一个名为Puhoi的新项目,这是新西兰的一个小镇,生产一些乳制品。我的目标是为他们的一些产品创建一个控制器。创建一个有形的、真实的工作示例是很好的。我倾向于远离书籍控制器或产品控制器之类的东西。

创建项目后,系统将提示您选择模板;通过选择 ASP.NET Core 2.0,选择以下屏幕截图中突出显示的模板:

我创建了一个新的控制器,保存在Controllers文件夹中:

另外,记下控制器的路线;它不包含控制器的名称:

让我们谈谈我们想要创造的东西和一点关于 Puhoi 奶酪的知识。他们有一些产品,如牛奶、奶酪和酸奶。

像任何公司一样,您希望列出所有产品、添加新产品和删除产品。因此,如果有一个前端,一个网站,或一个应用,那么他们会将自己连接到这个 API 中以获取相关信息。让我们开始构建一些逻辑。我们不会为此创建后端,因为它超出了本章的范围。

让我们构建 stores controller,它将列出一些顶级产品,例如百货商店。然后,让我们深入并创建一个子管道控制器,例如列出各种奶酪的东西。如果你不喜欢奶酪,你就不会喜欢这一章;我事先道歉。

模型

我们在一个单独的项目中创建模型,因为我们不想用一切污染 API 项目,它不是一个垃圾场。当我们创建类时,一个类有一个职责,当它们有多个职责时,我们就创建一个新类。项目应该包含具有并共享相同职责的类。我们还创建了一个BaseModel类,该类具有一些我们希望所有模型都具有并且应该具有的公共属性,因为它们是相关的。

    public  class BaseModel  
    { 
      public Guid Id { get; set; } 
    } 

创建模型项目后,我们有一个包含模型类和我们的BaseModel类的文件夹:

StoreModel类继承BaseModel类,属性表示模型属性:

现在我们有了我们的模型,让我们将其添加到控制器并开始。别忘了将 web API 中的引用添加到Models项目中。

稍后,您还将看到我们如何分别添加模型。我已经重构了我们的控制器以使用存储模型,现在看起来是这样的:

总之,我们在Get上返回一个或多个StoreModel,并且PostPut方法将我们的模型作为输入参数。

现在我们已经准备好了,剩下的是我们应该如何返回Get的数据并传输PutPost要存储的数据。

我们可以创建内存中的数据存储并将其用作持久存储。但这是有点黑客的,你并不是真的在生产系统中这样做。也许我们应该创建一个数据库,并将数据从 API 一直传递到数据库。这是一个很大的努力,这一章是关于控制器。因此,我们可能会创建一个到数据存储的接口,并将数据从 API 传递到接口,当您准备实现 API 时,这应该是一个很好的模式。

为了创建一个分层良好的架构并隐藏一些将 API 连接到数据层的代码,我们将创建一个新的类库项目并向该库添加一些类。这些类将引导数据进出数据存储接口。

Minimum-layered web API architecture

我已经用最少数量的组件制作了通用图。如果需要,可以创建更多组件。在应用中考虑边界点很重要。您的 API 项目不应该引用数据项目,数据项目也不应该引用 API 项目。

商业

使用应用的名称创建一个新的类库项目,然后创建.Business,如下所示:

还有它的文件夹结构。

这是IDataStoreManager的接口:

    namespace Puhoi.Business.Interfaces 
    { 
      public interface IStoreManager 
      { 
        HttpModelResult Add(BaseModel model); 
        HttpModelResult Update(BaseModel model, Guid id); 
        HttpModelResult Get(Guid id); 
        HttpModelResult Delete(Guid id); 
        HttpModelResult GetAll(); 
      } 
    } 

您需要对模型项目的引用,并添加对System.Web的引用。

请注意,我们正在返回HttpStatusCode。有人可能会争论为什么业务引用System.Web,在过去,这些可能是一个有效的论点。与我们的设计图一样,Business是 web API 和数据层之间的一层。它必须了解这两个方面,它是一个业务层,但它是一个 WebAPI 业务层。

HttpModelResult类如下所示:

我们现在创建一个实现IStoreManager的具体类。但是,我们需要对其进行注册。许多人会使用依赖项注入来创建对象,并使用像 Autofac 这样的库。

有了 ASP.NET Core,这是内置的。

依赖注入

以下是我注册StoreManager课程的方式:

我已经导航回我们的 PuhoiAPI 项目,并在Startup.cs类中添加了以下代码:

可用的选项如下所示:

  • AddTransient
  • AddScoped
  • AddInstance
  • AddSingleton

如果您是依赖注入DI的新手,以下内容应该能让您更好地理解:

  • 瞬态:每次需要时都会创建一个新对象。这最适合于无状态对象。
  • 范围:为每个请求创建一个新对象。
  • 单例:与单例模式类似。第一次需要该对象时,将创建一个新对象,并且该对象的每个后续依赖项都将使用该对象。
  • 实例:最好的描述方式是它的行为类似于单例,只是单例是延迟加载的。

既然管理器已经包含在我们的 DI 中,那么让我们将其合并到控制器中。

通过身份证

StoreController类被注入IStoreManager接口,作为其构造函数中的依赖项:

对于控制器上的Get,我们将对其进行重构,因此不要过于关注实现:

注意路线;我们使用的是 ID,我们的 ID 是Guid,这是我们的唯一标识符。然后,我们有storemanager,我们将 ID 传递给我们的商店经理,并从控制器返回一个模型。

相当容易;这在实际通话中是什么样子的,我们如何称呼它?

我创建了一个简单的实现,它将返回一个包含所请求内容的模型。

小提琴手的要求如下:

小提琴手的反应如下:

我们的状态代码是 200,这是成功的。结果非常好,因为我们有数据流。考虑没有找到结果的场景。返回一个空模型并不理想,因为我们将返回一个带有 200 的空模型。我们希望返回更直观的结果。

如果我们将实现更改为以下内容,从管理器检查模型并返回 null,我们的响应为 204,这不会告诉使用者特定资源不存在:

使用IAction结果类型更灵活,并提供所需的结果:

我们可以从经理那里测试我们的模型,并注意我们的回报。有三种不同的回报。一个是模型的OK。第二个是NotFound;如果经理没有找到我们查询的 ID,我们可以返回未找到状态。这对消费者来说更直观,并且检查结果代码比解析数据便宜得多。

最后,我们假设我们收到的请求是错误的,并返回一个错误的请求。

需要注意的是,这只是一个示例和一种模式,说明了如何构造控制器;您可以在 switch 语句中添加更多事例。

您也可以将BadRequest更改为更智能一点,而不仅仅是返回BadRequest;这里的重点是从消费者的角度展示IActionResult的用途以及它是如何凝胶的。

现在,您可以在 Fiddler 中看到所需的结果:

这是在StoreManager类的Get方法中实现逻辑的方式:

我们通过 ID 向DataStore请求对象,如果结果为空,则返回Not Found。如果有一个对象从数据存储返回,那么我们使用映射器在数据库中的对象和 API 公开的对象之间进行映射。本质上,我们在dtomodel之间映射。然后,我们将状态设置为OK并等待model结果。

映射

我们使用 AutoMapper 在模型Dto之间进行映射,反之亦然。下图显示了AutoMapper如何融入我们的解决方案:

为 API 项目和将进行映射的项目添加对AutoMapper的 NuGet 引用。

在 API 项目中,我创建了一个新类来设置映射。此类继承自AutoMappers概要文件类。

这是最简单的设置:

它说你应该在dto作为源和model作为目标之间创建一个映射。然而,我们知道这并不是那么简单。结果是这样的:

对于dto,我们忽略StoreIdUId,对于model,我们忽略Id。然后,映射完成后,将UIddto映射到Idmodel映射。

Startup.cs中通过以下方式完成管道内的设置:

为映射器配置创建成员变量。然后,在Startup构造函数中,我们设置了一个新的MapperConfiguration变量,并添加包含映射的概要文件。

我们快搞定了。在ConfigureService中,我们需要将其添加到服务中:

IMapper被创建为单例,因为完成的映射不会更改,并且它们不包含任何状态。

邮递

从控制器开始,实现 Post,如果对象被创建,或者如果它返回内部管理器发送给我们的内容,则返回 201。

我们没有特殊的路由,但是我们声明默认路由是清除的。用HttpPost装饰方法。与Get一样,我们返回IActionResult。从请求主体检索模型。如果您查看此方法中的代码,我们会将所有工作委托给经理。然后,我们得到一个结果;如果管理器发送给我们HttpStatusCode,我们知道对象已经创建,我们返回 201,创建时带有新创建资源的位置。任何其他结果被翻译回HttpStatusCodeResult。这是我对 Fiddler 的研究结果:

请求主体 JSON 如下所示:

服务器的响应如下所示:

经理的工作是从控制器获取模型并将其传递到此数据存储:

然而,它的责任比这多一点。它需要告诉管理器对象是否已创建,如果未创建,则需要指定问题所在。在数据存储可以插入对象之前,管理器会检查存储是否具有此对象。请注意,检查现有对象是否由管理器负责,而不是由数据存储负责。顾名思义,这是一家商店。如果对象存在,则管理器会将冲突返回给控制器。这是管理器中的 else 块:

我们利用AutoMappermodeldto之间切换并返回到模型。您必须在AutoMapperProfileConfiguration中进行更改,才能使其正常工作:

我们使用Put更新商店或创建新商店。Put的签名与Post不同:

我们在控制器类的签名中有 ID 和模型。路由具有 ID,并且模型位于请求主体中。您会注意到Post没有 ID,我们要求店长更新我们的型号;如果我们从商店管理器中获得一个已创建的资源,那么我们将发布新资源的这个位置。任何其他状态,包括OK,返回为HttpStatusCodeResult

我已经对我们的商店经理做了一些重构,就像人们通常做的那样。如果找不到提供给我们的 ID,那么我们将模型添加到数据存储中。在正常流程中,更新模型并返回 200。

我把这件事告诉了费德勒;注意动作Put和主体中的模型。

在此之前的步骤是使用Post创建模型,使用Put更新模型,然后在资源上调用 get 以检查是否已执行更新。

删去

Delete非常直截了当。我们将继续执行第 1 章微服务和面向服务架构简介中设定的原则,围绕其余删除原则。当我们第一次删除一个表示时,我们可以返回 200,但是当我们发出相同的请求时,表示不再存在,所以我们应该返回 404。让我们看看这个代码:

要求商店经理删除具有特定 ID 的对象,然后我们返回从经理那里得到的任何东西。让我们看看经理:

这也很简单;根据我们从数据存储中得到的信息,我们返回 200 或未找到。

我将不显示 Fiddler 请求和对不同流的响应,因为我觉得这对于我们在本章中介绍的内容非常基本,直到现在。这只是删除请求:

请注意,我们的操作是Delete

盖特尔

我们的GetAll操作看起来比Delete简单:

我们向我们的经理索要所有门店,并将其连同 200:

我们在 manager 中所做的就是,向数据存储请求所有 DTO,然后将它们映射到一个模型,并将它们作为IEnumerable返回。就在那之前,我们把状态设为 200。

路线是什么样的?它看起来类似于GetById,只是不需要设置 ID。

总结

在本章中,我们开发了一个完整的 CRUD 端点,并研究了 ASP.NETCore2.0 的一些新特性,如内置依赖项注入。

我们探索了HttpGetHttpPostHttpPutHttpDelete动作,以及一些基本的路由。

我们建立了一个干净的模式来分解类的职责,并使其更容易扩展给定的功能。

对象是松散耦合的,这使得它们更容易测试;这是通过内置依赖项注入实现的。我们还使用 Fiddler 来演示我们的 API 是如何工作的。

在下一章中,我们将深入讨论路由机制、路由生成器、属性路由、约束等等。