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

ASP.NET Core 2.1:将 VMD.RESTApiResponseWrapper.Core 集成到你的 REST API 应用程序

starIconstarIconstarIcon
emptyStarIcon
starIcon
emptyStarIcon

3.32/5 (4投票s)

2018年11月14日

CPOL

11分钟阅读

viewsIcon

9225

downloadIcon

140

一个关于如何将 VMD.RESTApiResponseWrapper.Core 库集成到你的 ASP.NET Core 2.1 REST API 应用程序的快速教程

引言

几个月前,我写了一篇文章,介绍了如何为你的 ASP.NET Core 和 Web API 应用程序创建一个统一的对象响应包装器。我还为该包装器制作了两个版本的 NuGet 包,可以在下面找到

令我惊讶的是,这两个 NuGet 包现在都有了数百次下载,并且我收到了一些开发者的评论和电子邮件,询问如何实际在项目中它们。本文旨在回答我收到的这些常见问题,所以在这里就给出来。

入门

在我们开始之前,我想感谢那些关注这个库并可能已经试用过的人。我非常感谢你们对这个库的所有反馈,并希望它能在你的项目中有所帮助。

废话不多说,让我们看看如何在应用程序中实际使用它。让我们打开 Visual Studio 2017。通过导航到 **文件** > **新建项目** 来创建一个新项目。在“新建项目”对话框的左侧窗格中,选择 **已安装** > **Visual C#** > **Web** > **.NET Core** > **ASP.NET Core Web 应用程序**。项目名称可以随意命名,但在此演示中,我们将其命名为“MyWebApp.API”,如下图所示

图 1:新建项目对话框

点击 **确定**,它应该会带你到下一个屏幕,如下图所示

图 2:ASP.NET Core 模板对话框

选择 **空**,然后点击 **确定**,让 Visual Studio 为你生成默认项目文件和依赖项。这是默认生成的项目文件

图 3:默认生成的项目文件

让我们快速了解一下每个生成的文件。

如果你已经了解 ASP.NET Core 的核心重要更改,那么你可以跳过这部分,但如果你是 ASP.NET Core 的新手,那么我想强调其中一些更改。如果你之前使用过 ASP.NET 的早期版本,你会注意到新的项目结构完全不同。项目现在包含以下文件

  • 依赖项:包含应用程序所需的分析器、NuGet 和 SDK 依赖项
  • 属性:包含 `launchSettings.json` 文件,该文件管理与每个调试配置文件(如 IIS、IIS Express 和应用程序本身)关联的应用程序配置设置。在这里,你可以为应用程序使用的框架定义特定于配置文件的配置(编译和调试配置文件)。
  • **wwwroot***:一个文件夹,其中将放置所有静态文件。这些是 Web 应用程序将直接提供给客户端的资源,包括 HTML、CSS、图像和 JavaScript 文件。
  • **Startup.cs***:这是你放置启动和配置代码的地方。
  • **Program.cs***:这是你初始化应用程序所需的所有服务的地方。

模型

让我们创建一些简单的 `Model` 来用于此演示,这些 `Model` 具有以下属性

namespace MyWebApp.API.Model
{
    public class Band
    {
        public int Id { get; set; }
        public string Name { get; set; }
        public string Genre { get; set; }
    }

    public class CreateBandDTO
    {
        public string Name { get; set; }
        public string Genre { get; set; }
    }

    public class UpdateBandDTO
    {
        public string Name { get; set; }
        public string Genre { get; set; }
    }
}

`Model` 只是简单的类,用于容纳一些属性来保存信息。

模拟数据

我将尽量让这个演示尽可能的简单,所以我不会在这里使用任何数据库,因为这并不是本文的真正意图。如果你想了解如何处理来自数据库的真实数据,请查看我的其他文章

现在,让我们回到工作。假设我们有以下定义模拟数据的类

public class SampleData
{
        public static List<Band> GetBands()
        {
            return new List<Band>()
            {
                new Band { Id = 1, Name="Alice in Chains", Genre= "Heavy Metal"},
                new Band { Id = 2, Name="Soundgarden", Genre= "Grunge"},
                new Band { Id = 3, Name="Pearl Jam", Genre= "Grunge"},
                new Band { Id = 4, Name="Audioslave", Genre= "Alternative Metal"},
                new Band { Id = 5, Name="Stone Temple Pilots", Genre= "Hard Rock"},
                new Band { Id = 6, Name="Nirvana", Genre= "Grunge"},
                new Band { Id = 7, Name="Third Eye Blind", Genre= "Alternative Rock"},
                new Band { Id = 8, Name="Led Zeppelin", Genre= "Blues Rock"},
                new Band { Id = 9, Name="The Beatles", Genre= "Rock and Roll"},
                new Band { Id = 10, Name="The Rolling Stones", Genre= "Blues Rock"}
            };
        }
}

