第 012 集 - 迁移到 Web API - ASP.NET Core:从零到精通
在本集中,我们将开始把当前的应用程序转换为 Web API,以便它可以在我们即将开发的单页应用程序中使用。
如需了解详细步骤,您可以观看下一个视频;如果您更喜欢快速阅读,请跳至文字总结部分。
整个系列的播放列表在这里。
引言
目前我们一直在构建一个服务器端渲染的 MVC 应用程序。然而,这个项目的最终目标是创建一个单页应用程序 (SPA),以处理大部分 UI 需求。为了实现这一目标,我们将把这个 MVC 应用程序迁移到 Web API,以便它将来以及我们构建的其他组件可以被 SPA(或其他可能需要它的组件)使用。
另外,关于我称之为 Web API 或 HTTP API 而不是 REST API 这一点,我想说明一下。我不想因为没有完全按照 REST API 的要求(包括但不限于超媒体)来实现,就把它称为 REST API 而惹恼任何人。姑且称之为类 REST API(😛),因为它实现了大多数人期望的功能,但并不完全符合 REST 的标准。
删除不需要的部分
由于我们要迁移到 Web API,有些东西我们不再需要了,可以进行清理/调整,具体如下:
- 移除视图
- 替换
AddMvc为AddMvcCore,仅添加所需的 MVC 功能 - 继承自
ControllerBase而不是Controller
关于第一个问题,没什么好说的,直接删除存放视图的文件夹就行了,因为我们不再需要它们了。至于其他问题,我们来详细说说。
将 AddMvc 替换为 AddMvcCore
AddMvc它会在依赖注入容器中注册 MVC 的服务,包括它确定需要的以及一些可能需要的服务。为此,它会调用AddMvcCore并添加一些其他服务。我们可以通过AddMvcCore直接调用来避免注册不需要的服务,以及任何其他我们已知需要的 MVC 服务。
这会对性能产生影响吗?可能不会有太大影响(虽然我没有实际测试过),但既然如此,我们就移除一些未使用的部分,并仔细研究一下可用的 MVC 功能。不过大多数情况下,保留它们就好AddMvc,不用担心 🙂
让我们看一下MvcServiceCollectionExtensions.csGitHub 上的文件,其中AddMvc定义了它。
// ...
public static class MvcServiceCollectionExtensions
{
/// <summary>
/// Adds MVC services to the specified <see cref="IServiceCollection" />.
/// </summary>
/// <param name="services">The <see cref="IServiceCollection" /> to add services to.</param>
/// <returns>An <see cref="IMvcBuilder"/> that can be used to further configure the MVC services.</returns>
public static IMvcBuilder AddMvc(this IServiceCollection services)
{
// ...
var builder = services.AddMvcCore();
builder.AddApiExplorer();
builder.AddAuthorization();
AddDefaultFrameworkParts(builder.PartManager);
// Order added affects options setup order
// Default framework order
builder.AddFormatterMappings();
builder.AddViews();
builder.AddRazorViewEngine();
builder.AddRazorPages();
builder.AddCacheTagHelper();
// +1 order
builder.AddDataAnnotations(); // +1 order
// +10 order
builder.AddJsonFormatters();
builder.AddCors();
return new MvcBuilder(builder.Services, builder.PartManager);
}
private static void AddDefaultFrameworkParts(ApplicationPartManager partManager)
{
var mvcTagHelpersAssembly = typeof(InputTagHelper).GetTypeInfo().Assembly;
if (!partManager.ApplicationParts.OfType<AssemblyPart>().Any(p => p.Assembly == mvcTagHelpersAssembly))
{
partManager.ApplicationParts.Add(new FrameworkAssemblyPart(mvcTagHelpersAssembly));
}
var mvcRazorAssembly = typeof(UrlResolutionTagHelper).GetTypeInfo().Assembly;
if (!partManager.ApplicationParts.OfType<AssemblyPart>().Any(p => p.Assembly == mvcRazorAssembly))
{
partManager.ApplicationParts.Add(new FrameworkAssemblyPart(mvcRazorAssembly));
}
}
// ...
}
我清理了文件中一些我们目前不需要的部分。
查看代码AddMvc,我们发现它首先调用了 `get_configuration()` 方法AddMvcCore,并将返回值存储IMvcBuilder在一个变量中以供后续配置,这也是我们需要做的。现在让我们来看看使用构建器配置的其他内容。
- AddApiExplorer 用于公开 MVC 应用程序的相关信息。例如,它可用于创建 Swagger 文档端点。更多信息请查看Andrew Lock 的文章。我们最终会使用 Swagger,但既然目前还没用上……那就先用起来吧!
- AddAuthorization - 添加我们将来肯定会需要的必要授权服务,但目前不需要,所以……退出!
- AddDefaultFrameworkParts - 查看
AddDefaultFrameworkParts下面的实现AddMvc,我们可以看到它正在注册来自 Razor 和 TagHelpers 程序集的服务,因此我们也可以绕过它。 - AddFormatterMappings——我看了看这个函数的实现才明白它的作用,它基本上注册了一个
FormatFilter检查请求的路由数据和查询字符串中是否存在格式参数的映射,其作用类似于Accept请求头。我们不需要这个映射,所以可以跳过它。 - 添加视图 - 视图...下一个!
- AddRazorViewEngine - 更多 Razor 相关内容,敬请期待!
- 添加 RazorPages - 以及更多 Razor 代码……我们继续跳过吧。
- AddCacheTagHelper - 更多 TagHelper,跳过。
- AddDataAnnotations - 我们目前还没有使用数据注释,而且我也不打算将来在这个 API 中使用它们(我们将使用Fluent Validation),所以我们也可以安全地忽略它。
- AddJsonFormatters - 现在我们需要这个,因为我们的 API 需要处理 JSON。
- AddCors - 我预计我们不会从不同的域访问 API,所以我们也可以安全地忽略这一点。
那么,最终结果是什么呢?在Startup类中,`the`services.AddMvc();被替换为services.AddRequiredMvcComponents();一个新的扩展方法,我们将其添加到ServiceCollectionExtensions已创建的类中。在这个文件中,我们添加以下内容:
ServiceCollectionExtensions.cs
public static class ServiceCollectionExtensions
{
public static IServiceCollection AddRequiredMvcComponents(this IServiceCollection services)
{
var mvcBuilder = services.AddMvcCore();
mvcBuilder.AddJsonFormatters();
return services;
}
// ...
}
考虑到我们现有的条件,我们只替换了 ` AddMvcwith` AddMvcCore,并添加了对 `.` 的调用AddJsonFormatters。稍后我们会再回到这个方法,因为我们需要一些额外的配置来进行更改。
继承自 ControllerBase 而不是 Controller
我们的GroupsController类目前继承自Controller。它仍然可以继承自Controller,但由于唯一Controller没有ControllerBase(顺便说一句,它继承自 )的是对视图的支持,我们可以跳过不必要的额外部分。
由于这一改动,我们仍然在调用该View方法,因此会出现编译错误。我们马上就来解决这个问题。
调整端点
现在让我们把我们的代码重构GroupsController为一个 API 控制器。总而言之,它最终会包含一些签名与原代码基本相同的方法IGroupsService,这些方法会调用服务的各种方法,并添加一些额外的“控制器特性”,例如路由、HTTP 方法和 HTTP 响应。
GroupsController.cs
[Route("groups")]
public class GroupsController : ControllerBase
{
private readonly IGroupsService _groupsService;
public GroupsController(IGroupsService groupsService)
{
_groupsService = groupsService;
}
[HttpGet]
[Route("")]
public async Task<IActionResult> GetAllAsync(CancellationToken ct)
{
var result = await _groupsService.GetAllAsync(ct);
return Ok(result.ToModel());
}
[HttpGet]
[Route("{id}")]
public async Task<IActionResult> GetByIdAsync(long id, CancellationToken ct)
{
var group = await _groupsService.GetByIdAsync(id, ct);
if (group == null)
{
return NotFound();
}
return Ok(group.ToModel());
}
[HttpPut]
[Route("{id}")]
public async Task<IActionResult> UpdateAsync(long id, GroupModel model, CancellationToken ct)
{
model.Id = id; //not needed when we move to MediatR
var group = await _groupsService.UpdateAsync(model.ToServiceModel(), ct);
return Ok(group.ToModel());
}
[HttpPut]
[HttpPost]
[Route("")]
public async Task<IActionResult> AddAsync(GroupModel model, CancellationToken ct)
{
model.Id = 0; //not needed when we move to MediatR
var group = await _groupsService.AddAsync(model.ToServiceModel(), ct);
return CreatedAtAction(nameof(GetByIdAsync), new { id = group.Id }, group.ToModel());
}
[HttpDelete]
[Route("{id}")]
public async Task<IActionResult> RemoveAsync(long id, CancellationToken ct)
{
await _groupsService.RemoveAsync(id, ct);
return NoContent();
}
}
我觉得这里发生的事情很容易理解,但我还是会尽量指出一些细节。
-
读取操作及其路由基本保持不变,只是对一些方法名称进行了调整,并直接返回模型而不是视图(JSON 或任何其他格式的序列化由框架处理)。
-
更新操作绑定到 HTTP PUT,这是 HTTP API 中常见的做法,因为我们期望完全替换存储的实体。
-
添加操作同时绑定到 HTTP POST 和 PUT 方法,与更新操作不同的是,添加操作不需要 ID。当然,这取决于当前的实现,因为该实现会自动生成 ID。其他实现可能允许客户端提供 ID,在这种情况下,我们可能会将这两个操作合并为一个操作:如果实体不存在则添加,否则更新。
-
添加操作返回 HTTP 201 Created 状态代码,其中包含
Location指向要获取添加实体的 URL 的标头,同时也将其包含在响应正文中。
本节最后一点需要注意的是,在 add 和 update 方法中,我覆盖了客户端返回的 id。我本不应该这样做,但这样做是为了确保服务不会收到不一致或意外的 id——例如,在 add 方法中定义了一个本不应该有 id 的 id(至少在我们目前的做法中是这样,允许定义 id 也可能是有效的),而在 update 方法中定义了一个与路由中设置的 id 不同的 id。
问题主要在于我们对所有操作都使用了相同的模型。最佳方案是为每个操作使用特定的模型,我们将在把MediatR添加到项目时讨论这个问题。
使用 API 控制器属性
API 基本已经准备就绪,但如果我们现在尝试运行它,读取操作将按预期工作,但写入操作则不会。
我们尝试通过向http://localhost:5000/groupsinfo发送 POST 请求来添加一个新组{ "name": "Test Group" }。我们收到的响应如下:
{
"id": 1,
"name": null,
"rowVersion": "611"
}
和id是rowVersion正确的,但不是name。前者没问题,因为它们是在服务器端生成的,但名称没有传递给操作方法。如果我们在方法中设置断点,AddAsync会发现model参数为空。同样的情况也发生在UpdateAsync。
问题在于,操作本身并不知道应该model从响应体中反序列化数据。我们可以通过[FromBody]向model参数添加属性来轻松解决这个问题。
这尚可接受,但我们可以做得更好。ASP.NET Core 2.1ApiController引入了 `@Controller` 属性,允许我们装饰控制器,使其遵循一些约定,从而节省我们的工作,例如推断复杂对象来自请求体。当然,这些约定是可以重写的,但默认设置可以避免一些重复输入。您可以点击此处阅读更多关于此属性的信息。
要使用此属性,我们必须转到依赖注入 MVC 配置并添加以下内容:
ServiceCollectionExtensions.cs
public static class ServiceCollectionExtensions
{
public static IServiceCollection AddRequiredMvcComponents(this IServiceCollection services)
{
var mvcBuilder = services.AddMvcCore();
mvcBuilder.SetCompatibilityVersion(CompatibilityVersion.Version_2_2);
// ...
}
// ...
}
由于使用此属性需要使用 ASP.NET Core MVC 2.1 及更高版本中的新组件,这可能会导致与 2.0 版本相比出现重大变更,因此我们必须明确告知系统我们要使用这些新特性。点击此处了解更多关于兼容性版本的信息。
我使用的是兼容版本 2.2,因为在这个版本中,我们可以将该ApiController属性用作程序集属性,自动应用于所有控制器,而无需对每个控制器进行修饰。
我在Startup类文件中添加了属性[assembly: ApiController],现在控制器可以正常工作了🙂
创建例外过滤器
基于前几集所学的知识,我们可以创建一个过滤器ExceptionFilter。在本例中,我们将创建一个过滤器,当由于组已过期而导致更新失败时(类似于我们在上一集中看到的乐观并发异常),该过滤器将返回 HTTP 409 冲突状态码。我们稍后可以改进该过滤器以处理更多类型的错误。
ApiExceptionFilter.cs
public class ApiExceptionFilter : IExceptionFilter
{
public void OnException(ExceptionContext context)
{
if (context.Exception is DbUpdateConcurrencyException)
{
context.Result =
new ConflictObjectResult(
new
{
Message = "The updated entity has changed, please refresh your current copy."
});
}
}
}
未来我们可以改进这一点,让业务层将到达 API 的异常抽象成更通用的并发异常,而不是绑定到 Entity Framework 特有的异常。但就目前而言,这已经足够好了。
现在我们回到ServiceCollectionExtensions课堂,通过向 MVC 配置中添加此过滤器,完成(今天)对 MVC 配置的更改。
public static class ServiceCollectionExtensions
{
public static IServiceCollection AddRequiredMvcComponents(this IServiceCollection services)
{
services.AddTransient<ApiExceptionFilter>();
var mvcBuilder = services.AddMvcCore(options =>
{
options.Filters.AddService<ApiExceptionFilter>();
});
mvcBuilder.SetCompatibilityVersion(CompatibilityVersion.Version_2_2);
mvcBuilder.AddJsonFormatters();
return services;
}
// ...
}
结尾
这篇文章就到这里。我们把 MVC 应用程序迁移到了 Web API——虽然 Web API 本身也是基于 MVC 构建的,但你应该明白我的意思了!
在以后的剧集中,我们将在此 API 的基础上进行构建,虽然它目前还很简单,但已经很好地满足了我们的需求,我们也得以探索许多 ASP.NET Core 构建模块和功能。
不过在下一集中,我们将暂时放下 ASP.NET Core,开始使用 Vue.js 创建一个单页应用程序,这将是我们的 PlayBall 项目前端,并将使用此 API 来实现其功能。
帖子中的链接:
本文的源代码在这里。
如果您想查看从先前代码到本集更改的变化,不要忘记您可以查看 GitHub 中的提交和拉取请求,例如,您可以在这里看到本集的 PR 。
欢迎随时提问,也请不吝提供反馈意见。
谢谢光临,cyaz!
文章来源:https://dev.to/joaofbantunes/episode-012---move-to-a-web-api---aspnet-core-from-0-to-overkill-50fg