本文共 4555 字,大约阅读时间需要 15 分钟。
当两个用户获取同一个资源后,再同时修改该资源,就会导致并发问题
常见实现并发的方法有以下两种:
由于 HTTP 无状态,对于 RESTful API 应用程序来说,只能使用开放式并发控制,可以使用上一节提到的 ETag 来实现
接下来为图书资源更新与部分更新实现并发控制
对于 PUT 或 PATCH 请求,必须检查客户端的请求消息头是否包含 If-Match 消息头,可以通过过滤器判断
namespace Library.API.Filters{ public class CheckIfMatchHeaderFilterAttribute : ActionFilterAttribute { public override Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next) { if (!context.HttpContext.Request.Headers.ContainsKey(HeaderNames.IfMatch)) { context.Result = new BadRequestObjectResult(new ApiError { Message = "必须提供 If-Match 消息头" }); } return base.OnActionExecutionAsync(context, next); } }}
接着在 BookController 的 UpdateBookAsync 和 PartiallyUpdateBookAsync 两个方法应用该特性
[HttpPut("{bookId}")][CheckIfMatchHeaderFilter]public async TaskUpdateBookAsync(Guid authorId, Guid bookId, BookForUpdateDto updateBook){ var book = await RepositoryWrapper.Book.GetBookAsync(authorId, bookId); if (book == null) { return NotFound(); } // 资源已被修改,返回412 var entityHash = HashFactory.GetHash(book); if (Request.Headers.TryGetValue(HeaderNames.IfMatch, out var requestETag) && requestETag != entityHash) { return StatusCode(StatusCodes.Status412PreconditionFailed); } Mapper.Map(updateBook, book, typeof(BookForUpdateDto), typeof(Book)); RepositoryWrapper.Book.Update(book); if (!await RepositoryWrapper.Book.SaveAsync()) { throw new Exception("更新资源 Book 失败"); } // 资源未被修改,更新散列值 var entityNewHash = HashFactory.GetHash(book); Response.Headers[HeaderNames.ETag] = entityNewHash; return NoContent();}
PartiallyUpdateBookAsync 逻辑同上
指定版本的方法有两种:
ASP.NET Core MVC 默认的方式是使用查询字符串,参数名为 api-version
添加nuget
Install-Package Microsoft.AspNetCore.Mvc.Versioning
然后添加 API 版本服务,在 ConfigureServices 中
services.AddApiVersioning(options =>{ // 指明当客户端未提供版本时是否使用默认版本,默认为false options.AssumeDefaultVersionWhenUnspecified = true; // 指明了默认版本 options.DefaultApiVersion = new ApiVersion(1, 0); // 指明是否在HTTP响应消息头中包含api-supported-versions和api-deprecated-versions这两项 options.ReportApiVersions = true;});
接下来,添加一个 PersonController
using Microsoft.AspNetCore.Mvc;namespace Library.API.Controllers.V1{ [Microsoft.AspNetCore.Components.Route("api/person")] [ApiVersion("1.0")] public class PersonController : ControllerBase { [HttpGet] public ActionResultGet() => "Result from v1"; }}namespace Library.API.Controllers.V2{ [Microsoft.AspNetCore.Components.Route("api/person")] [ApiVersion("2.0")] public class PersonController : ControllerBase { [HttpGet] public ActionResult Get() => "Result from v2"; }}
运行程序,访问
结果返回 Result from v1,因为默认版本1.0
访问
结果返回 Result from v2
参数名 api-version 可改为自定义参数名,通过 ApiVersionReader 设置
options.ApiVersionReader = new QueryStringApiVersionReader("ver");
使用 URL 路径形式来访问指定版本 API,需要为 Controller 修改路由
[Route("api/v{version:apiVersion}/students")]
运行程序,访问
即可访问相应的版本接口
使用自定义 HTTP 消息头访问指定 API
options.ApiVersionReader = new HeaderApiVersionReader("api-version");
在消息头添加 api-version 项
还可以通过媒体类型来获取 API
options.ApiVersionReader = new MediaTypeApiVersionReader();
在请求消息头添加 Content-Type,它的值为 application/json;v=2
如果要同时支持多种方式,则可以使用 Combine
options.ApiVersionReader = ApiVersionReader.Combine( new MediaTypeApiVersionReader(), new QueryStringApiVersionReader("api-version"));
除了 Controller 级别的版本外,还可以创建 Action 级别的版本
namespace Library.API.Controllers{ [Route("api/news")] [ApiVersion("1.0")] [ApiVersion("2.0")] public class NewController : ControllerBase { [HttpGet] public ActionResultGet() => "Result from v1"; [HttpGet, MapToApiVersion("2.0")] public ActionResult GetV2() => "Result from v2"; }}
先前的版本不需要时,可以将 Deprecated 属性设置为 true
[ApiVersion("1.0", Deprecated = true)]
除了特性外,ASP.NET Core MVC 还支持使用约定的方式来指定
options.Conventions.Controller() .HasApiVersion(new ApiVersion(1,0));
相比特性,这种方式的优点是能够集中地管理应用程序所有 API 的版本信息,还可以灵活、动态地为 API 配置版本
在程序中获取客户端请求版本信息
protected ApiVersion RequestApiVersion => HttpContext.GetRequestedApiVersion();
本作品采用进行许可。
欢迎转载、使用、重新发布,但务必保留文章署名 郑子铭 (包含链接: ),不得用于商业目的,基于本文修改后的作品务必以相同的许可发布。
如有任何疑问,请与我联系 (MingsonZheng@outlook.com) 。