上面的 `class` 只是一个普通的类,带有一个名为 `GetBands()` 的 `public static` 方法。该方法定义了一个 `Band` 类型的 `List`,并向集合中添加了一些默认记录。

创建 API 控制器

让我们创建一个新的 ASP.NET Core API 控制器,并定义一些可用于测试的终结点。我的 `Controller` 类看起来是这样的

using Microsoft.AspNetCore.Mvc;
using MyWebApp.API.Model;
using System.Collections.Generic;
using System.Linq;

namespace MyWebApp.API.v1
{
    [Route("api/v1/[controller]")]
    [ApiController]
    public class BandsController : ControllerBase
    {
        // GET: api/v1/bands
        [HttpGet]
        public IEnumerable<Band> Get()
        {
            return SampleData.GetBands();
        }

        // GET api/v1/bands/7
        [HttpGet("{id}")]
        public Band Get(int id)
        {
            var data = SampleData.GetBands().Where(o => o.Id.Equals(id));
            if (data.Any())
                return data.First();

            return null;
        }

        // POST api/v1/bands
        [HttpPost]
        public IActionResult Post([FromBody]CreateBandDTO band)
        {
             //Call a method to add a new record to the entity
            return Ok();
        }

        // PUT api/v1/bands/7
        [HttpPut("{id}")]
        public IActionResult Put(int id, [FromBody]UpdateBandDTO band)
        {
            //Call a method to update the entity
            return Ok();
        }

        // DELETE api/bands/7
        [HttpDelete("{id}")]
        public IActionResult Delete(int id)
        {
            //Call a method to delete an entity
            return Ok();
        }
    }
}

上面的 `Controller` 类包含四个基本的 **HTTP** 方法,如 `GET`、`POST`、`PUT` 和 `DELETE`。典型的 RESTful API 就是这样的。请注意,你找不到 `POST`、`PUT` 和 `DELETE` 的任何代码实现。这是因为我们没有在这里处理数据库或内存中的数据存储。我只是把它们放在那里,以便你可以直观地看到终结点是什么样的。

测试输出

让我们构建并运行应用程序。这是我从 POSTMAN 进行的一些测试的示例屏幕截图

GET:/api/v1/bands

图 4:HTTP Get 请求

GET:api/v1/bands/{id}

图 5:HTTP Get 请求

此时,API 可以工作,但问题在于它没有给开发人员有意义的响应。我们知道数据是响应中非常重要的一部分。然而,仅仅将数据作为 **JSON** 响应输出并不是特别有用,尤其是在每次请求之间发生意外行为时。

快速回顾一下,如果你正在采用 RESTful 方法来构建 API,那么你将利用 **HTTP** 动词,如 `GET`、`POST`、`PUT` 和 `DELETE`。这些操作中的每一个都可能根据你的方法/操作的设计返回不同的类型。你的 `POST`、`PUT` 和 `DELETE` 终结点可能返回数据,也可能不返回数据。你的 `GET` 终结点可能返回一个字符串、一个 `List<T>`、一个 `IEnumerable`、一个自定义 `class` 或一个 `object`。另一方面,如果你的 API 抛出错误,它将返回一个 `object`,或者更糟的是一个 **HTML** `string`,说明错误的发生原因。所有这些响应之间的差异使得 API 难以使用,因为消费者需要知道每次返回的数据的 `type` 和结构。客户端代码和服务端代码都变得难以管理。

这就是我提出一个库的原因,该库为成功和错误结果提供了响应格式的一致性。

3 个简单步骤集成 VMD.RESTApiResponseWrapper.Core 库!

只需几个步骤,你就可以将 API 控制器转换为返回有意义的响应,而无需付出太多开发努力。你所要做的就是

步骤 1

从 NuGet 下载并安装库。

图 6:NuGet 包管理器

你可以按照上图所示通过 NPM 安装该包,或使用 NPM 控制台中的以下命令

PM> Install-Package VMD.RESTApiResponseWrapper.Core -Version 1.0.4
引用

**注意**:截至本文撰写之时,最新版本是 `v1.0.4`,它针对的是 ASP.NET Core 2.1 版本。

第二步

在 `Startup.cs` 中声明下面的命名空间

using VMD.RESTApiResponseWrapper.Core.Extensions;

步骤 3

在 `Startup.cs` 的 `Configure()` 方法中注册 `UseAPIResponseWrapperMiddleware`,如下所示

public void Configure(IApplicationBuilder app, IHostingEnvironment env) {
    if (env.IsDevelopment())
    {
        app.UseDeveloperExceptionPage();
    }

    app.UseAPIResponseWrapperMiddleware();
    app.UseMvc();
}
引用

**注意**:请务必在 MVC 中间件“之前”注册它。

