场景:
由一次大的项目改动引起的 App 端 API 不兼容问题, 这时候就需要对 API 做版本控制了, 权衡之后因为用户不多, 选择了强更, 没人想在已经写了 8000 行代码的单个 svc 文件中维护好几个版本的接口或者继续新建 svc(wcf 配置较繁琐), 但暴露出的版本控制问题还是要解决的, 不能每次都强更呀.
API 版本控制方案:
分项目进行版本控制, 一个项目一个版本号, 维护两个版本号, 分开部署, 根据其版本号路由到对应 host.
根据当前项目情况, 为了同时完成技术迭代 (因没有 code review, 多次经手, wcf 中基于 http 的 API 已经难以维护, 并且使用的是. net fx4.0, 各种不便, 完全重构是不可能的), 所以新版本采用 restful web API(本来想直接上. net core Web API, 成本...).
关于 API 版本控制的其他几种方案不详述, 网上很多文章, 可以自己搜搜看, 对比选用适合自己的方案.
REST 风格的 API 版本控制
此时需要将老的 wcf 和新 Web API 管理起来了, 做一个 API 网关再合适不过 , 不仅处理了版本控制的问题, 后面还可扩展网关的其他职能, 迈开了从单体过渡到微服务的步伐
API 网关方案: 使用. net core Web 自实现 (本来想使用 Ocelot https://www.cnblogs.com/shanyou/p/10363360.html , 全套当然方便, 因其版本控制基于 url, 我更倾向于 REST 基于 http headers accept 来控制, 所以自己写吧)
API 网关落地:
1. 对服务的管理设计
建了一个 JSON 文件来配置对服务的管理, 因其要保留网关其他职能的可能性, 所以设计时要注意其扩展性, 当然目前没做服务发现, 所以需要这么一个设计来管理服务.
知道 vnd 啥意思吗? 搜了半天才知道, 自定义 mime 类型的前缀, 当然在这里不是必须的, 反正都是你自己约定解析的, 当然对公还是遵循通用规范好一些.
rule 里面是对请求的验证规则, 顺便使用 polly 做了重试和超时.
- {
- "Rule": {
- "CORS": {
- "AllowOrigins": "",
- "AllowMethods": "GET,POST,PUT,DELETE",
- "AllowHeaders": "Accept,Content-Type"
- },
- "Headers": {
- "AcceptPrefix": "vnd.saas.services.",
- "AcceptKey": "version",
- "AcceptContentType": "json"
- },
- "API": {
- "FilterAPI": "",
- "APITimeOut": "60"
- }
- },
- "Services": {
- "main": {
- "v1": {
- "Host": "",
- "Port": "",
- "Deprecated": false
- },
- "v2": {
- "Host": "",
- "Port": "",
- "Deprecated": false
- }
- }
- }
- }
- services.JSON
2. 构建中间件处理请求
* 注入 IConfiguration 和 IHttpClientFactory, 用来读取 JSON 配置和发送请求
* 解析请求中 http headers 中 accept 属性, 我这里就是通过分割字符串来解析, 反正网上我是没找到解析 accept 中自定义 mime 类型的方法, 然后与配置中的要求对比, 拿到其路由信息
* 处理一些非法请求及错误
* 发送请求到该版本服务, 正确返回后响应给请求者
- public class RequestMiddleware
- {
- private readonly RequestDelegate _next;
- private readonly IConfiguration _configuration;
- private readonly IHttpClientFactory _clientFactory;
- public RequestMiddleware(RequestDelegate next, IConfiguration configuration, IHttpClientFactory clientFactory)
- {
- _next = next;
- _configuration = configuration;
- _clientFactory = clientFactory;
- }
- public async Task InvokeAsync(HttpContext context)
- {
- var acceptStr = context.Request.Headers["Accept"]+"";
- var acceptPrefix= _configuration.GetSection("Rule:Headers:AcceptPrefix").Value;
- var acceptKey = _configuration.GetSection("Rule:Headers:AcceptKey").Value;
- var acceptContentType = _configuration.GetSection("Rule:Headers:AcceptContentType").Value;
- var filterAPI = _configuration.GetSection("Rule:API:FilterAPI").Value;
- var version = new ServiceVersion();
- var response = new HttpResponseMessage();
- var error_code = 0;
- var error_message = string.Empty;
- //default in now
- var accept = new Accept()
- {
- ServiceName = "main",
- Version = "v1",
- AcceptContentType = "json"
- };
- if (!string.IsNullOrEmpty(acceptStr))
- {
- var acceptArray = acceptStr.Split(';');
- if (acceptArray.Length>= 2 && acceptArray[0].Contains(acceptPrefix) && acceptArray[1].Contains(acceptKey))
- {
- accept.ServiceName = acceptArray[0].Split('+')[0].Replace(acceptPrefix, "");
- accept.AcceptContentType = acceptArray[0].Split('+')[1];
- accept.Version = acceptArray[1].Split('=')[1];
- }
- }
- if (_configuration.GetSection($"Services:{accept.ServiceName}:{accept.Version}").Exists())
- {
- _configuration.GetSection($"Services:{accept.ServiceName}:{accept.Version}").Bind(version);
- }
- else
- {
- response.StatusCode= HttpStatusCode.BadRequest;
- error_code = (int)HttpStatusCode.BadRequest;
- error_message = "You should check that the request headers is correct";
- }
- if (version.Deprecated)
- {
- response.StatusCode = HttpStatusCode.MovedPermanently;
- error_code = (int)HttpStatusCode.MovedPermanently;
- error_message = "You should check the version of the API";
- }
- try
- {
- if (error_code == 0)
- {
- // filter API
- var sourceName = context.Request.Path.Value.Split('/');
- if (sourceName.Length> 1 && filterAPI.Contains(sourceName[sourceName.Length - 1]))
- accept.ServiceName = "FilterAPI";
- context.Request.Host = new HostString(version.Host, version.Port);
- context.Request.PathBase = "";
- var client = _clientFactory.CreateClient(accept.ServiceName);//rename to filter
- var requestMessage = context.Request.ToHttpRequestMessage();
- //var response = await Policy.NoOpAsync().ExecuteAsync(()=> {
- // return client.SendAsync(requestMessage, context.RequestAborted); ;
- //});
- response = await client.SendAsync(requestMessage, context.RequestAborted);
- }
- }
- catch (Exception ex)
- {
- response.StatusCode= HttpStatusCode.InternalServerError;
- error_code = (int)HttpStatusCode.InternalServerError;
- error_message = "Internal Server Error";
- }
- finally
- {
- if (error_code> 0)
- {
- response.Headers.Clear();
- response.Content = new StringContent("error is no content");
- response.Headers.Add("error_code", error_code.ToString());
- response.Headers.Add("error_message", error_message);
- }
- await response.ToHttpResponse(context);
- }
- await _next(context);
- }
- }
- // Extension method used to add the middleware to the HTTP request pipeline.
- public static class RequestMiddlewareExtensions
- {
- public static IApplicationBuilder UseRequestMiddleware(this IApplicationBuilder builder)
- {
- return builder.UseMiddleware<RequestMiddleware>();
- }
- }
- RequestMiddleware
目前就构建了一个中间件, 代码写得较乱
3. 注册中间件和服务配置
* 在 ConfigureServices 中将 services.JSON 注册进去, 这样中间件中才能读到
* 在 Configure 中使用中间件
- public void ConfigureServices(IServiceCollection services)
- {
- var configuration = new ConfigurationBuilder().SetBasePath(Directory.GetCurrentDirectory()).AddJsonFile("Configs/services.json").Build();
- services.AddSingleton<IConfiguration>(c => configuration);
- var AllowOrigins =configuration.GetSection("Rule:CORS:AllowOrigins").Value.Split(',');
- var AllowMethods = configuration.GetSection("Rule:CORS:AllowMethods").Value.Split(',');
- var AllowHeaders = configuration.GetSection("Rule:CORS:AllowHeaders").Value.Split(',');
- services.AddCors(options => options.AddPolicy(
- "CORS", policy => policy.WithOrigins(AllowOrigins).WithHeaders(AllowHeaders).WithMethods(AllowMethods).AllowCredentials()
- ));
- var APITimeOut = int.Parse(configuration.GetSection("Rule:API:APITimeOut
来源: http://www.bubuko.com/infodetail-3164751.html