// 第一次写,写的比较乱
要求绝大部分的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即可。

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);
}
});
}





(完)
记录一次.net core web Api 项目中在全局对返回值进行类包装的解决过程
原文:https://www.cnblogs.com/ttyycc/p/14982788.html