十一、Web API

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

  • 使用 ActionResults
  • 配置内容协商
  • 配置跨域起始请求
  • 使用轻便
  • 测试 Web api
  • 管理异常

使用 ActionResult

在本食谱中,您将学习如何使用ActionResult返回 Web API。 ActionResult是一种核心类型的 MVC,用于从服务器返回结果到客户端。 ActionResult是一个基类和它的抽象类,所以我们可以使用它的派生类之一,例如JsonResultViewResultRedirectResultFileResult

准备

我们将创建一个带有CRUD方法的 Web API 控制器,以理解每个 HTTP 谓词必须返回的ActionResult内容。

怎么做……

ASP.NET Core MVC 和 Web api 合并,基类现在是相同的。 ActionResults现在返回 HTTP 状态码的结果,就像ApiController基类在 ASP. xml 之前返回.NET 的核心。

我们将使用ActionResults来返回带有CRUDWeb API 控制器的 HTTP 状态代码:

  1. 首先,让我们通过创建一个空 Web 应用来创建 Web API 应用。

  2. 接下来,我们将添加 ASP.NET Core MVC 依赖于项目:

"Microsoft.AspNetCore.MVC": "2.0.0",
  1. 接下来,我们将以下代码添加到Startup.cs中。 这段代码将允许我们使用 Web API 的控制器:
public void ConfigureServices(IServiceCollection services)
{
  services.AddMVC();
}
public void Configure(IApplicationBuilder app)
{
  app.UseMVC();
}
  1. 接下来,我们将创建一个在这个 Web API 控制器中使用的存储库:
public interface IProductRepository
{
  IEnumerable<Product> GetAllProducts();
  void Add(Product product);
  Product Find(int id);
  void Update(Product product);
  void Remove(int id);
}
  1. 接下来,让我们在Startup.cs中注册和配置存储库生命周期:
services.AddScoped<IProductRepository, ProductRepository>();
  1. 接下来,让我们创建 Web API 控制器:
[Route("api/[controller]")]
public class ProductApiController : Controller
{
  private readonly IProductRepository _productRepository;
  public ProductApiController(IProductRepository repository)
  {
    _productRepository = repository;
  }
}
  1. 现在我们将添加GET方法。 其路线为api/productapi:
[HttpGet]
public IActionResult Get()
{
  var productsFromRepo = _productRepository.GetAllProducts();
  if (productsFromRepo == null)
  return NotFound();
  // return HTTP response status code 404
  return Ok(productsFromRepo);
  // return HTTP status code 200
}
  1. 现在我们将添加不同路径的GET方法api/productapi/{id}:
[HttpGet("{id:int}")]
public IActionResult Get(int id)
{
  var productFromRepo = _productRepository.Find(id);
  if (productFromRepo == null)
  return NotFound();
  // return HTTP response status code 404
  return Ok(productFromRepo);
  // return HTTP response status code 200
}
  1. 接下来,我们将添加POST方法。 它的路由为api/productapi,在请求体中有一个Product对象:
[HttpPost]
public IActionResult Post([FromBody]Product product)
{
  if (product == null)
  return BadRequest();
  // return HTTP response status code 400
  _productRepository.Add(product);
  return CreatedAtRoute("GetProduct",
  new { id = product.Id }, product);
  // return HTTP response status code 201
}
  1. 接下来,我们将添加PUT方法。 它的路由为api/productapi/{id},在请求体中有一个Product对象:
[HttpPut("{id}")]
public IActionResult Put(int id, [FromBody]Product product)
{
  if (product == null || product.Id != id)
  return BadRequest();
  // return HTTP response status code 401
  var productFromRepo = _productRepository.Find(id);
  if (productFromRepo == null)
  return NotFound();
  // return HTTP response status code 404
  _productRepository.Update(product);
  return new NoContentResult();
  // return HTTP response status code 204
}
  1. 接下来,我们将添加PATCH方法。 它的路由为api/productapi/{id},在请求体中有一个Product对象。 当我们进行部分更新时,使用PATCH动词:
[HttpPut("{id}")]
public IActionResult Patch(int id, [FromBody]Product product)
{
  if (product == null)
  return BadRequest();
  // return HTTP response status code 400
  var productFromRepo = _productRepository.Find(id);
  if (productFromRepo == null)
  return NotFound();
  // return HTTP response status code 404
  productFromRepo.Id = product.Id;
  _productRepository.Update(product);
  return new NoContentResult();
  // return HTTP response status code 204
}
  1. 接下来,我们将添加DELETE方法。 其路线为api/productapi/{id}:
[HttpDelete("{id}")]
public IActionResult Delete(int id)
{
  var productFromRepo = _productRepository.Find(id);
  if (productFromRepo == null)
  return NotFound();
  // return HTTP response status code 404
  _productRepository.Remove(id);
  return new NoContentResult();
  // return HTTP response status code 204
}

它是如何工作的…

IActionResultActionResult抽象类实现。 ActionResult抽象类用于创建多个类,向调用方客户端返回预定义的 HTTP 状态码,如下所示:

  • OkResult:返回200HTTP 状态码
  • CreatedResult:返回201HTTP 状态码
  • CreatedAtActionResult:返回201HTTP 状态码
  • CreatedAtRouteResult:返回201HTTP 状态码
  • NoContentResult:返回204HTTP 状态码
  • BadRequestResult:返回400HTTP 状态码
  • UnauthorizedResult:返回401HTTP 状态码
  • NotFoundResult:返回404HTTP 状态码

ControllerBase类具有针对每种结果类型的几个方法。 我们可以从 Web API 中调用其中一个来返回所需的状态代码,比如下面的代码:

return Ok(); // returns OkResult 

return Created(); // returns CreatedResult 

return NotFound(); // returns NotFoundResult 

配置内容协商

在本食谱中,您将学习如何在 ASP 中管理内容协商.NET 的核心。 ASP。 由于ContentFormatters,NET Core 可以以任何格式(如 JSON、XML、PLIST 和 SOAP)返回数据。 更多信息请参见https://docs.microsoft.com/en-us/aspnet/core/mvc/advanced/custom-formatters

任何客户机都可以发出一个带有头的请求,以告诉服务器它希望响应的格式。 服务器应用可以读取报头值,并在创建对请求的响应时使用它。 内容协商是整个流程的名称。

准备

默认情况下,ASP.NET Core 将从动作方法返回 JSON。

让我们从前面的配方中获得代码,并让我们看看从GetAllProducts得到的结果:

我们得到 JSON 值,因为在默认情况下,ASP.NET Core 不考虑浏览器发送的Accept头。

怎么做……

  1. 首先,让我们在项目中添加以下依赖项:
"Microsoft.AspNetCore.MVC.Formatters.Xml": "2.0.0"
  1. 接下来,我们将以下代码添加到Startup.csConfigureServices方法中:
services.AddMVC(options =>
{
  options.RespectBrowserAcceptHeader = true;
  options.InputFormatters.Add(
  new XmlDataContractSerializerInputFormatter());
  options.OutputFormatters.Add(
  new XmlDataContractSerializerOutputFormatter());
});
  1. 最后,我们可以看到 XML 格式的产品列表:

配置跨域起始请求

在本教程中,您将学习如何在 ASP 中配置和使用跨源资源共享(CORS).NET Core 应用。

准备

为了配置和使用 CORS,我们将创建两个应用:一个 Web API 应用公开配置了 CORS 约束的服务,另一个客户机应用试图通过 jQuery AJAX 调用使用 Web API 服务。

怎么做……

我们将创建两个独立的应用项目,并从其中一个向other发出请求。 other项目是一个 ASP.NET Core 项目,我们将在其中启用/配置 CORS:

  1. 首先,让我们创建 Web API 应用,通过创建一个空的 Web 应用:
    dotnet new mvc -n Chapter11.R3.Server
  1. 接下来,我们将添加 ASP.NET Core MVC 依赖于项目:
"Microsoft.AspNetCore.MVC": "2.0.0"
  1. 接下来,我们将以下代码添加到Startup.cs中。 这段代码允许我们使用 Web API 的控制器:
public void ConfigureServices(IServiceCollection services)
{
  services.AddMVC();
}
public void Configure(IApplicationBuilder app)
{
  app.UseMVC();
}
  1. 接下来,让我们将 CORS 中间件添加到 ASP.NET Core 管道,让我们通过将以下代码添加到Startup.cs来配置它:
public void ConfigureServices(IServiceCollection services)
{
  services.AddMVC();
 services.AddCors(options => { options.AddPolicy("AllowMyClientOrigin", builder =>
 builder.WithOrigins("http://localhost:63125")
 .WithMethods("GET", "HEAD")
 .WithHeaders("accept", "content-type", "origin"));
 }); }
public void Configure(IApplicationBuilder app)
{
  app.UseMVC();
 app.UseCors("AllowMyClientOrigin"); }

要使用在ConfigureServices方法中创建的新的 CORS 策略,我们在Configure方法中调用该策略。 当前的 CORS 策略有三种方法:

如果我们想要允许所有客户端域名的 url 到 Web API 中,我们应该用ConfigureServices方法中的这段代码改变 CORS 策略:

services.AddCors(options =>options.AddPolicy("AllowMyClientOrigin",
p => p.AllowAnyOrigin()));
  1. 接下来,我们将用GET方法创建一个 API 控制器:
[Route("api/[controller]")]
public class TestAPIController : Controller
{
  [HttpGet]
  public IActionResult Get()
  {
    var result = new { Success = "True", Message = "API Message" };
    return Ok(result);
  }
}
  1. 最后,为了允许来自其他域的应用使用GET方法,我们还将在我们希望公开给域的 Web API 方法之上添加以下属性。 EnableCors属性接受一个字符串参数,这是之前在Startup.cs中创建的 CORS 策略:
[HttpGet]
[EnableCors("AllowMyClientOrigin")] public IActionResult Get()
{
  var result = new { Success = "True", Message = "API Message" };
  return Ok(result);
}
  1. 为了允许控制器级的 CORS 配置对该控制器中的所有方法可用,我们可以在controller之上添加EnableCors属性,而不是Action方法:
[Route("api/[controller]")]
[EnableCors("AllowMyClientOrigin")] public class TestAPIController : Controller
{
  [HttpGet]
  public IActionResult Get()
  {
    var result = new { Success = "True", Message = "API Message" };
    return Ok(result);
  }
}
  1. 为了允许应用级别的 CORS 配置可用于此应用中的所有控制器,我们可以将CorsAuthorizationFilterFactory过滤器添加到全局过滤器集合中。 代码最终应该如下:
public void ConfigureServices(IServiceCollection services)
{
  services.AddMVC();
  services.AddCors(options =>
  {
    options.AddPolicy("AllowMyClientOrigin", builder =>
    builder.WithOrigins("http://localhost:63125")
    .WithMethods("GET", "HEAD")
    .WithHeaders("accept", "content-type", "origin"));
  });
 services.Configure<MVCOptions>(options =>
 { options.Filters.Add(new 
    CorsAuthorizationFilterFactory("AllowMyClientOrigin"));
 }); }
[Route("api/[controller]")]
public class TestAPIController : Controller
{
  [HttpGet]
  public IActionResult Get()
  {
    var result = new { Success = "True", Message = "API Message" };
    return Ok(result);
  }
}
  1. 接下来,让我们创建一个新的空 web 应用来测试 web API,并通过右键单击项目的根,添加一个bower.js文件来添加前端依赖项。 然后,去添加|新项目| .NET Core|客户端| Bower 配置文件:

  1. 接下来,让我们通过右键单击项目的根目录,将 jQuery 添加到使用 Bower 包管理器的客户机应用中。 然后,选择 Manage Bower Packages 并安装 jQuery:

  1. 接下来,让我们在wwwroot文件夹中添加一个 HTML 页面,以添加 JavaScript 代码,它将调用 API 服务。 wwwroot文件夹是我们添加所有静态文件(.html.css.js.font、图像等等)的文件夹。 我们也可以将这个.js代码添加到视图中:

  1. 在 ASP 中允许静态文件.NET Core 应用,我们必须添加以下代码到Configure方法:
public void Configure(IApplicationBuilder app)
{
  app.UseStaticFiles();
}
  1. 接下来,我们将在刚刚创建的 HTML 页面中添加以下代码:
<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8" />
    <title></title>
  </head>
  <body>
    <button id="bt1">Call API</button>
    <script src="lib/jquery/dist/jquery.js"></script>
      <script type="text/javascript">
      $(document).ready(function () {
        jQuery.support.cors = true;
        $('#bt1').click(function () {
          console.log('Clicked');
          $.ajax({
            url: 'http://localhost:64112/api/TestAPI',
            type: 'GET',
            dataType: 'json',
            success: function (data) {
              console.log(data.success);
              console.log(data.message);
            },
            error: function (jqXHR, textStatus, errorThrown) {
              console.log(textStatus, errorThrown);
            }
          });
        });
      });
    </script>
  </body>
</html>
  1. 最后,通过单击 call API 按钮,我们将启动 HTML 页面并使用GETHTTP 方法调用TestAPI控制器。 为了确保没有发生错误,我们将通过测试应用的浏览器中的F12按钮启动。 让我们打开控制台,看看结果:

  1. 如果发生任何错误,控制台消息将是如下截图所示:

使用轻便

在本教程中,您将学习如何使用Swagger为我们的 REST api 创建帮助页面和文档。

准备

让我们用 VS 2017 创建一个空项目。

怎么做……

  1. 首先,我们将Swashbuckle引用添加到项目中:
"Swashbuckle": "5.6.0"
  1. 接下来,让我们在Startup.cs中添加Swagger作为中间件:
public void ConfigureServices(IServiceCollection services)
{
  services.AddMVC();
  services.AddSwaggerGen();
}
public void Configure(IApplicationBuilder app)
{
  app.UseMVC();
  app.UseSwagger();
  app.UseSwaggerUi();
}
  1. 现在,让我们通过访问http://{UrlAPI}/swagger/ui来启动 API 文档。 我们现在可以看到生成的 API 文档:

  1. 当我们点击每个 HTTP 方法时,我们可以看到 Swagger 提供给我们的所有选项,比如轻松地测试 API:

  1. 接下来,让我们使用另一个特性:添加关于 API 的信息。 为此,我们将以下代码添加到Startup.cs:
public class Startup
{
  public Startup(IHostingEnvironment env)
  {
    var builder = new ConfigurationBuilder()
    .SetBasePath(env.ContentRootPath);
    Configuration = builder.Build();
  }
 public IConfigurationRoot Configuration 
  {
 get; 
  }
  public void ConfigureServices(IServiceCollection services)
  {
    services.AddMVC();
    services.AddSwaggerGen();
 services.ConfigureSwaggerGen(options => { options.SingleApiVersion(new Info
 { Version = "v1",
 Title = "ECommerce API",
 Description = "The Api to get all data from the SB Store",
 TermsOfService = "None"
 }); options.IncludeXmlComments( Path.ChangeExtension(Assembly.GetEntryAssembly().Location, "xml")
 ); options.DescribeAllEnumsAsStrings(); });  }
  public void Configure(IApplicationBuilder app)
  {
    app.UseMVC();
    app.UseSwagger();
    app.UseSwaggerUi();
  }
}

options.IncludeXmlComments()方法意味着我们可以考虑 API 方法上面的 XML 注释来生成 Swagger 文档:

  1. 让我们在 c#代码中添加一些注释,看看所有这些变化:
/// <summary>
/// This method insert a product in the database
/// </summary>
/// <param name="product"></param>
/// <returns>CreatedAtRouteResult</returns>
[HttpPost]
public IActionResult Post([FromBody]Product product)
  1. 让我们看看现在在 Swagger UI 中的变化:

  1. 最后,让我们看看 Swagger 更有趣的功能:Try it Out! 按钮测试 HTTP 方法,并允许我们查看响应和请求 URL 的更多相关信息:响应体、响应代码和响应头:

它是如何工作的…

Swagger 为我们提供了一种很好的、简单的方法来生成完整的 UI 文档。 该文档将由现有的 API 源代码生成,并且可以通过应用中的 XML 注释来完成。 它还允许我们测试每个 HTTP 方法(如Postman),通过生成的 UI 发送值,并选择不同的内容类型报头,如application/jsonapplication/xml

它还提供了一些客户端 api,允许 Swagger 生成的端点与 JavaScript、AngularJS 或 Xamarin 等客户端技术之间进行通信。

测试 Web API

在本教程中,您将学习如何使用 Moq 和 xUnit 测试 Web API 控制器。

准备

让我们在 VS 2017 中创建一个类库项目,并使用本章Using ActionResultsrecipe 中创建的ProductAPIController方法。 我们将为这个 API 创建一些测试用例。

怎么做……

  1. 首先,让我们在解决方案中创建一个类库项目:

  1. 接下来,我们将更改项目中生成的代码,以导入xunitdotnet-test-xunitmoq。 我们还必须在 Web API 项目中添加引用。

  2. 下面是一些测试方法:

public class ProductApiControllerTests
{
  #region Tests for GET : api/productapi
  [Fact]
  public void GET
  _Returns404NotFoundResultIfProductListHaveNoItemsInRepo()
  {
    // Arrange
    var mockRepo = new Mock<IProductRepository>();
    var emptyProductList = GetEmptyProductsList();
    mockRepo.Setup(repo => repo.GetAllProducts())
    .Returns(emptyProductList);
    var controller = new ProductApiController(mockRepo.Object);
    // Act
    var result = controller.Get();
    // Assert
    Assert.IsType<NotFoundObjectResult>(result);
  }
  [Fact]
  public void GET
  _Returns200OkResultIfProductListHaveAtLeastOneItemInRepo()
  {
    // Arrange
    var mockRepo = new Mock<IProductRepository>();
    var productList = GetProductsList();
    mockRepo.Setup(repo => repo.GetAllProducts())
    .Returns(productList);
    var controller = new ProductApiController(mockRepo.Object);
    // Act
    var result = controller.Get();
    // Assert
    Assert.IsType<OkObjectResult>(result);
  }
  #endregion
  #region Tests for POST : api/productapi
  public void POST
  _Returns400BadRequestResultIfProductFromRequestParameterIsNull()
  {
    // Arrange
    var mockRepo = new Mock<IProductRepository>();
    var product = GetProduct();
    var nullProduct = GetNullProduct();
    mockRepo.Setup(repo => repo.Add(product));
    var controller = new ProductApiController(mockRepo.Object);
    // Act
    var result = controller.Post(nullProduct);
    // Assert
    Assert.IsType<BadRequestObjectResult>(result);
  }
  public void POST
  _Returns201CreatedAtRouteResultIfProductIsInsertedSuccesfully
  InRepo()
  {
    // Arrange
    var mockRepo = new Mock<IProductRepository>();
    var product = GetProduct();
    mockRepo.Setup(repo => repo.Add(product));
    var controller = new ProductApiController(mockRepo.Object);
    // Act
    var result = controller.Post(product);
    // Assert}
    Assert.IsType<CreatedAtRouteResult>(result);
  }
  #endregion
  #region private methods
  private IEnumerable<Product> GetEmptyProductsList()
  {
    return new List<Product>();
  }
  private IEnumerable<Product> GetProductsList()
  {
    return new List<Product>
    {
      new Product { Id = 1, Name = "Phone" },
      new Product { Id = 2, Name = "Laptop" },
      new Product { Id = 3, Name = "Computer" },
      new Product { Id = 4, Name = "Screen" },
      new Product { Id = 5, Name = "Mouse" }
    };
  }
  private Product GetEmptyProduct()
  {
    return new Product();
  }
  private Product GetNullProduct()
  {
    return null;
  }
  private Product GetProduct()
  {
    return new Product
    {
      Id = 6, Name = "Tablet"
    };
  }
  private Product GetProductById(int id)
  {
    return GetProductsList().Where(p => p.Id == id).SingleOrDefault();
  }
  private Product GetProductAleadyExist()
  {
    return GetProductsList()
    .Where(p => p.Id == 1).SingleOrDefault();
  }
  #endregion
}

