在目前的软件开发的潮流中,不论是先后端分离仍是服务化改造,后端更多的是经过构建 API 接口服务从而为 web、app、desktop 等各类客户端提供业务支持,如何构建一个符合规范、容易理解的 API 接口是咱们后端开发人员须要考虑的。在本篇文章中,我将列举一些我在使用 ASP.NET Core Web API 构建接口服务时使用到的一些小技巧,因才疏学浅,可能会存在不对的地方,欢迎指出。css
代码仓储:https://github.com/Lanesra712...html
由于本篇文章中涉及到的一些知识点在以前的文章中也已经有具体的解释了,因此这里只会说明如何在 ASP.NET Core Web API 中如何去使用,不会作过多的详细介绍。若是你须要详细了解的话,能够跳转到文章中给出的外链地址去查看。前端
本篇文章中使用的代码是基于 .NET Core 2.2 + .NET Standard 2.0 进行构建的,若是你采用的版本与我使用的不一样,可能最终实现起来的代码会有所不一样,请提早知悉。同时,本篇文章中全部示例代码都会存在于前言中所列出的 github repo 中,我会尝试将每一个功能点的开发做为一次 commit,而且也会在后续进行不按期的更新完善,最终搭建一个基于领域驱动思想的后端项目模板,若是对你有帮助的话,欢迎持续关注。nginx
在我以前的一篇文章中(构建可读性更高的 ASP.NET Core 路由)有提到过,由于 .NET 默认采用 Pascal 的类命名方式,若是采用默认生成的路由,最终构建出的路由地址会存在大小写混在一块儿的状况,虽然在 .NET Core 中大小写的路由地址最终都会对于到正确的资源上,可是为了更好的符合前端的规范,因此这里咱们首先按照以前的文章中所列出的方法去修改默认生成的路由地址格式。git
由于这里咱们最终想要实现的是符合 Restful 风格的 API 接口,因此这里咱们首先须要将默认生成的 URL 地址改成全小写模式。github
public void ConfigureServices(IServiceCollection services) { // 采用小写的 URL 路由模式 services.AddRouting(options => { options.LowercaseUrls = true; }); }
若是你有看过构建可读性更高的 ASP.NET Core 路由这篇文章,你会发现其实咱们最终实现的是 hyphen(-) 格式的 Url 地址,那么这里咱们为何不进行后续的修改了呢?web
若是你有查看 .NET Core 默认模板中生成的 API Controller,仔细看下,这里实际上是使用的特性路由,因此这里咱们并不能经过 Startup.UseMvc 定义的传统路由模板,或是直接在 Startup.Configure 中的 UseMvcWithDefaultRoute 方法去修改咱们的生成的路由地址格式。json
[Route("api/[controller]")] [ApiController] public class ValuesController : ControllerBase { }
不论是后端接口的服务化改造,仍是只是单纯的先后端分离项目开发,咱们的前端项目与后端接口一般不会部署在一块儿,因此咱们须要解决前端访问接口时会涉及到的跨域访问的问题。后端
针对跨域请求,咱们能够采用 jsonp、或者是经过给 nginx 服务器配置响应的 header 参数头信息、或者是使用 CORS,又或是其它的解决方案。你能够自由选择,这里我采用在后端接口中直接配置对于 CORS 的支持。api
在 .NET Core 中,已经在 Microsoft.AspNetCore.Cors 这个类库中添加了对于 CORS 的支持,由于这个类库是存在于咱们已经安装的 .NET Core SDK 中,因此这里咱们并不须要经过 Nuget 进行安装,能够直接使用。
在 .NET Core 中配置 CORS 规则,咱们能够经过在 Startup.ConfigureServices 这个方法中添加不一样的受权策略,以后再针对某个 Controller 或是 Action 经过添加 EnableCors 这个 Attribute 的方式进行配置,这里若是指定了 policy 策略名称,则会使用指定的策略,若是没有指定,则适用于系统的默认配置。一样的,咱们也能够只设置一个策略,直接针对整个项目进行配置,这里我采用对整个项目采用通用的跨域请求配置方案。
在配置 CORS 策略时,咱们能够设置只容许来源于某些 URL 地址的请求能够访问,或者是指定接口只容许某些 HTTP 方法进行访问,或者是在请求的 header 中必须包含某些信息才能够访问咱们的接口。
在下面的代码中,我定义了针对整个项目的跨域请求策略,这里我只是设置了对于接口请求方 URL 地址的控制,经过读取配置文件中的数据,从而达到只容许某些 IP 能够访问的咱们接口的目的。
public class Startup { // 默认的跨域请求策略名称 private const string _defaultCorsPolicyName = "Ingos.Api.Cors"; // This method gets called by the runtime. Use this method to add services to the container. public void ConfigureServices(IServiceCollection services) { services.AddMvc( // 添加 CORS 受权过滤器 options => options.Filters.Add(new CorsAuthorizationFilterFactory(_defaultCorsPolicyName)) ).SetCompatibilityVersion(CompatibilityVersion.Version_2_2); // 配置 CORS 受权策略 services.AddCors(options => options.AddPolicy(_defaultCorsPolicyName, builder => builder.WithOrigins( Configuration["Application:CorsOrigins"] .Split(",", StringSplitOptions.RemoveEmptyEntries).ToArray() ) .AllowAnyHeader() .AllowAnyMethod() .AllowCredentials())); } // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. public void Configure(IApplicationBuilder app, IHostingEnvironment env) { // 容许跨域请求访问 app.UseCors(_defaultCorsPolicyName); } }
例如在下面的设置中,我只容许这一个地址能够访问咱们的接口,若是须要指定多个的话,则能够经过英文的 , 进行分隔。
"Application": {
"CorsOrigins": "http://127.0.0.1:5050"
}
某些状况下,若是咱们不想进行限制的话,只须要将值改成 * 便可。
"Application": {
"CorsOrigins": "*"
}
在一些涉及到接口功能升级的场景下,当咱们须要修改接口逻辑而旧版本的接口没法停用的状况时,为了减小对于原有接口的影响,咱们能够采起为接口添加版本信息的形式,从而下降因采用不一样版本而形成的影响。若是你想要详细了解的话,能够查看这篇文章,电梯直达 =》ASP.NET Core 实战:构建带有版本控制的 API 接口。
在实现具备版本控制的接口前,首先咱们须要经过 Nuget 添加下面的两个 dll,由于我是在 Ingos.Api.Core 这个类库中进行配置的,因此我安装到了这个类库下,你须要根据你本身的状况选择最终是安装到 Api 接口项目中仍是在别的类库下。
Install-Package Microsoft.AspNetCore.Mvc.Versioning
Install-Package Microsoft.AspNetCore.Mvc.Versioning.ApiExplorer
在安装完成以后,咱们就能够在 Startup.ConfigureServices 方法中,为项目中的接口配置版本信息,这里我采用的方案是将版本号添加到接口的 URL 地址中。
由于对于全部中间件的配置都会在 Startup.ConfigureServices 方法中,为了保持该方法的纯净性,这里我写了一个扩展方法用于配置咱们的 api 的版本,以后直接调用便可。
public static class ApiVersionExtension { /// <summary> /// 添加 API 版本控制扩展方法 /// </summary> /// <param name="services">生命周期中注入的服务集合 <see cref="IServiceCollection"/></param> public static void AddApiVersion(this IServiceCollection services) { // 添加 API 版本支持 services.AddApiVersioning(o => { // 是否在响应的 header 信息中返回 API 版本信息 o.ReportApiVersions = true; // 默认的 API 版本 o.DefaultApiVersion = new ApiVersion(1, 0); // 未指定 API 版本时,设置 API 版本为默认的版本 o.AssumeDefaultVersionWhenUnspecified = true; }); // 配置 API 版本信息 services.AddVersionedApiExplorer(option => { // api 版本分组名称 option.GroupNameFormat = "'v'VVVV"; // 未指定 API 版本时,设置 API 版本为默认的版本 option.AssumeDefaultVersionWhenUnspecified = true; }); } }
扩展方法最终实现方式如上面的代码所示,以后咱们就能够直接在 ConfigureServices 方法中直接进行调用这个扩展方法就能够了。
// This method gets called by the runtime. Use this method to add services to the container. public void ConfigureServices(IServiceCollection services) { // Config api version services.AddApiVersion(); }
如今咱们删除项目建立时默认生成的 ValuesController,在 Controllers 目录下创建一个 v1 文件夹,表明此文件夹下都是 v1 版本的控制器。添加一个 UsersController 用来获取系统的用户资源,如今项目的文件结构以下图所示。
如今咱们来改造咱们的 UsersController,咱们只须要在 Controller 或是 Action 上添加 ApiVersion 特性就能够指定当前 Controller/Action 的版本信息。同时,由于我须要将 API 的版本信息添加到生成的 URL 地址中,因此这里咱们须要修改特性路由的模板,将咱们的版本以占位符的形式添加到生成的路由 URL 地址中,修改完成后的代码及实现的效果以下所示。
[ApiVersion("1.0")] [ApiController] [Route("api/v{version:apiVersion}/[controller]")] public class UsersController : ControllerBase { }
在先后端分离开发的状况下,咱们须要提供给前端开发人员一个接口文档,从而让前端开发人员知道以什么样的 HTTP 方法或是传递什么样的参数给后端接口,从而获取到正确的数据,而 Swagger 则提供了一种自动生成接口文档的方式,同时也提供相似于 Postman 的功能,能够实现对于接口的实时调用测试。
首先,咱们须要经过 Nuget 添加 Swashbuckle.AspNetCore 这个 dll 文件,以后咱们就能够在此基础上实现对于 Swagger 的配置。
Install-Package Swashbuckle.AspNetCore
与上面配置 API 接口的版本信息类似,这里我依旧采用构建扩展方法的方式来实现对于 Swagger 中间件的配置。具体的配置过程能够查看我以前写的文章(ASP.NET Core 实战:构建带有版本控制的 API 接口),这里只列出最终配置完成的代码。
public static void AddSwagger(this IServiceCollection services) { // 配置 Swagger 文档信息 services.AddSwaggerGen(s => { // 根据 API 版本信息生成 API 文档 // var provider = services.BuildServiceProvider().GetRequiredService<IApiVersionDescriptionProvider>(); foreach (var description in provider.ApiVersionDescriptions) { s.SwaggerDoc(description.GroupName, new Info { Contact = new Contact { Name = "Danvic Wang", Email = "danvic96@hotmail.com", Url = "https://yuiter.com" }, Description = "Ingos.API 接口文档", Title = "Ingos.API", Version = description.ApiVersion.ToString() }); } // 在 Swagger 文档显示的 API 地址中将版本信息参数替换为实际的版本号 s.DocInclusionPredicate((version, apiDescription) => { if (!version.Equals(apiDescription.GroupName)) return false; var values = apiDescription.RelativePath .Split('/') .Select(v => v.Replace("v{version}", apiDescription.GroupName)); apiDescription.RelativePath = string.Join("/", values); return true; }); // 参数使用驼峰命名方式 s.DescribeAllParametersInCamelCase(); // 取消 API 文档须要输入版本信息 s.OperationFilter<RemoveVersionFromParameter>(); // 获取接口文档描述信息 var basePath = Path.GetDirectoryName(AppContext.BaseDirectory); var apiPath = Path.Combine(basePath, "Ingos.Api.xml"); s.IncludeXmlComments(apiPath, true); }); }
当咱们配置完成后就能够在 Startup 类中去启用 Swagger 文档。
public void ConfigureServices(IServiceCollection services) { // 添加对于 swagger 文档的支持 services.AddSwagger(); } public void Configure(IApplicationBuilder app, IHostingEnvironment env, IApiVersionDescriptionProvider provider) { // 启用 Swagger 文档 app.UseSwagger(); app.UseSwaggerUI(s => { // 默认加载最新版本的 API 文档 foreach (var description in provider.ApiVersionDescriptions.Reverse()) { s.SwaggerEndpoint($"/swagger/{description.GroupName}/swagger.json", $"Sample API {description.GroupName.ToUpperInvariant()}"); } }); }
由于咱们在以前设置构建的 API 路由时包含了版本信息,因此在最终生成的 Swagger 文档中进行测试时,咱们都须要在参数列表中添加 API 版本这个参数。这无疑是有些不方便,因此这里咱们能够经过继承 IOperationFilter 接口,控制在生成 API 文档时移除 API 版本参数,接口的实现方法以下所示。
public class RemoveVersionFromParameter : IOperationFilter { public void Apply(Operation operation, OperationFilterContext context) { var versionParameter = operation.Parameters.Single(p => p.Name == "version"); operation.Parameters.Remove(versionParameter); } }
当咱们实现自定义的接口后就能够在以前针对 Swagger 的扩展方法中调用这个过滤方法,从而实现移除版本信息的目的,扩展方法中的添加位置以下所示。
public static void AddSwagger(this IServiceCollection services) { // 配置 Swagger 文档信息 services.AddSwaggerGen(s => { // 取消 API 文档须要输入版本信息 s.OperationFilter<RemoveVersionFromParameter>(); }); }
最终的实现效果以下图所示,能够看到,参数列表中已经没有版本信息这个参数,可是咱们在进行接口测试时会自动帮咱们添加上版本参数信息。
这里须要注意,由于咱们须要在最终生成的 Swagger 文档中显示出咱们对于 Controller 或是 Action 添加的注释信息,因此这里咱们须要在 Web Api 项目的属性选项中勾选上输出 XML 文档文件。同时若是你不想 VS 一直提示你有方法没有添加参数信息,这里咱们能够在取消显示警告这里添加上 1591 这个参数。
在没有采用 Restful 风格来构建接口返回值时,咱们可能会习惯于在接口返回的信息中添加一个接口是否请求成功的标识,就像下面代码中示例的这种返回形式。
{ sueecss: true msg: '', data: [{ id: '20190720214402', name: 'zhangsan' }] }
可是,当咱们想要构建符合 Restful 风格的接口时,咱们就不能再这样进行设计了,咱们应该经过返回的 HTTP 响应状态码来标识此次访问是否成功。一些比较经常使用的 HTTP 状态码以下表所示。
HTTP 状态码 | 涵义 | 解释说明 |
---|---|---|
200 | OK | 用于通常性的成功返回,不可用于请求错误返回 |
201 | Created | 资源被建立 |
202 | Accepted | 用于资源异步处理的返回,仅表示请求已经收到。对于耗时比较久的处理,通常用异步处理来完成 |
204 | No Content | 此状态可能会出如今 PUT、POST、DELETE 的请求中,通常表示资源存在,但消息体中不会返回任何资源相关的状态或信息 |
400 | Bad Request | 用于客户端通常性错误信息返回, 在其它 4xx 错误之外的错误,也可使用,错误信息通常置于 body 中 |
401 | Unauthorized | 接口须要受权访问,为经过受权验证 |
403 | Forbidden | 当前的资源被禁止访问 |
404 | Not Found | 找不到对应的信息 |
500 | Internal Server Error | 服务器内部错误 |
咱们知道 HTTP 共有四个谓词方法,分别为 Get、Post、Put 和 Delete,在以前咱们可能更多的是使用 Get 和 Post,对于 Put 和 Delete 方法可能并不会使用。一样的,若是咱们须要建立符合 Restful 风格的接口,咱们则须要根据这四个 HTTP 方法谓词一些约定俗成的功能定义去定义对应接口的 HTTP 方法。
HTTP 谓词方法 | 解释说明 |
---|---|
GET | 获取资源信息 |
POST | 提交新的资源信息 |
PUT | 更新已有的资源信息 |
DELETE | 删除资源 |
例如,对于一个获取全部资源的方法,咱们可能会定义接口的默认返回 HTTP 状态码为 200 或是 400,当状态码为 200 时,表明数据获取成功,接口能够正常返回数据,当状态码为 400 时,则表明接口访问出现问题,此时则返回错误信息对象。
在 ASP.NET Core Web API 中,咱们能够经过在 Action 上添加 ProducesResponseType 特性来定义接口的返回状态码。经过 F12 按键咱们能够进入 ProducesResponseType 这个特性,能够看到这个特性存在两个构造方法,咱们能够只定义接口返回 HTTP 状态码或者是在定义接口返回的状态码时同时返回的具体对象信息。
上面给出的接口案例的示例代码以下所示,从下图中能够看到,Swagger 会自动根据咱们的 ProducesResponseType 特性来列出咱们接口可能返回的 HTTP 状态码和对象信息。这里由于是示例程序,UserListDto 并无定义具体的属性信息,因此这里显示的是一个不包含任何属性的对象数组。
/// <summary> /// 获取所有的用户信息 /// </summary> /// <returns></returns> [HttpGet] [ProducesResponseType(typeof(IEnumerable<UserListDto>), StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status400BadRequest)] public IActionResult Get() { // 一、获取资源数据 // 二、判断数据获取是否成功 if (true) return Ok(new List<UserListDto>()); else return BadRequest(new { statusCode = StatusCodes.Status400BadRequest, description = "错误描述", msg = "错误信息" }); }
可能这里你可能会有疑问,当接口返回的 HTTP 状态码为 400 时,返回的信息是什么鬼,与咱们定义的错误信息对象字段不一样啊?原来,在 ASP.NET Core 2.1 以后的版本中,对于 API 接口返回 400 的 HTPP 状态码会默认返回 ProblemDetails 对象,由于这里咱们并无将接口中的返回 BadRequest 中的错误信息对象做为 ProducesResponseType 特性的构造函数的参数,因此这里就采用了默认的错误信息对象。
固然,当接口的 HTTP 返回状态码为 400 时,最终仍是会返回咱们自定义的错误信息对象,因此这里为了避免形成先后端对接上的歧义,咱们最好将返回的对象信息也做为参数添加到 ProducesResponseType 特性中。
同时,除了上面示例的接口中经过返回 OK 方法和 BadRequest 方法来代表接口的返回 HTTP 状态码,在 ASP.NET Core Web API 中还有下列继承于 ObjectResult 的方法来代表接口返回的状态码,对应信息以下。
HTTP 状态码 | 方法名称 |
---|---|
200 | OK() |
201 | Created() |
202 | Accepted() |
204 | NoContent() |
400 | BadRequest() |
401 | Unauthorized() |
403 | Forbid() |
404 | NotFound() |
在上面的示例中,由于咱们须要指定接口须要返回的 HTTP 状态码,因此咱们须要提早添加好 ProducesResponseType 特性,在某些时候咱们可能在代码中添加了一种 HTTP 状态码的返回结果,但是却忘了添加特性描述,那么有没有一种便捷的方式提示咱们呢?
在 ASP.NET Core 2.2 及之后更新的 ASP.NET Core 版本中,咱们能够经过 Nuget 去添加 Microsoft.AspNetCore.Mvc.Api.Analyze 这个包,从而实现对咱们的 API 进行分析,首先咱们须要将这个包添加到咱们的 API 项目中。
Install-Package Microsoft.AspNetCore.Mvc.Api.Analyzers
例如在下面的接口代码中,咱们根据用户的惟一标识去寻找用户数据,当获取不到数据的时候,返回的 HTTP 状态码为 400,而咱们只添加了 HTTP 状态码为 200 的特性说明。此时,分析器将 HTTP 404 状态代码的缺失特性说明作为一个警告,并提供了修复此问题的选项,咱们进行修复后就能够自动添加特性。
/// <summary> /// 获取用户详细信息 /// </summary> /// <param name="id">用户惟一标识</param> /// <returns></returns> [HttpGet("{id}")] [ProducesResponseType(typeof(UserEditDto), StatusCodes.Status200OK)] public IActionResult Get(string id) { // 一、根据 Id 获取用户信息 UserEditDto user = null; if (user == null) return NotFound(); else return Ok(user); }
可是,在自动完成文档补全后其实仍是须要咱们进行一些操做的,例如,若是咱们须要指定返回值的 Type 类型,仍是须要咱们本身手动添加到 ProducesResponseType 特性上的。
在进行特性补齐的时候,分析器也帮咱们填加了一个 ProducesDefaultResponseType 特性。经过在微软的文档中指向的 Swagger 文档(Swagger Default Response)中能够了解到,若是咱们接口不论是什么状态,最终返回的 response 响应结构都是相同的,咱们就能够直接使用 ProducesDefaultResponseType 特性来指定 response 的响应结构,而不须要每一个 HTTP 状态都添加一个特性。