很简单!现在再次尝试构建并运行应用程序。根据我们的示例,响应将如下所示

图 7:HTTP Get 请求

你会注意到响应对象现在包含几个属性,如 `Version`、`StatusCode`、`Message`,并且实际数据存储在 `Result` 属性中。

这是我们尝试指向一个不存在的 URL 时的另一个示例输出

图 8:HTTP Get 请求

任何可能发生的意外错误都将被自动处理,而无需你做任何事情。你会注意到响应输出是动态的。动态的意思是,我们没有包含 `Result` 属性,而是使用 `ResponseException` 属性来存储错误和异常信息。

启用自定义响应

让我们继续修改现有的 API 终结点,为其他 **HTTP** 动词返回消息。

GET

// GET api/bands/7
[HttpGet("{id}")]
public APIResponse Get(int id) {
    var data = SampleData.GetBands().Where(o => o.Id.Equals(id));
    if (data.Any())
        return new APIResponse(200, "Data found", data.First());

    return null;
}

结果

图 9:HTTP Get 请求

POST

// POST api/bands
[HttpPost]
public APIResponse Post([FromBody]CreateBandDTO band) {
    long bandID = 10; //10 is just an example here

    //Call a method to add a new record to the entity
    //bandID = YourRepo.Add(band);

    return new APIResponse(201, $"New record with ID {bandID} successfully added.");
}

结果

图 10:HTTP Post 请求

PUT

// PUT api/bands/6
[HttpPut("{id}")]
public APIResponse Put(long id, [FromBody]UpdateBandDTO band) {
    //Call a method to update the entity
    //YourRepo.Update(band);

    return new APIResponse(200, $"The record with ID {id} has been successfully updated.", id);
}

结果

图 11:HTTP Put 请求

删除

// DELETE api/bands/7
[HttpDelete("{id}")]
public APIResponse Delete(int id) {
    //Call a method to delete an entity
    //YourRepo.Delete(id);

    return new APIResponse(200, $"The record with ID {id} has been successfully deleted.", id);
}

结果

图 12:HTTP Delete 请求

你会注意到响应对象对于每个 HTTP 操作请求都是一致的。这无疑为你的 API 消费者提供了更好、更有意义的信息。

实现模型验证

模型验证允许你在 `class`/`property` 级别强制执行预定义的验证规则。通常使用这种验证技术来保持关注点清晰分离,因此你的验证代码编写、维护和测试起来会更简单。

正如你已经知道的,ASP.NET Core 2.1 引入了 `APIController` 属性,该属性会自动执行模型状态验证,用于 **400 Bad Request** 错误。当 `Controller` 被 `APIController` 属性修饰时,框架会自动注册一个 `ModelStateInvalidFilter`,该过滤器在 `OnActionExecuting` 事件上运行。它检查 `Model State` 的有效性并相应地返回响应。这是一个很棒的功能,但由于我们希望返回自定义响应对象而不是 **400 Bad Request** 错误,因此在这种情况下我们将禁用此功能。

要禁用自动模型状态验证,只需在 `Startup.cs` 文件的 `ConfigureServices()` 方法中添加以下代码

public void ConfigureServices(IServiceCollection services) {
    services.Configure<ApiBehaviorOptions>(options =>
    {
        options.SuppressModelStateInvalidFilter = true;
    });

    services.AddMvc();
}

使用数据注解验证

数据注解是位于 `System.ComponentModel.DataAnnotations` 命名空间下的类属性,你可以使用它们来修饰类或属性以强制执行预定义的验证规则。要启用数据注解模型验证,我们需要注册以下命名空间

using System.ComponentModel.DataAnnotations;

让我们修改 `CreateBandDTO` 类,使用数据注解实现基本模型验证。下面是修改后的代码

public class CreateBandDTO
{
    [Required]
    [MaxLength(20)]
    public string Name { get; set; }
    [Required]
    public string Genre { get; set; }
}

现在,当我们再次运行应用程序并发出 `POST` 请求时,如果 `Name` 属性留空,响应应该如下图所示

图 13:HTTP Post 请求

你会注意到包装器捕获了验证错误,并将其放入 `ValidationErrors` 属性中,以便于跟踪。有关模型验证的更多信息,请参阅 ASP.NET Core MVC 中的模型验证

使用 Fluent Validation

如果出于某种原因,你不想使用 `System.ComponentModel.DataAnnotations` 来验证你的 `Models`,而是想使用 `FluentValidation`,你也可以这样做。让我们通过一个简单的例子来看看如何集成 `FluentValidation`。

首先,下载并安装如下图所示的 NuGet 包

图 14:NuGet 包管理器

你也可以通过运行以下命令在 NPM 控制台中安装它

Install-Package FluentValidation.AspNetCore