Most of the test methods have been removed for brevity. The test project can be found on the GitHub repository, at  https://github.com/polatengin/B05277.

有更多的…

我们可以使用一些工具来测试我们的 api,例如:

管理异常

在本菜谱中,您将学习如何使用 Web API 管理异常。

通常,我们不想失去异常的原因。 因此,我们应该持久化并维护所有异常的日志。 就持久化异常日志所需的存储容量而言,异常日志可能是巨大的。

我们可以记录所有异常文件或数据库表(如该软件,甲骨文,或 MySql),或文档存储(如 MongoDb (https://www.mongodb.com/),或 Azure CosmosDb(https://azure.microsoft.com/en-us/services/cosmos-db/)。

记录的异常至少包含:

  • 一个描述性的信息
  • 异常消息
  • 异常。net 类型
  • 堆栈跟踪

我们通常只向客户端发送描述性消息,并记录关于异常的其他信息。

使用 Web API 2,在 ASP 之前。 在 asp.net Core 中,HttpError类向客户端发送了一个结构化错误。

HttpError传统上由 Web API 使用,以(某种)标准化的方式向客户端提供错误信息。 它有一些有趣的特性:

  • ExceptionMessage
  • ExceptionType
  • InnerException
  • Message
  • ModelState
  • MessageDetail
  • StackTrace

准备

让我们打开 ASP.NET Core Web API 项目,我们在Using ActionResultsrecipe 中创建。

怎么做……

  1. 首先,让我们将WebApiCompatShim库添加到project.json:
"Microsoft.AspNetCore.MVC.WebApiCompatShim": "1.0.0"
  1. 为了使用WebApiCompatShim,我们需要修改ConfigureServices中的一些代码来添加一个服务:
services.AddMVC().AddWebApiConventions();
  1. 接下来,我们将更改api/productapi/{id}的现有代码:
[HttpGet("{id:int}")]
public IActionResult Get(int id)
{
  var productFromRepo = _productRepository.Find(id);
  if (productFromRepo == null)
  return NotFound();
  return Ok(productFromRepo);
}

我们可以增加一个不同参数的Get()方法,如下:

[Route("{id:int}")]
public HttpResponseMessage Get(int id, HttpRequestMessage request)
{
  var product = _productRepository.Find(id);
  if (product == null)
  {
    Return request.CreateErrorResponse(HttpStatusCode.NotFound,
    new HttpError
    {
      Message = "This product does not exist",
      MessageDetail = string.Format("The product requested with
      ID {0} does not exist in the repository", id)
    });
  }
  return request.CreateResponse(product);
}
  1. 最后,如果我们发送一个产品 ID(在存储库中不存在)作为参数,我们将收到以下消息:

有更多的…

我们可以通过创建一个Exception中间件来全局管理异常。

从 Web API 2.1 开始,我们使用IExceptionHandler来全局处理异常。 它帮助捕获控制器、操作、过滤器、路由,有时还捕获MessageHandlersMediaTypeFormatters中的异常。

我们也可以考虑创建一个自定义全局Exception过滤器来记录异常,但是我们将在第 12 章Filters中讨论这个问题。