
云原生 API 网关 APISIX 入门教程
首先需要明确一点的就是REST Api它不是一个标准,而是一种架构风格
WebApi通常是指“使用HTTP协议并通过网络调用的API”,由于它使用了HTTP协议,所以需要通过URI信息来指定端点。
WebApi就是一个Web系统,通过访问URI可以与其进行信息交互。
而常用的MVC模式是主要用来构建UI的架构模式。
特点:松耦合,关注点分离、MVC不是一个完整的应用程序框架
Model:它赋值处理程序数据的逻辑
View:它是程序里复制展示数据的那部分。构建API的时候,VView就是数据或资源的展示。通常使用JSON格式。
Controller,它复负责View和Model之间的交互。
需要注意的是,在配置服务的时候在core3.0以前可能写的是AddMvc,但是这个服务涉及了View视图以及TagHelper的一些功能,所以在做WebApi的时候用不到
public void ConfigureServices(IServiceCollection services)
{
//services.AddMvc(); core 3.0以前是这样写的,这个服务包括了 TageHelper等 WebApi不需要的东西,所有3.0以后可以不这样写
services.AddControllers();
}
注意配置中间件的区域管道顺序不能随意改动。
管道就是客户端通过一些指令指向服务器端,在这个过程中呢,会经过一些手动配置的中间件,比如说路由中间件、静态资源中间件等,从客户端出发到服务器端,将数据处理后,再由服务器端原路返回到客户端这样的一个过程。但是在请求的过程中也不排除中间件出现短路的情况,这样也就不会进入到第二个中间件了,而是直接返回到客户端。
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
app.UseRouting();
app.UseAuthorization();
app.UseEndpoints(endpoints =>
{
endpoints.MapControllers();
});
}
API对外合约:API消费者需要使用到三个概念
API对外提供统一资源接口,业界对RESTful资源命名也有规则
使用名词而不是动词
要体现资源的结构/关系
通过id获取单个用户应该是:api/user/{userId},而不是 api/user/users。这样写就是让API具有很好的可读性和可预测性
需求案例1
系统存在两个资源:Company(公司)、Employee(员工),现在需要获取某个公司下的所有员工
分析:应该使用HTTP GET。API在设计的时候需要体现公司与员工的一个包含关系
常见错误做法:api/employees,api/employee/{companyId} 。这两个URI都没有体现公司和员工的一个包含关系
建议做法:api/companies/{companyId}/employees
需求案例2
需要获取某个公司下的某个员工
常见错误做法:api/employees/{employeeId}
建议做法:api/companies/{companyId}/employees/{employeeId}
自定义查询怎么命名?
需求:获取所有用户信息,并且按年龄从大到小排序
常见错误做法:api/user/orderby/age
建议做法:api/user?orderby=age (通过QueryString查询字符串,多条件使用 & 符号)
HTTP状态码
请求是否成功?如果请求失败了,谁来为此负责
2xx 开头状态码
3xx 开头状态码
用于跳转。例如告诉浏览器搜索引擎,某个页面的网址已经永久改变,绝大多数的WebApi都不需要这类的状态码
4xx 开头:客户端错误
5xx 开头状态码
500 – Internal serever error,表示服务器出现了错误,客户端无能为力,只能以后再试试
还有就是RESTful API 返回的结果不一定Json格式的
先看控制器代码:
using Microsoft.AspNetCore.Mvc;
using Routine.Api.Service;
using System;
using System.Threading.Tasks;
namespace Routine.Api.Controllers
{
[ApiController] //好处:ApiController不是强制的
//1.会启用使用属性路由(Attribute Routing)
//2.自动HTTP 400响应
//3.推断参数的绑定源
//4.Multipart/form-data 请求推断
//5.错误状态代码的问题详细信息
[Route("api/companies")] //写法一
//[Route("api/[controller]")] //写法二:意思是相当于刨除了Controller后缀,获取前面的 Companies C可以是小写,如果你改名了那么你路由的uri也跟着变了(不建议这样写)
public class CompaniesController:ControllerBase
{
private readonly ICompanyRepository _companyRepository;
public CompaniesController(ICompanyRepository companyRepository)
{
_companyRepository = companyRepository ??
throw new ArgumentNullException(nameof(companyRepository));
}
[HttpGet]
//IActionResult定义了一些合约,它可以代表ActionResult返回的结果
public async Task<IActionResult> GetCompanies()
{
var companies =await _companyRepository.GetCompaniesAsync();//读取出来的是List
return Ok(companies);
}
[HttpGet("{companyId}")] // Controller标注了ApiController => uri=> api/companies/{companyId}
public async Task<IActionResult> GetCompany(Guid companyId)
{
//判断该公司是否存在方法一:这种方法在处理并发请求时可能会出现错误,原因是查到之后,进行删除,进入company后也可能是404找不到了
//var exists =await _companyRepository.CompanyExistsAsync(compamyId);
//if (!exists)
//{
// //不存在应该返回404
// return NotFound();
//}
var company = await _companyRepository.GetCompanyAsync(companyId);//读取出来的是List
//方法二
if (company==null)
{
return NotFound();
}
return Ok(company);
}
}
}
为了更好的构建RESTful API 对于 uri 的设计规则也有很严格的要求。
在控制器标注 ApiController,它会自动启用路由属性
通过 [Route] 设计路由规则
比如:接口一:GetCompanies,请求的方式:GET,通过Route 去设置路由规则 [Router(“api/companies”)],即查询所有公司信息
[Router(“api/companies”)] => api/companies
接口二:GetCompany,请求方式:GET,只不过在 添加了 [HTTPGET(”{companyId}”)] =>api/companies/{companyId},即查询某一公司的信息
关于第二种路由写法请看注释
通过Postman工具测试一下
测试一:接口一
测试二:接口二
以上两个接口测试完毕!!!
对于ASP.NET Core 3.x以前对于 404 NotFound请求状态码输出的格式不太友好,而ASP.NET Core 3.x对于404请求状态码也做了友好的提示。
现在将接口伪造错误信息,提示 404 如图:
关于构建 RESTful API 存在的内容协商
所谓内容协商就是这样一个过程,针对一个响应,当有多种表述格式可用时,选取最佳的一种表述格式,这些表述可以是XML,JSON,甚至是自定义的格式规则
Accept Header:负责指定输出类型
Media Type(媒体类型)
404 Not Acceptable
输出格式:ASP.NET Core 里面对应的就是 Output Formatters 我们称为:输出类型的格式化器
也就是说如果一个API消费者,设置了Accept Header的媒体类型为Json,那么这个RESTful API也应该返回的是JSON,
但是呢如果服务器只接收XML的格式,这个时候请求的媒体类型不被服务器所接受,那么就会返回 406 这个状态码
总而言之,尽量避免不写Accept Header,避免客户端和服务器端接收和返回的类型不一致导致错误。
有输出那么就会有输入了!!!
Content-Type-Header:负责指定输入
Media Type(媒体类型)
输出格式:ASP.NET Core里面对应的就是 Input Formatters
比如说:对于一个客户端的POST请求,即添加资源信息,那么就需要输入参数,这些参数可能是放在Body里面,那么在Body里面的这些参数可能是对象的那种格式。
那么我们就需要通过 Content-Type-Header来确定Body里面的参数是什么样的类型,可能是Json也可能是Xml或者是自定义的格式,指明之后,RESTful API才能更好的对这些参数进行处理。
看Startup类代码:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Formatters;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using Routine.Api.Data;
using Routine.Api.Service;
namespace Routine.Api
{
public class Startup
{
public Startup(IConfiguration configuration)
{
Configuration = configuration;
}
public IConfiguration Configuration { get; }
public void ConfigureServices(IServiceCollection services)
{
//services.AddMvc(); core 3.0以前是这样写的,这个服务包括了TageHelper等 WebApi不需要的东西,所有3.0以后可以不这样写
services.AddControllers(setup =>
{
//setup.ReturnHttpNotAcceptable=false;//如果客户端默认为xml格式,服务器端为json,false就不会返回406
setup.ReturnHttpNotAcceptable = true;//如果请求的类型和服务器请求的类型不一致就返回406
//setup.OutputFormatters.Add(new XmlDataContractSerializerOutputFormatter());
//setup.OutputFormatters.Insert(0, new XmlDataContractSerializerOutputFormatter());
}).AddXmlDataContractSerializerFormatters();
//配置接口服务:涉及到这个服务注册的生命周期这里采用AddScoped,表示每次的Http请求
services.AddScoped<ICompanyRepository, CompanyRepository>();
//获取配置文件中的数据库字符串连接
var sqlConnection = Configuration.GetConnectionString("SqlServerConnection");
//配置上下文类DbContext,因为它本身也是一套服务
services.AddDbContext<RoutineDbContext>(options =>
{
options.UseSqlServer(sqlConnection);
});
}
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
app.UseRouting();
app.UseAuthorization();
app.UseEndpoints(endpoints =>
{
endpoints.MapControllers();
});
}
}
}
setup.ReturnHttpNotAcceptable就是处理是在客户端与服务器端数据产生冲突时,是否要即将产生 406 的状态码。
setup.OutputFormatters.Add(new XmlDataContractSerializerOutputFormatter())
分析:实际上OutputFormatters 是一个集合 ,通过Add方法添加服务器允许接受XML格式的数据功能。因为集合中默认只有Json
setup.OutputFormatters.Insert(0,new XmlDataContractSerializerOutputFormatter())
分析:实际上刚刚写的是一种方法。Insert就是指明格式顺序,默认是JSON,通过Insert设置 0 ,就是指明XML为默认接受的数据格式
实际上以上两种写法都是 ASP.NET Core 3.x以前的写法。
ASP.NET Core 3.x的实际写法:就是在AddControllers后面添加XmlDataContractSerializerOutputFormatter方法。这样不管是输入输出都已经设置好了XML的格式数据
postman接口测试:取消setup.OutputFormatters.Insert(0,new XmlDataContractSerializerOutputFormatter())的注释
默认xml:
最后,关于构建RESTFUL Api的URI规则及原理
关于Entity Model vs 面向外部的Model
Entity Framework Core 使用 Entity Model 用来表示数据库里面的记录。
面向外部的Model 则表示要传输的东西,有时候被称为 Dto,有时候被称为 ViewModel。
关于Dto,API消费者通过Dto,仅提供给用户需要的数据起到隔离的作用,防止API消费者直接接触到核心的Entity Model。
可能你会觉得有点多余,但是仔细想想你会发现,Dto的存在是很有必要的。
Entity Model 与数据库实际上应该是有种依赖的关系,数据库某一项功能发生改变,Entity Model也应该会做出相应的动作,那么这个时候 API消费者在请求服务器接口数据时,如果直接接触到了 Entity Model数据,那么它也就无法预测到底是哪一项功能做出了改变。这个时候可能在做 API 请求的时候发生不可预估的错误。Dto的存在一定程度上解决了这一问题。
编写Company的 Dto:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace Routine.Api.Models
{
public class CompanyDto
{
public Guid Id { get; set; }
public string Name { get; set; }
}
}
对比Company的 Entity Model:
using System;
using System.Collections.Generic;
namespace Routine.Api.Entities
{
/// <summary>
/// 公司
/// </summary>
public class Company
{
public Guid Id { get; set; }
public string Name { get; set; }
public string Introduction { get; set; }
public ICollection<Employee> Employees { get; set; }
}
}
Id和Name属性是一致的,对于 Employees集合 以及 Introduction 字符串为了区分,这里不提供给 Dto
这里就涉及到了如何从 Entity Model 的数据转化到 Dto
分析:我们给API消费者提供的数据肯定是一个集合,那么可以先将Company的Dto定义为一个List集合,再通过循环 Entity Model 的数据,将数据添加到集合并且赋值给 Dto 对应的属性。
控制器代码:
[HttpGet]
//IActionResult定义了一些合约,它可以代表ActionResult返回的结果
public async Task<ActionResult<IEnumerable<CompanyDto>>> GetCompanies()
{
var companies =await _companyRepository.GetCompaniesAsync();//读取出来的是List
var companyDtos = new List<CompanyDto>();
foreach (var company in companies)
{
companyDtos.Add(new CompanyDto
{
Id = company.Id,
Name = company.Name
});
};
return Ok(companyDtos);
}
}
这里你可能注意到了 返回的是 ActionResult<T>
关于 ActionResult<T>,好处就是让 API 消费者意识到此接口的返回类型,就是将接口的返回类型进一步的明确,可以方便调用,让代码的可读性也更高。
你可以返回IEnumerable类型,也可以直接返回List,当然这两者并没有什么区别,因为List也实现了 IEnumerable 这个接口!
那么这样做会面临又一个问题。如果 Dto 需要的数据又20甚至50条往上,那么这样写会显得非常的笨拙而且也很容易出错。
如何处理呢?dotnet生态给我们提供了一个很好的对象属性映射器 AutoMapper!!!
关于 AutoMapper,官方解释:基于约定的对象属性映射器。
它还存在一个作用,在处理映射关系时出现如果出现空引用异常,就是映射的目标类型出现了与源类型不匹配的属性字段,那么就会自动忽略这一异常。
打开 nuget 工具包,搜索 AutoMapper ,下载第二个!!!原因是这个更好的实现依赖注入,可以看到它也依赖于 AutoMapper,相当于把第一个也一并下载了。
第一步进入 Startup类 注册AutoMapper服务!
public void ConfigureServices(IServiceCollection services)
{
//services.AddMvc(); core 3.0以前是这样写的,这个服务包括了TageHelper等 WebApi不需要的东西,所有3.0以后可以不这样写
services.AddControllers();
//注册AutoMapper服务services.AddAutoMapper(AppDomain.CurrentDomain.GetAssemblies());
//配置接口服务:涉及到这个服务注册的生命周期这里采用AddScoped,表示每次的Http请求
services.AddScoped<ICompanyRepository, CompanyRepository>();
//获取配置文件中的数据库字符串连接
var sqlConnection = Configuration.GetConnectionString("SqlServerConnection");
//配置上下文类DbContext,因为它本身也是一套服务
services.AddDbContext<RoutineDbContext>(options =>
{
options.UseSqlServer(sqlConnection);
});
}
关于 AddAutoMapper() 方法,实际上它需要返回一个 程序集数组,就是AutoMapper的运行配置文件,那么通过 GetAssemblies 去扫描AutoMapper下的所有配置文件即可。
第二步:建立处理 AutoMapper 映射类
using AutoMapper;
using Routine.Api.Entities;
using Routine.Api.Models;
namespace Routine.Api.Profiles
{
public class CompanyProfiles:Profile
{
public CompanyProfiles()
{
//添加映射关系,处理源类型与映射目标类型属性名称不一致的问题
//参数一:源类型,参数二:目标映射类型
CreateMap<Company, CompanyDto>()
.ForMember(target=>target.CompanyName,
opt=> opt.MapFrom(src=>src.Name));
}
}
}
分析:通过CreateMap,对于参数一:源类型,参数二:目标映射类型。
关于 ForMember方法的作用,有时候你得考虑一个情况,前面已经说过,AutoMapper 是基于约定的对象到对象(Object-Object)的属性映射器,如果所映射的属性字段不一致一定是无法映射成功的!
约定即属性字段与源类型属性名称须一致!!!但是你也可以处理这一情况的发生,通过lambda表达式,将目标映射类型和源类型关系重映射即可。
第三步:开始数据映射
先来看映射前的代码:通过集合循环赋值:
[HttpGet]
//IActionResult定义了一些合约,它可以代表ActionResult返回的结果
public async Task<ActionResult<IEnumerable<CompanyDto>>> GetCompanies()
{
var companies =await _companyRepository.GetCompaniesAsync();//读取出来的是List
var companyDtos = new List<CompanyDto>();
foreach (var company in companies)
{
companyDtos.Add(new CompanyDto
{
Id = company.Id,
Name = company.Name
});
}
return Ok(companyDtos);
}
通过 AutoMapper映射:
[HttpGet]
//IActionResult定义了一些合约,它可以代表ActionResult返回的结果
public async Task<ActionResult<IEnumerable<CompanyDto>>> GetCompanies()
{
var companies =await _companyRepository.GetCompaniesAsync();//读取出来的是List
var companyDtos = _mapper.Map<IEnumerable<CompanyDto>>(companies);
return Ok(companyDtos);
}
分析:Map()方法处理需要返回的目标映射类型,然后带入源类型。
关于获取父子关系的资源:
所谓 父:Conmpany(公司)、子:Employees(员工)
可能你注意到了基本上就是主从表的引用关系
那么我们在设计AP uri 的时候也需要考虑到这一点
需求案例 1:查询某一公司下的所有员工信息
分析:设计到员工信息,也需要需要实现 Entity Model 对 EmployeeDtos 的转换,所以需要建立 EmployeeDto
对比 Employee 的 Entity Model和EmployeeDto
Entity Model 代码:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace Routine.Api.Entities
{
/// <summary>
/// 员工
/// </summary>
public class Employee
{
public Guid Id { get; set; }
//公司外键
public Guid CompanyId { get; set; }
//公司表导航属性
public Company Company { get; set; }
public string EmployeeNo { get; set; }
public string FirstName { get; set; }
public string LastName { get; set; }
//性别枚举
public Gender Gender { get; set; }
public DateTime DateOfBirth { get; set; }
}
}
EmployeeDto 代码:
分析:对性别 Gender 枚举类型做了处理,改成了string类型,方便调用。另外对于姓名 Name 也是将 FirstName 和 LastName合并,年龄 Age 改成了 int类型
那么,这些改动我们都需要在 EmployeeProfile类中在映射时进行标注,不然由于对象属性映射器的约定,无法进行映射!!!
using System;
namespace Routine.Api.Models
{
public class EmployeeDto
{
public Guid Id { get; set; }
public Guid CompanyId { get; set; }
public string EmployeeNo { get; set; }
public string Name { get; set; }
public string GenderDispaly { get; set; }
public int Age { get; set; }
}
}
EmployeeProfile类代码:
逻辑和 CompanyProfile类的映射是一样的
using AutoMapper;
using Routine.Api.Entities;
using Routine.Api.Models;
using System;
namespace Routine.Api.Profiles
{
public class EmployeeProfile:Profile
{
public EmployeeProfile()
{
CreateMap<Employee, EmployeeDto>()
.ForMember(target => target.Name,
opt => opt.MapFrom(src => $"{src.FirstName} {src.LastName}"))
.ForMember(target=>target.GenderDispaly,opt=>opt.MapFrom(src=>src.Gender.ToString()))
.ForMember(target=>target.Age,
opt=>opt.MapFrom(src=>DateTime.Now.Year-src.DateOfBirth.Year));
}
}
}
接下来开始建立 EmployeeController 控制器,来通过映射器实现映射关系
EmployeeController :
需要注意 uir 的设计,我们查询的是某一个公司下的所有员工信息,所以也需要是 Entity Model 对 EmployeeDtos的转换,同样是借助 对象属性映射器。
using AutoMapper;
using Microsoft.AspNetCore.Mvc;
using Routine.Api.Models;
using Routine.Api.Service;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace Routine.Api.Controllers
{
[ApiController]
[Route("api/companies/{companyId}/employees")]
public class EmployeesController:ControllerBase
{
private readonly IMapper _mapper;
private readonly ICompanyRepository _companyRepository;
public EmployeesController(IMapper mapper, ICompanyRepository companyRepository)
{
_mapper = mapper ?? throw new ArgumentNullException(nameof(mapper));
_companyRepository = companyRepository ?? throw new ArgumentNullException(nameof(companyRepository));
}
[HttpGet]
public async Task<ActionResult<IEnumerable<EmployeeDto>>> GetEmployeesForCompany(Guid companyId)
{
if (! await _companyRepository.CompanyExistsAsync(companyId))
{
return NotFound();
}
var employees =await _companyRepository.GetEmployeesAsync(companyId);
var employeeDtos = _mapper.Map<IEnumerable<EmployeeDto>>(employees);
return Ok(employeeDtos);
}
}
}
接口测试(某一公司下的所有员工信息):
需求案例 2:查询某一公司下的某一员工信息
来想想相比需求案例1哪些地方需要进行改动的?
既然是某一个员工,说明 uir 需要加个员工的参数 Id进去。
还有除了判断该公司是否存在,还需要判断该员工是否存在。
另外,既然是某一个员工,所以返回的应该是个对象而非IEnumable集合。
代码:
[HttpGet("{employeeId}")]
public async Task<ActionResult<EmployeeDto>> GetEmployeeForCompany(Guid companyId,Guid employeeId)
{
//判断公司存不存在
if (!await _companyRepository.CompanyExistsAsync(companyId))
{
return NotFound();
}
//判断员工存不存在
var employee = await _companyRepository.GetEmployeeAsync(companyId, employeeId);
if (employee==null)
{
return NotFound();
}
//映射到 Dto
var employeeDto = _mapper.Map<EmployeeDto>(employee);
return Ok(employeeDto);
}
接口测试(某一公司下的某一员工信息):
可以看到测试成功!
关于故障处理:
这里的“故障”主要是指服务器故障或者是抛出异常的故障,ASP.NET Core 对于 服务器故障一般会引发 500 状态码错误,对于这种错误,会导致一种后果就是在出现故障后
故障信息会将程序异常细节显示出来,这就对API消费者不够友好,而且也造成一定的安全隐患。但此后果是在开发环境下产生也就是 Development。
当然ASP.NET Core开发团队也意识到了这种问题!
伪造程序异常:
引发异常后接口测试:
可以看到此异常已经暴露了程序细节给 API 消费者 ,这种做法欠妥。
怎么办呢?试试改一下开发的环境状态!
重新测试接口:
问题解决!
但是你可能想根据这些异常抛出一些自定义的信息给 API 消费者 实际上也可以。
回到 Stratup 类:添加一个中间件 app.UseExceptionHandler即可
分析:意思是如果有未处理的异常发生的时候就会走 else 里面的代码,实际项目中这一块需要记录一下日志
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
else
{
app.UseExceptionHandler(appBulider =>
{
appBulider.Run(async context =>
{
context.Response.StatusCode = 500
await context.Response.WriteAsync("The program Error!");
});
});
}
app.UseRouting();
app.UseAuthorization();
app.UseEndpoints(endpoints =>
{
endpoints.MapControllers();
});
}
再来测试一下接口是否成功返回自定义异常信息:
测试成功!!!
文章转自微信公众号@DotNet