安装完成后,我们就可以开始使用 `FluentValidation` API 了。你应该在你声明 `Models` 的地方声明以下命名空间

using FluentValidation;

让我们将 `CreateBandDTO` 类恢复到其原始状态,并添加一个名为 `CreateBandValidator` 的新类。下面是修改后的代码

public class CreateBandDTO
{
    public string Name { get; set; }
    public string Genre { get; set; }
}

public class CreateBandValidator : AbstractValidator<CreateBandDTO>
{
    public CreateBandValidator() {
        RuleFor(o => o.Name).NotEmpty().MaximumLength(20);
        RuleFor(o => o.Genre).NotEmpty();
    }
}

你会注意到我们不再使用 `Required` 和 `MaxLength` 属性来强制对 `Model` 应用预定义的验证规则。相反,我们保持它们简单明了。我喜欢 `FluentValidation` 的地方在于,我们可以通过为每个我们想实现一些约束和其他验证规则的 `Model` 创建一个 `Validator` 类来分离验证逻辑。

让这一切生效的最后一步是按照下面的代码在 `Startup.cs` 文件中配置 `FluentValidation`

public void ConfigureServices(IServiceCollection services) {
    services.Configure<ApiBehaviorOptions>(options =>
    {
        options.SuppressModelStateInvalidFilter = true;
    });

    services.AddMvc().AddFluentValidation();
    services.AddTransient<IValidator<CreateBandDTO>, CreateBandValidator>();
}

更多信息,请参阅 此链接

以下是模型验证失败时的响应示例屏幕截图

图 15:HTTP Post 请求

拥有信息丰富、一致且有意义的响应,应该有助于开发人员轻松地使用你的 API 并进行故障排除。

处理自定义错误和异常

你可以使用 `ApiException` 对象来返回错误和异常消息。例如,以下代码使用 `try-catch` 块来处理和模拟你的代码中可能发生的意外错误

[HttpGet]
public IEnumerable<Band> Get() {
    try
    {
        //This is just an example to trigger an error
        int number = Convert.ToInt32("100abc");
    }
    catch (NullReferenceException ex)
    {
        throw new ApiException(ex);
    }
    catch (FormatException ex)
    {
        throw new ApiException(ex);
    }
    catch (ArithmeticException ex)
    {
        throw new ApiException(ex);
    }
    catch (Exception ex)
    {
        throw new ApiException(ex);
    }

    return SampleData.GetBands();
}

上面的代码尝试将包含非数字值的 `string` 转换为 `integer` 类型,这将在运行时导致错误。响应输出将如下所示

图 16:HTTP Get 请求

你还可以使用 `ApiException` 来在自定义代码验证失败时抛出自己的消息。例如,如果你的代码验证用户凭据失败,你可以这样做

throw new ApiException($"The userid {id} does not exist.", 400);

启用 Swagger

Swagger 为你的 API 提供了高级文档,它允许开发人员引用你的 API 终结点的详细信息并在需要时进行测试。这尤其有用,当你的 API 是 `public` 并且你期望许多开发人员使用它时。

要为你的 API 应用程序启用 Swagger,请继续通过 NPM 下载并安装 `Swashbuckle` 包,如下图所示

图 17:NuGet 包管理器

在 `Startup.cs` 文件的 `ConfigureServices()` 方法中添加以下代码

// Register the Swagger generator, defining 1 or more Swagger documents
services.AddSwaggerGen(c =>
{
     c.SwaggerDoc("v1", new Info 
     { Title = "My REST API with VMD.RESTApiResponseWrapper.Core", Version = "v1" });
});

接下来,我们需要启用用于提供生成的 `JSON` 文档和 `Swagger` UI 的中间件。为此,请在 `Startup.cs` 文件的 `Configure()` 方法中添加以下代码

// Enable middleware to serve generated Swagger as a JSON endpoint.
app.UseSwagger();

// Enable middleware to serve swagger-ui (HTML, JS, CSS, etc.), 
// specifying the Swagger JSON endpoint.
app.UseSwaggerUI(c =>
{
     c.SwaggerEndpoint("/swagger/v1/swagger.json", "My API V1");
});

现在运行你的应用程序,并在 URL 后面加上“`/swagger`”,它应该会显示 `Swagger` UI,如下图所示

图 18:Swagger UI

这是从 `Swagger` UI 发出的示例 `POST` 请求/响应

图 19:Swagger UI HTTP Post 请求

太棒了!

更多信息,请参阅 此链接

摘要

在本文中,我们学习了如何将 `VMD.RESTApiResponseWrapper.Core` 包库集成到你的 ASP.NET Core 2.1 应用程序中。

欢迎随时尝试。评论和建议都受欢迎,所以请随时留言,我很乐意尽我所能回答任何疑问。

© . All rights reserved.