// 第一次写,写的比较乱
一. 先看需求:
要求绝大部分的api接口的返回值都用此类型包装过后进行返回,将原返回值放到result中。
/// <summary> /// 结果返回模型 /// </summary> /// <typeparam name="T"></typeparam> public class ApiResult<T> { public int code { get; set; } public string message { get; set; } public T result { get; set; } }
二、 分析:
如果只是在返回值中进行类包装,那应该是很简单的一个需求了,直接在OnActionExecuted中进行一下包装即可,但是如果只是如此的话,swagger中肯定只是有原模型的信息,而不是包装好的。那么我将这个需求分为两部分。
第一部分就是将返回值先进行类包装,第二部分再看一下如何将swagger中的信息进行类包装。
三、第一部分(将返回值先进行类包装):
这个直接用filter拦截器就可以了
using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.Controllers; using Microsoft.AspNetCore.Mvc.Filters; using Newtonsoft.Json; using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; namespace OnePiece.Tools.Filters { public class ApiResult : IActionResult { public int code { get; set; } public string message { get; set; } public object result { get; set; } public Task ExecuteResultAsync(ActionContext context) { HttpResponse response = context.HttpContext.Response; string json = string.Empty; if (this != null) { json = JsonConvert.SerializeObject(this); } response.Headers["content-type"] = "application/json; charset=utf-8"; return Task.FromResult(response.WriteAsync(json)); } } public class ApiResourceFilter : IActionFilter { public void OnActionExecuted(ActionExecutedContext filterContext) { if (((ControllerActionDescriptor)filterContext.ActionDescriptor).MethodInfo.CustomAttributes.Any(e => e.AttributeType.Name == nameof(NotApiResultAttribute))) { return; } filterContext.Result = new ApiResult { code = 200, message = "", result = ((ObjectResult)filterContext.Result).Value }; } public void OnActionExecuting(ActionExecutingContext filterContext) { } } }
这是拦截器的代码,
1. 对类进行包装的拦截器实现了IActionFilter接口,OnActionExecuted方法使得action结束时会经过该方法,那么我们在此进行类包装即可。
2. 由于filterContext.Result的值需要实现IActionResult,我只好重新写了一个ApiResult类,实现了一下IActionResult接口。
3. 其中用到了一个特性NotApiResultAttribute,这个特性中没有什么实际的内容,只是为了标识一下它不需要被类包装。
4. 最后再在全局中添加上此filter即可。
四、第二部分(将swagger中的信息进行类包装)
1.先看看swagger返回的json是什么含义
要想对swagger进行类包装就先需要知道swagger是如何呈现的,通过把源码下载到本地并调试,我知道了实际上swagger会返回如下图的json,“--”后面的内容是我自己写的(其实通过F12看api的返回内容也是可以看到的)
那么再详细看看paths中,以及components中的内容,诶?我发现paths中的每个路由下都有一个responses,而且在responses中还有一段
"OnePiece.Models.Models.ApiResult`1[[OnePiece.Entities.Partner, OnePiece.Models, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null]]",前面还有一个“$ref”,em...ref?引用?
如果比较了解反射,应该可以看出来这是ApiResult<Partner>此类型的Fullname,如果看不出来也没关系,我发现components中有一个完全相同的key,那是不是上面的那个ref就是指引用这个模型呢?
经过我尝试了几次之后,发现确实是。那么下一步,我就想去看看有没有什么办法可以改变paths和components,如果可以改变,那我就可以通过手动往components中添加我自己的字典,然后改变paths中的responses中的“$ref”的值来实现对swagger进行类包装了。
那么顺着这个思路继续往下走,我先是大概溜了一遍,我相信swagger一定会有暴露给我们的,供我们可以进行类包装的方法。
下图我贴出swaggermiddleware的核心invoke方法,以及其中的getswagger方法
public async Task Invoke(HttpContext httpContext, ISwaggerProvider swaggerProvider) { if (!RequestingSwaggerDocument(httpContext.Request, out string documentName)) { await _next(httpContext); return; } try { var basePath = httpContext.Request.PathBase.HasValue ? httpContext.Request.PathBase.Value : null; var swagger = swaggerProvider.GetSwagger( documentName: documentName, host: null, basePath: basePath); // One last opportunity to modify the Swagger Document - this time with request context foreach (var filter in _options.PreSerializeFilters) { filter(swagger, httpContext.Request); } if (Path.GetExtension(httpContext.Request.Path.Value) == ".yaml") { await RespondWithSwaggerYaml(httpContext.Response, swagger); } else { await RespondWithSwaggerJson(httpContext.Response, swagger); } } catch (UnknownSwaggerDocument) { RespondWithNotFound(httpContext.Response); } }
我发现其中有一个filters是PreSerializeFilters,可以看到它的入参是有一个swagger和一个rquest,根据上下文可知,它应该是一个已经完整的模型,还没被json序列化而已。
既然如此,那我就开始往components里面加我自己的keyvalue了,那么我先看看同一个类型,被包装过和没有被包装过的区别好了,还是看上面那个partner返回类型的例子:
未被包装的responses:
components中对应的字典:
被包装过的responses:
components中对应的字典:
通过对比,我基本知道自己该怎么做了,第一步,将responses中的$ref 改成被包装过得类型的fullname。第二步,往components中加入一个以该fullname为key的keyvalue,其中除了result中的$ref,其它全部照搬上图即可。(其实也不需要照搬,看两眼这个图基本也能知道各个kv所代表的含义,自己写也完全没问题。)
直接贴出我的代码了
// 这是startup里的Configure中的内容
app.UseSwagger(
c => c.PreSerializeFilters.Add(ApiRuslt)
);
// 这是上面filter的具体实现
public void ApiRuslt(OpenApiDocument a, HttpRequest b) { var codeSchema = new OpenApiSchema { AdditionalPropertiesAllowed = true, Description = "相应代码", Format = "int32", Type = "integer" }; var messageSchema = new OpenApiSchema { AdditionalPropertiesAllowed = true, Description = "信息", Nullable = true, Type = "string" }; var keys = a.Components.Schemas.Keys.ToList(); for (int i = 0; i < keys.Count; i++) { var type = typeof(ApiResult<>); Type s = StaticMethods.GetTypeByName(keys[i]); type = type.MakeGenericType(s); var schema = a.Components.Schemas[keys[i]]; OpenApiSchema aa = new OpenApiSchema(); aa.Type = "object"; aa.Description = "结果返回模型"; aa.Properties = new Dictionary<string, OpenApiSchema> { { "code", codeSchema } }; aa.Properties.Add("message", messageSchema); aa.Properties.Add("result", new OpenApiSchema { Properties = new Dictionary<string, OpenApiSchema> { }, Reference = new OpenApiReference { Id = keys[i], Type = ReferenceType.Schema }, Description = "信息", Type = "object" }); if (!a.Components.Schemas.ContainsKey(type.FullName)) { a.Components.Schemas.Add(type.FullName, aa); } } }
代码写的比较具体,其实可以写成可以泛用的,我这边没太大兴趣写了。。。
但是有个东西我还没有实现,那就是如何判断这个action是否有notapiresult这个特性呢,想判断这个的话,就需要去别的地方再找找了,但我知道我需要找一个可以单独看action相关内容的地方。
后来在GetSwagger方法中的GeneratePaths方法中的GenerateOperations方法中的GenerateOperation终于找到了我想要的OperationFilters
直接贴代码了
services.AddSwaggerGen(options => { ......... options.OperationFilter<AddSwaggerBizParametersFilters>(); ......... });
public class AddSwaggerBizParametersFilters : IOperationFilter { public void Apply(OpenApiOperation operation, OperationFilterContext context) { if (context.MethodInfo.CustomAttributes.Any(e => e.AttributeType.Name == nameof(NotApiResultAttribute))) { return; } operation.Responses.ForEach(e => e.Value.Content.ForEach(a => { var type = typeof(ApiResult<>); Type s = StaticMethods.GetTypeByName(a.Value.Schema.Reference.Id); type = type.MakeGenericType(s); a.Value.Schema.Reference.Id = type.FullName; })); } }
我这边把修改responses中的$ref的工作也放到这里了。
至于修改Id就可以修改$ref的原因在于下图:
至此,swagger里的object类型的对象都可以进行包装了。
但是,当我用bool或者int或者string或者guid等等这种基本类型的时候,问题出现了,因为他们根本就不需要引用其他类型
他们的schema只有type和format,之后我尝试了通过type和format去创建类似ApiResult<Int>,ApiResult<decimal>之类的类型的fullname,但是失败了,其实在失败之后我有想过是不是基本类型不能使用我想的这种方法,
但是第二天我突然想到会不会只要$ref的值和components中key的对应即可,并不需要一定是反射出来的fullname,经过测试,发现果真如此。
除了跟swagger有关的两个filter进行了一些改动,其他没有任何变动,我只贴出两个filter的代码了:
public class AddSwaggerBizParametersFilters : IOperationFilter { public void Apply(OpenApiOperation operation, OperationFilterContext context) { if (context.MethodInfo.CustomAttributes.Any(e => e.AttributeType.Name == nameof(NotApiResultAttribute))) { return; } operation.Responses.ForEach(e => e.Value.Content.ForEach(a => { var type = typeof(ApiResult<>); if (a.Value.Schema.Reference == null) { a.Value.Schema = new OpenApiSchema { AdditionalPropertiesAllowed = false, Reference = new OpenApiReference { Id = a.Value.Schema.Format + "tyc" + a.Value.Schema.Type, Type = ReferenceType.Schema }, Items = a.Value.Schema.Items }; return; } Type s = StaticMethods.GetTypeByName(a.Value.Schema.Reference.Id); type = type.MakeGenericType(s); a.Value.Schema.Reference.Id = type.FullName; })); } }
public void ApiRuslt(OpenApiDocument a, HttpRequest b) { var codeSchema = new OpenApiSchema { AdditionalPropertiesAllowed = true, Description = "相应代码", Format = "int32", Type = "integer" }; var messageSchema = new OpenApiSchema { AdditionalPropertiesAllowed = true, Description = "信息", Nullable = true, Type = "string" }; var needAdds = a.Paths.SelectMany(e => e.Value.Operations).SelectMany(e => e.Value.Responses) .SelectMany(e => e.Value.Content).Select(e => e.Value.Schema).Where(e => e.Reference != null && e.Reference.Id.Contains("tyc")).ToList(); var needButy = a.Components.Schemas.SelectMany(e => e.Value.Properties).Where(e => e.Value.Description == null && e.Value.Reference != null).Select(e => e.Value).ToList(); needButy.ForEach(e => { var oo = a.Components.Schemas.Where(s => e.Reference.Id.Contains(s.Key)).FirstOrDefault(); e.Description = oo.Value.Description; }); var keys = a.Components.Schemas.Keys.ToList(); for (int i = 0; i < keys.Count; i++) { var type = typeof(ApiResult<>); Type s = StaticMethods.GetTypeByName(keys[i]); type = type.MakeGenericType(s); var schema = a.Components.Schemas[keys[i]]; OpenApiSchema aa = new OpenApiSchema(); aa.Type = "object"; aa.Description = "结果返回模型"; aa.Properties = new Dictionary<string, OpenApiSchema> { { "code", codeSchema } }; aa.Properties.Add("message", messageSchema); aa.Properties.Add("result", new OpenApiSchema { Properties = new Dictionary<string, OpenApiSchema> { }, Reference = new OpenApiReference { Id = keys[i], Type = ReferenceType.Schema }, Description = "信息", Type = "object" }); if (!a.Components.Schemas.ContainsKey(type.FullName)) { a.Components.Schemas.Add(type.FullName, aa); } } needAdds.ForEach(e => { if (!a.Components.Schemas.ContainsKey(e.Reference.Id)) { OpenApiSchema xx = new OpenApiSchema(); xx.Type = "object"; xx.Description = "结果返回模型"; xx.AdditionalPropertiesAllowed = false; xx.Properties = new Dictionary<string, OpenApiSchema> { { "code", codeSchema } }; xx.Properties.Add("message", messageSchema); var list = e.Reference.Id.Split("tyc"); var schema = new OpenApiSchema { Properties = new Dictionary<string, OpenApiSchema> { }, Items = e.Items }; schema.AdditionalPropertiesAllowed = true; if (list.Length == 2) { if (list[0] != "") { schema.Format = list[0]; } schema.Type = list[1]; if (schema.Items != null) { schema.Format = schema.Items.Format; } } xx.Properties.Add("result", schema); a.Components.Schemas.Add(e.Reference.Id, xx); } }); }
五、看看实际效果
(完)