加速 ASP.NET Core WEB API 应用:第一部分






4.93/5 (60投票s)
使用 ASP.NET Core 2.1 创建一个测试 RESTful WEB API 服务
引言
在本文中,我们将回顾使用 ASP.NET Core 创建 ASP.NET WEB API 应用程序的过程。主要关注点将是应用程序的生产力。
文章分为两部分
- 第一部分:创建一个测试 RESTful WEB API 应用程序
- 第二部分:使用各种方法提高应用程序的生产力
- 第三部分:深入重构和优化 ASP.NET Core WEB API 应用程序代码
第一部分. 创建一个测试 RESTful WEB API 服务
在第一部分中,我们将创建一个异步 RESTful WEB API 服务,该服务能够搜索数据库中的产品,并获取不同供应商针对特定产品的价格列表。
进行编码,我们需要 Microsoft Visual Studio 2017(更新至 .NET Core 2.1)和 Microsoft SQL Server(任何版本)。
我们将审查以下内容
- 控制器 – 服务 – 存储库 – 数据库架构
- 在 Microsoft SQL Server 中创建数据库
- 创建一个 RESTful WEB API 应用程序
- 使用 Entity Framework Core (EFC) 处理数据库
- 异步设计模式
- 强制数据库数据完整性
- 使用 Swagger 服务检查 API
应用程序架构
我们将使用控制器 – 服务 – 存储库 – 数据库架构来构建我们的 WEB API 应用程序。
控制器负责路由 – 它们接收 http 请求并使用从请求参数或正文中接收的参数调用服务的相应方法。按照惯例,我们将封装业务逻辑的类命名为“Services”。请求处理后,服务将 `IActionResult` 类型的结果返回给控制器。控制器不关心服务结果的类型,而是直接将其作为 http 响应传输给用户。所有从数据库接收数据或将数据存储到数据库的方法都封装在存储库 (Repositories) 中。如果服务需要某些数据,它会请求存储库,而无需知道数据存储在哪里以及如何存储。
此模式提供了应用程序层的最大解耦,并使应用程序的开发和测试变得容易。
数据库
在我们的应用程序中,我们使用 Microsoft SQL Server。让我们为应用程序创建一个数据库,并用测试数据填充它,然后在 Microsoft SQL Server Management Studio 中执行以下查询
USE [master]
GO
CREATE DATABASE [SpeedUpCoreAPIExampleDB]
GO
USE [SpeedUpCoreAPIExampleDB]
GO
CREATE TABLE [dbo].[Products] (
[ProductId] INT IDENTITY (1, 1) NOT NULL,
[SKU] NCHAR (50) NOT NULL,
[Name] NCHAR (150) NOT NULL,
CONSTRAINT [PK_Products] PRIMARY KEY CLUSTERED ([ProductId] ASC)
);
GO
CREATE TABLE [dbo].[Prices] (
[PriceId] INT IDENTITY (1, 1) NOT NULL,
[ProductId] INT NOT NULL,
[Value] DECIMAL (18, 2) NOT NULL,
[Supplier] NCHAR (50) NOT NULL,
CONSTRAINT [PK_Prices] PRIMARY KEY CLUSTERED ([PriceId] ASC)
);
GO
ALTER TABLE [dbo].[Prices] WITH CHECK ADD CONSTRAINT [FK_Prices_Products] FOREIGN KEY([ProductId])
REFERENCES [dbo].[Products] ([ProductId])
ON DELETE CASCADE
GO
ALTER TABLE [dbo].[Prices] CHECK CONSTRAINT [FK_Prices_Products]
GO
INSERT INTO Products ([SKU], [Name]) VALUES ('aaa', 'Product1');
INSERT INTO Products ([SKU], [Name]) VALUES ('aab', 'Product2');
INSERT INTO Products ([SKU], [Name]) VALUES ('abc', 'Product3');
INSERT INTO Prices ([ProductId], [Value], [Supplier]) VALUES (1, 100, 'Bosch');
INSERT INTO Prices ([ProductId], [Value], [Supplier]) VALUES (1, 125, 'LG');
INSERT INTO Prices ([ProductId], [Value], [Supplier]) VALUES (1, 130, 'Garmin');
INSERT INTO Prices ([ProductId], [Value], [Supplier]) VALUES (2, 140, 'Bosch');
INSERT INTO Prices ([ProductId], [Value], [Supplier]) VALUES (2, 145, 'LG');
INSERT INTO Prices ([ProductId], [Value], [Supplier]) VALUES (2, 150, 'Garmin');
INSERT INTO Prices ([ProductId], [Value], [Supplier]) VALUES (3, 160, 'Bosch');
INSERT INTO Prices ([ProductId], [Value], [Supplier]) VALUES (3, 165, 'LG');
INSERT INTO Prices ([ProductId], [Value], [Supplier]) VALUES (3, 170, 'Garmin');
GO
现在我们有了一个名为 `SpeedUpCoreAPIExampleDB` 的数据库,其中填充了测试数据。
`Products` 表包含一个 `products` 列表。SKU 字段用于在列表中搜索 `products`。
`Prices` 表包含一个价格列表。
这些表之间的关系可以形象地表示为
注意! 我们创建了一个带有 CASCADE 删除规则的外键 `FK_Prices_Products`,以便在删除 `Products` 表中的记录时,MS SQL Server 能够提供 `Products` 和 `Prices` 表之间的数据完整性。
创建 ASP.NET Core WEB API 应用程序
在 Microsoft Visual Studio 中,启动新的 .NET Core 项目 `SpeedUpCoreAPIExample`。
然后选择 Web API
由于我们正在制作一个 Web API Core 应用程序,我们应该安装 `Microsoft.AspNetCore.Mvc` NuGet 包。转到菜单主菜单 > 工具 > NuGet 包管理器 > 管理解决方案的 NuGet 程序包,然后输入 `Microsoft.AspNetCore.Mvc`。选择并安装该包
下一步是创建我们应用程序的数据模型。由于我们已经创建了数据库,因此使用脚手架机制从数据库结构生成数据模型似乎是合乎逻辑的。但我们不会使用脚手架,因为数据库结构不能完全反映应用程序的数据模型 - 在数据库中,我们遵循使用复数名称(如“Products
”和“Prices
”)来命名表,同时将表视为“Product
”和“Price
”的行集合。在我们的应用程序中,我们想将实体类命名为“Product
”和“Price
”,但脚手架完成后,我们将创建名称为“Products
”和“Prices
”的实体,以及一些反映实体之间关系的自动创建的其他对象。
因此,我们不得不重写代码。这就是为什么我们决定手动创建数据模型。
在解决方案资源管理器中,右键单击你的项目并选择添加 > 新建文件夹。
将其命名为 *Models*。在 *Models* 文件夹中,让我们创建两个实体类:*Product.cs* 和 *Price.cs*。
右键单击 *Models* 文件夹,然后选择添加项 > 类 > *Product.cs*。
输入 `Product` 类的文本
namespace SpeedUpCoreAPIExample.Models
{
public class Product
{
public int ProductId { get; set; }
public string Sku { get; set; }
public string Name { get; set; }
}
}
对于 *Price.cs* 也是如此
namespace SpeedUpCoreAPIExample.Models
{
public class Price
{
public int PriceId { get; set; }
public int ProductId { get; set; }
public decimal Value { get; set; }
public string Supplier { get; set; }
}
}
在 `Price` 类中,我们使用 `Value` 字段来存储价格值 - 我们不能将字段命名为“Price
”,因为字段不能与类的名称相同(我们在数据库的 `Prices` 表中也使用“Value
”字段)。稍后在 `Database` 上下文类中,我们将把这些实体映射到数据库表“Products
”和“Prices
”。
请注意,在模型中,`Products` 和 `Prices` 实体之间没有任何关系。
使用 Entity Framework Core 进行数据库访问
为了访问数据库,我们将使用 Entity Framework Core。为此,我们需要为我们的数据库安装一个 `EntityFrameworkCore` 提供程序。转到菜单主菜单 > 工具 > NuGet 包管理器 > 管理解决方案的 NuGet 程序包,并在浏览字段中输入 `Microsoft.EntityFrameworkCore.SqlServer`,因为我们正在使用 Microsoft SQL Server。选择并安装该包
为了让 Entity Framework 知道如何处理我们的数据模型,我们应该创建一个 `Database` 上下文类。为此,让我们创建一个新文件夹 *Contexts*,右键单击它并选择添加 > 新项 > ASP.NET Core > 代码 > 类。将类命名为 *DefaultContext.cs*。
输入类的以下文本
using Microsoft.EntityFrameworkCore;
using SpeedUpCoreAPIExample.Models;
namespace SpeedUpCoreAPIExample.Contexts
{
public class DefaultContext : DbContext
{
public virtual DbSet<Product> Products { get; set; }
public virtual DbSet<Price> Prices { get; set; }
public DefaultContext(DbContextOptions<DefaultContext> options) : base(options)
{
}
}
}
在以下几行中,我们映射了数据模型实体类到数据库表
public virtual DbSet<Product> Products { get; set; }
public virtual DbSet<Price> Prices { get; set; }
根据 Entity Framework Core 对实体键名称的命名约定,模型键字段应命名为“Id
”或 `EntitynameId`(不区分大小写),以便 EFC 自动映射到数据库键。我们使用“ProductId
”和“PriceId
”名称,它们符合约定。如果我们使用非标准名称作为键字段,我们将在 `DbContext` 中显式配置键。
下一步是声明数据库上下文,在应用程序的 `Startup` 类中。打开应用程序根目录下的 *Startup.cs* 文件,并在 `ConfigureServices` 方法中添加“using
”指令
using Microsoft.EntityFrameworkCore;
using SpeedUpCoreAPIExample.Contexts;
并更正 `ConfigureServices` 过程。
public void ConfigureServices(IServiceCollection services)
{
services.AddMvc();
services.AddDbContext<DefaultContext>(options =>
options.UseSqlServer(Configuration.GetConnectionString("DefaultDatabase")));
}
最后一步是配置数据库的连接字符串。为此,找到我们应用程序根目录下的 *appsettings.json* 文件,并添加以下 `ConnectionStrings` 会话
"ConnectionStrings": {
"DefaultDatabase": "Server=localhost;Database=SpeedUpCoreAPIExampleDB;Integrated Security=True;"
}
但请注意,在应用程序的根目录下还有一个 *appsettings.Development.json* 配置文件。默认情况下,此文件在开发过程中使用。因此,您应该将 `ConnectionStrings` 会话复制到那里,否则 `Configuration.GetConnectionString` 可能会返回 `null`。
现在,我们的应用程序已准备好处理数据库。
异步设计模式
异步工作是我们应用程序提高生产力的第一步。异步的所有好处将在第二部分讨论。
我们希望所有存储库都能异步工作,因此它们将返回 `Task
存储库
我们将创建两个存储库 – 一个用于 `Product` 实体,另一个用于 `Price` 实体。我们将首先在相应的接口中声明存储库方法,以便存储库准备好进行依赖注入。
让我们为接口创建一个新文件夹 Interfaces。右键单击 *Interfaces* 文件夹,并添加一个名为 *IProductsRepository.cs* 的新类,并将其代码更改为
using SpeedUpCoreAPIExample.Models;
using System.Collections.Generic;
using System.Threading.Tasks;
namespace SpeedUpCoreAPIExample.Repositories
{
public interface IProductsRepository
{
Task<IEnumerable<Product>> GetAllProductsAsync();
Task<Product> GetProductAsync(int productId);
Task<IEnumerable<Product>> FindProductsAsync(string sku);
Task<Product> DeleteProductAsync(int productId);
}
}
然后是 *IPricesRepository.cs*
using SpeedUpCoreAPIExample.Models;
using System.Collections.Generic;
using System.Threading.Tasks;
namespace SpeedUpCoreAPIExample.Interfaces
{
public interface IPricesRepository
{
Task<IEnumerable<Price>> GetPricesAsync(int productId);
}
}
存储库实现
创建一个新文件夹 *Repositories*,并添加具有以下代码的 `ProductsRepository` 类
using Microsoft.EntityFrameworkCore;
using SpeedUpCoreAPIExample.Contexts;
using SpeedUpCoreAPIExample.Interfaces;
using SpeedUpCoreAPIExample.Models;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace SpeedUpCoreAPIExample.Repositories
{
public class ProductsRepository : IProductsRepository
{
private readonly DefaultContext _context;
public ProductsRepository(DefaultContext context)
{
_context = context;
}
public async Task<IEnumerable<Product>> GetAllProductsAsync()
{
return await _context.Products.ToListAsync();
}
public async Task<Product> GetProductAsync(int productId)
{
return await _context.Products.Where(p => p.ProductId == productId).FirstOrDefaultAsync();
}
public async Task<IEnumerable<Product>> FindProductsAsync(string sku)
{
return await _context.Products.Where(p => p.Sku.Contains(sku)).ToListAsync();
}
public async Task<Product> DeleteProductAsync(int productId)
{
Product product = await GetProductAsync(productId);
if (product != null)
{
_context.Products.Remove(product);
await _context.SaveChangesAsync();
}
return product;
}
}
}
在 `ProductsRepository` 类的构造函数中,我们通过依赖注入注入了 `DefaultContext`。
然后创建 *PricesRepository.cs*,其中包含代码
using Microsoft.EntityFrameworkCore;
using SpeedUpCoreAPIExample.Contexts;
using SpeedUpCoreAPIExample.Interfaces;
using SpeedUpCoreAPIExample.Models;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace SpeedUpCoreAPIExample.Repositories
{
public class PricesRepository : IPricesRepository
{
private readonly DefaultContext _context;
public PricesRepository(DefaultContext context)
{
_context = context;
}
public async Task<IEnumerable<Price>> GetPricesAsync(int productId)
{
return await _context.Prices.Where(p => p.ProductId == productId).ToListAsync();
}
}
}
最后一步是在 `Startup` 类中声明我们的存储库。在 *Startup.cs* 的 `ConfigureServices` 方法中添加“using
”指令
using SpeedUpCoreAPIExample.Interfaces;
using SpeedUpCoreAPIExample.Repositories;
在 `DefaultContext` 声明之后
services.AddScoped<IProductsRepository, ProductsRepository>();
services.AddScoped<IPricesRepository, PricesRepository>();
注意! 声明的顺序对于依赖注入很重要 – 如果您想将 `DefaultContext` 注入到存储库中,那么 `DefaultContext` 应该在存储库之前声明。对于存储库,我们使用 Scoped 生命周期模型,因为系统会自动以 Scoped 模型注册数据库上下文。而使用该上下文的存储库应具有相同的生命周期模型。
服务
我们的“Services
”类封装了除数据库访问以外的所有业务逻辑 – 为此,它们注入了存储库。所有服务方法都异步运行,并根据数据处理结果返回 `IActionResult`。服务还会处理错误并相应地执行输出结果格式化。
在开始实现服务之前,让我们考虑一下我们将要发送回给用户的数据。例如,我们的模型类“Price
”有两个字段“PriceId
”和“ProductId
”,我们使用它们从数据库获取数据,但它们对用户来说毫无意义。更重要的是,如果我们 API 返回整个实体,我们可能会偶尔暴露一些敏感数据。此外,我们将使用“Price
”字段来返回价格值,这在处理价格列表时很常见。我们将使用“Id
”字段来返回 `ProductId`。“Id
”名称将对应于产品标识的 API 参数名称(将在“Controllers
”部分介绍)。
因此,为输出创建一个具有有限字段集的数据模型是一个好习惯。
让我们创建一个新文件夹 *ViewModels*,并在那里添加两个类
namespace SpeedUpCoreAPIExample.ViewModels
{
public class ProductViewModel
{
public int Id { get; set; }
public string Sku { get; set; }
public string Name { get; set; }
}
}
和
namespace SpeedUpCoreAPIExample.ViewModels
{
public class PriceViewModel
{
public decimal Price { get; set; }
public string Supplier { get; set; }
}
}
这些类是实体类 `Product` 和 `Price` 的精简安全版本,没有额外的字段,并且调整了字段名称。
服务接口
一个服务可以完成所有工作,但我们将创建尽可能多的服务,就像存储库一样。通常,我们从在接口中声明服务方法开始。
右键单击 *Interfaces* 文件夹,并创建一个名为 `IProductsService` 的新类,其代码如下
using Microsoft.AspNetCore.Mvc;
using System.Threading.Tasks;
namespace SpeedUpCoreAPIExample.Interfaces
{
public interface IProductsService
{
Task<IActionResult> GetAllProductsAsync();
Task<IActionResult> GetProductAsync(int productId);
Task<IActionResult> FindProductsAsync(string sku);
Task<IActionResult> DeleteProductAsync(int productId);
}
}
然后是 `IPricesService`,代码如下
using Microsoft.AspNetCore.Mvc;
using System.Threading.Tasks;
namespace SpeedUpCoreAPIExample.Interfaces
{
public interface IPricesService
{
Task<IActionResult> GetPricesAsync(int productId);
}
}
服务的实现
创建一个新文件夹 *Services*,并添加一个新类 `ProductsService`
using Microsoft.AspNetCore.Mvc;
using SpeedUpCoreAPIExample.Interfaces;
using SpeedUpCoreAPIExample.Models;
using SpeedUpCoreAPIExample.ViewModels;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace SpeedUpCoreAPIExample.Services
{
public class ProductsService : IProductsService
{
private readonly IProductsRepository _productsRepository;
public ProductsService(IProductsRepository productsRepository)
{
_productsRepository = productsRepository;
}
public async Task<IActionResult> FindProductsAsync(string sku)
{
try
{
IEnumerable<Product> products = await _productsRepository.FindProductsAsync(sku);
if (products != null)
{
return new OkObjectResult(products.Select(p => new ProductViewModel()
{
Id = p.ProductId,
Sku = p.Sku.Trim(),
Name = p.Name.Trim()
}
));
}
else
{
return new NotFoundResult();
}
}
catch
{
return new ConflictResult();
}
}
public async Task<IActionResult> GetAllProductsAsync()
{
try
{
IEnumerable<Product> products = await _productsRepository.GetAllProductsAsync();
if (products != null)
{
return new OkObjectResult(products.Select(p => new ProductViewModel()
{
Id = p.ProductId,
Sku = p.Sku.Trim(),
Name = p.Name.Trim()
}
));
}
else
{
return new NotFoundResult();
}
}
catch
{
return new ConflictResult();
}
}
public async Task<IActionResult> GetProductAsync(int productId)
{
try
{
Product product = await _productsRepository.GetProductAsync(productId);
if (product != null)
{
return new OkObjectResult(new ProductViewModel()
{
Id = product.ProductId,
Sku = product.Sku.Trim(),
Name = product.Name.Trim()
});
}
else
{
return new NotFoundResult();
}
}
catch
{
return new ConflictResult();
}
}
public async Task<IActionResult> DeleteProductAsync(int productId)
{
try
{
Product product = await _productsRepository.DeleteProductAsync(productId);
if (product != null)
{
return new OkObjectResult(new ProductViewModel()
{
Id = product.ProductId,
Sku = product.Sku.Trim(),
Name = product.Name.Trim()
});
}
else
{
return new NotFoundResult();
}
}
catch
{
return new ConflictResult();
}
}
}
}
以及 `PricesService`
using Microsoft.AspNetCore.Mvc;
using SpeedUpCoreAPIExample.Interfaces;
using SpeedUpCoreAPIExample.Models;
using SpeedUpCoreAPIExample.ViewModels;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace SpeedUpCoreAPIExample.Services
{
public class PricesService : IPricesService
{
private readonly IPricesRepository _pricesRepository;
public PricesService(IPricesRepository pricesRepository)
{
_pricesRepository = pricesRepository;
}
public async Task<IActionResult> GetPricesAsync(int productId)
{
try
{
IEnumerable<Price> pricess = await _pricesRepository.GetPricesAsync(productId);
if (pricess != null)
{
return new OkObjectResult(pricess.Select(p => new PriceViewModel()
{
Price = p.Value,
Supplier = p.Supplier.Trim()
}
)
.OrderBy(p => p.Price)
.ThenBy(p => p.Supplier)
);
}
else
{
return new NotFoundResult();
}
}
catch
{
return new ConflictResult();
}
}
}
}
Products 和 Prices 表之间的数据完整性
在 `ProductsService` 中,我们有一个 `DeleteProductAsync` 方法,它调用 `PricesRepository` 的相应方法来从 `Products` 表中删除产品数据行。我们还通过 `FK_Prices_Products` 外键在 `Products` 和 `Prices` 表之间建立了关系。由于 `FK_Prices_Products` 外键具有 CASCADE 删除规则,因此在从 `Products` 表删除记录时,`Prices` 表中的相关记录也会被自动删除。
还有其他一些方法可以在数据库中不使用带有级联删除的外键来强制执行数据完整性。例如,我们可以配置 Entity Framework 使用 `WillCascadeOnDelete()` 方法执行级联删除。但这还需要对我们的数据模型进行重新设计。另一种方法是在 `PricesService` 中实现一个 `DeletePricessAsync` 方法,并在 `DeleteProductAsync` 方法中调用它。但我们必须考虑在一个事务中完成此操作,因为我们的应用程序可能会在 `Product` 已删除但价格尚未删除时失败。这样,我们就会丢失数据完整性。
在我们的示例中,我们使用带有级联删除的外键来强制执行数据完整性。
注意! 显然,在实际应用程序中,`DeleteProductAsync` 方法不应如此轻易地调用,因为重要数据可能会因意外或有意而丢失。在我们的示例中,我们仅用于揭示数据完整性的概念。
服务模式
在服务的构造函数中,我们通过依赖注入注入相应的存储库。每个方法都在 `try` – `catch` 结构中从存储库获取数据,并根据数据处理结果返回 `IActionResult`。当返回一个 `dataset` 时,数据将被转换为 *ViewModel* 文件夹中的一个类。
请注意,响应 `OkObjectResult()`、`NotFoundResult()`、`ConflictResult()` 等分别对应于 `Controller` 的 `ControllerBase Ok()`、`NotFound()`、`Conflict()` 方法。`Service` 以相同的 `IActionResult` 类型将其响应发送给 `Controller`,就像 `Controller` 发送给用户一样。这意味着 `Controller` 可以直接将响应传递给用户,而无需进行调整。
服务的最后一步是在 `Startup` 类中声明它们。在 `ConfigureServices` 中添加 `using` 指令
using SpeedUpCoreAPIExample.Services;
并在存储库声明之后,在 `ConfigureServices` 方法中声明 `Services`
services.AddTransient<IProductsService, ProductsService>();
services.AddTransient<IPricesService, PricesService>();
由于我们的服务是轻量级的且无状态的,我们可以使用 Transient Services 作用域模型。
此时,最终的 `ConfigureServices` 方法是
public void ConfigureServices(IServiceCollection services)
{
services.AddMvc();
services.AddDbContext<DefaultContext>(options =>
options.UseSqlServer(Configuration.GetConnectionString("DefaultDatabase")));
services.AddScoped<IProductsRepository, ProductsRepository>();
services.AddScoped<IPricesRepository, PricesRepository>();
services.AddTransient<IProductsService, ProductsService>();
services.AddTransient<IPricesService, PricesService>();
}
控制器
在我们的设计模式中,留给控制器的内容不多了,只是充当传入请求的网关 – 没有业务逻辑、数据访问、错误处理等。控制器将根据其路由接收传入请求,调用我们通过依赖注入注入的服务的方法,并返回这些方法的执行结果。
通常,我们将创建两个小型控制器而不是一个大型控制器,因为它们处理逻辑上不同的数据,并且具有不同的 API 路由。
ProductsController 路由
[HttpGet] 路由
- /api/products – 返回所有产品的列表
- /api/products/1 – 返回 ID = 1 的一个产品
- /api/products/find/aaa – 返回 SKU 字段包含参数“sku”值“aaa”的产品的列表
[HttpDelete] 路由
- /api/product/1 – 删除 ID = 1 的产品及其价格(级联删除)
PricesController 路由
[HttpGet] 路由
- /api/prices/1 – 返回 ID = 1 的产品的价格列表
创建控制器
右键单击 *Controllers* 文件夹,然后选择添加项 > 类 > *ProductsController.cs* 并将文本更改为
using Microsoft.AspNetCore.Mvc;
using SpeedUpCoreAPIExample.Interfaces;
using System.Threading.Tasks;
namespace SpeedUpCoreAPIExample.Controllers
{
[Route("api/[controller]")]
[ApiController]
public class ProductsController : Controller
{
private readonly IProductsService _productsService;
public ProductsController(IProductsService productsService)
{
_productsService = productsService;
}
// GET /api/products
[HttpGet]
public async Task<IActionResult> GetAllProductsAsync()
{
return await _productsService.GetAllProductsAsync();
}
// GET /api/products/5
[HttpGet("{id}")]
public async Task<IActionResult> GetProductAsync(int id)
{
return await _productsService.GetProductAsync(id);
}
// GET /api/products/find
[HttpGet("find/{sku}")]
public async Task<IActionResult> FindProductsAsync(string sku)
{
return await _productsService.FindProductsAsync(sku);
}
// DELETE /api/products/5
[HttpDelete("{id}")]
public async Task<IActionResult> DeleteProductAsync(int id)
{
return await _productsService.DeleteProductAsync(id);
}
}
}
Controller
的名称应带有“Controller
”后缀。使用指令 `[Route("api/[controller]")]` 意味着所有 `ProductsController` 的基本路由,即控制器的 API 将是 */api/products*。
对于 `PricesController` 控制器也是如此
using Microsoft.AspNetCore.Mvc;
using SpeedUpCoreAPIExample.Interfaces;
using System.Threading.Tasks;
namespace SpeedUpCoreAPIExample.Contexts
{
[Route("api/[controller]")]
[ApiController]
public class PricesController : ControllerBase
{
private readonly IPricesService _pricesService;
public PricesController(IPricesService pricesService)
{
_pricesService = pricesService;
}
// GET /api/prices/1
[HttpGet("{Id}")]
public async Task<IActionResult> GetPricesAsync(int id)
{
return await _pricesService.GetPricesAsync(id);
}
}
}
现在一切都差不多准备好了,可以第一次启动我们的应用程序了。在启动应用程序之前,让我们看一下应用程序的 */Properties* 文件夹中的 *launchSettings.json* 文件。我们可以看到,“launchUrl
”: “api/values
”。让我们删除“/values”。在此文件中,我们还可以更改 `applicationUrl` 参数中的端口号:“applicationUrl
”: “https://:49858/
”,在我们的例子中,端口是 `49858`。
我们还可以从 *Controllers* 文件夹中删除 *ValuesController.cs* 控制器。该控制器由 Visual Studio 自动创建,我们在应用程序中不使用它。
通过单击主菜单 > 调试 > 启动但不调试(或按 Ctrl+F5)来启动应用程序。应用程序将在 Internet Explorer 浏览器(默认)中打开,URL 为 *https://:49858/api*。
检查应用程序
我们将使用 Swagger 工具来检查我们的应用程序。最好为此使用 Google Chrome 或 Firefox 浏览器。因此,打开 Firefox 并将 https://inspector.swagger.io/builder 输入到 URL 字段。系统将要求您安装 Swagger Inspector 扩展。
添加扩展。
现在浏览器中有一个按钮可以启动该扩展。打开它,选择 `GET` 方法,然后输入 API 的 URL
https://:49858/api/products
单击发送按钮,您将收到一个 JSON 格式的所有产品列表
检查特定 `Product`
https://:49858/api/products/1
按 SKU 的一部分查找产品
https://:49858/api/products/find/aa
然后检查 `PricesController` API
https://:49858/api/prices/1
检查 `Delete` API。
在 Swagger 中将方法更改为 `DELETE` 并调用 API
https://:49858/api/products/3
要检查删除结果,我们可以使用 `GET` 方法调用 API *https://:49858/api/products/3*。结果将是 `404 Not Found`。
调用 *https://:49858/api/prices/3* 将返回一个空的价格集。
摘要
最后,我们拥有了一个工作的 ASP.NET Core RESTful WEB API 服务。
到目前为止,一切都相对简单,只是为探索应用程序生产力问题做准备。
但在现阶段,为了提高应用程序性能,已经完成了一些重要的事情——实现了异步设计模式。