C#根据反射动态创建ShowDoc接口文本信息
我目前每天主要工作以开发api为主,这都离不开接口文档。如果远程对接的话前端总说Swagger不清晰,只能重新找一下新的接口文档。ShowDoc就是一个不错的选择,简洁、大方、灵活部署。
但是话说回来,既然是文档每个接口你都得写。总感觉这样效率太慢了,能不能自己生成一下,自己只要Ctrl+C、Ctrl+V就万事大吉了。
早就想写一下,今天抽空做了一下(后期我会继续完善,时间、精力有限),已完善😅。提前说好,我只写了一个查询的。不可能说是生成了就不用改了,里面的文本信息全都符合各位同学的预期。但至少百分之八十的接口只要已生成直接copy就ok,还有一些个别接口只能... ...
一般来说,分页查询的响应信息结构都是一样的。不同的接口数据不同而已,所以返回的那个实体对象就反射那个对象。我是在属性上标注特性以获得相应注释信息。
添加和修改在实体参数上标注特性,删除就最简单了(请看代码)。至于返回值这些,一般都是提前定好的,个别接口不一样的话,就手动改一下。
首先新建一个Api项目
定义一个实体类和要返回的信息类。
public class Products { [DescriptionAttribute("数据id")] public int id { get; set; } [DescriptionAttribute("商品名称")] [Required(ErrorMessage = "商品名称必传")] public string productNams { get; set; } [DescriptionAttribute("商品价格")] [Required(ErrorMessage = "价格必传")] public float price { get; set; } }
/// <summary> /// 通用返回信息类 /// </summary> public class MessageModel<T> where T : class { [DescriptionAttribute("状态码")] public int code { get; set; } = 200; /// <summary> /// 操作是否成功 /// </summary> [DescriptionAttribute("操作是否成功")] public bool success { get; set; } = false; /// <summary> /// 返回信息 /// </summary> [DescriptionAttribute("返回信息")] public string msg { get; set; } = "服务器异常"; /// <summary> /// 返回数据集合 /// </summary> [DescriptionAttribute("返回数据集合")] public T response { get; set; } } /// <summary> /// 通用分页信息类 /// </summary> public class PageModel<T> { /// <summary> /// 当前页标 /// </summary> [DescriptionAttribute("当前页标")] public int pageIndex { get; set; }; /// <summary> /// 总页数 /// </summary> [DescriptionAttribute("总页数")] public int pageCount { get; set; }; /// <summary> /// 数据总数 /// </summary> [DescriptionAttribute("数据总数")] public int dataCount { get; set; }; /// <summary> /// 每页大小 /// </summary> [DescriptionAttribute("每页大小")] public int PageSize { set; get; } /// <summary> /// 返回数据 /// </summary> [DescriptionAttribute("返回的数据集合")] public T[] data { get; set; } }
写三个特性,一个用来标注属性信息,一个用来标注search查询对象中的参数(我这边分页查询,查询参数传json对象字符串,pageIndex和pageSize除外),还有一个用来标注添加和修改的Model对象。
//类和类中的属性信息用这个特性 public class DescriptionAttribute : Attribute { public string _details = string.Empty; public DescriptionAttribute(string details) { this._details = details; } } //接口方法中的search参数用这个特性 public class SearchAttribute : Attribute { public string _details = string.Empty; public SearchAttribute(string details) { this._details = details; } }
//添加和修改的Model参数用这个特性 public class ModelParameterAttribute : Attribute { public Type _objectT; public ModelParameterAttribute(Type objectT) { this._objectT = objectT; } }
将要请求的ip地址写入appsettings.json中
{ "Logging": { "LogLevel": { "Default": "Information", "Microsoft": "Warning", "Microsoft.Hosting.Lifetime": "Information" } }, "AllowedHosts": "*", "requestUrl": { "ip": "http://127.0.0.1:5001" } }
写好要查询的接口,待会儿生成这个接口的接口文档信息
[Route("api/[controller]/[action]")] [ApiController] public class InstanceController : Controller { public InstanceController() { } [HttpGet] [DescriptionAttribute("获取所有商品数据")] [SearchAttribute("{\"eId\": \"设备id\",\"startTime\": \"2020-06-05\",\"endTime\": \"2020-06-06\"}")] public async Task<MessageModel<PageModel<Products>>> GetAllDatas(string search = "", int pageIndex = 1, int pageSize = 30) { var list = new List<Products>() { new Products{ id=1,productNams="商品1",price=13.6f}, new Products{ id=2,productNams="商品2",price=14.6f}, new Products{ id=3,productNams="商品3",price=15.6f} }.ToArray(); return new MessageModel<PageModel<Products>>() { success = true, msg = "数据获取成功", response = new PageModel<Products>() { pageIndex = pageIndex, pageCount = Convert.ToInt32(Math.Ceiling(Convert.ToDecimal(list.Length) / pageSize)), dataCount = list.Length, PageSize = pageSize, data = list } }; } [HttpPost] [DescriptionAttribute("添加商品数据")] public async Task<IActionResult> SaveDatas([FromBody] [ModelParameterAttribute(typeof(Products))]Products products) { return Json(new { success = true, msr = "操作成功", code = 200 }); } [HttpDelete] [DescriptionAttribute("删除商品数据")] public async Task<IActionResult> DeleteDatas(int id) { return Json(new { success = true, msr = "操作成功", code = 200 }); } }
再写一个接口,专门用来查询指定接口的信息。两个参数controlleName(控制器名称)、apiMethodsName(接口名称)
[Route("api/[controller]/[action]")] [ApiController] public class ShowDocFileControlle : Controller { private readonly IHostingEnvironment _hostingEnvironment; private IConfiguration _configuration; public ShowDocFileControlle(IHostingEnvironment hostingEnvironment, IConfiguration configuration) { _hostingEnvironment = hostingEnvironment; _configuration = configuration; } /// <summary> /// 反射获取指定接口的信息 /// </summary> /// <returns></returns> [HttpGet("{controlleName}/{apiMethodsName}")] public async Task<IActionResult> GetShowDocApiFiles(string controlleName, string apiMethodsName) { #region 首先拿到要操作的文件 //获取文件 路径 string webRootPath = _hostingEnvironment.WebRootPath + @"\ApiInfo.txt"; //得到文件流 FileStream stream = new FileStream(webRootPath, FileMode.Create, FileAccess.Write); //创建写入的文件流对象 StreamWriter writer = new StreamWriter(stream); #endregion try { #region 根据参数反射操作对应的对象 writer.WriteLine("**简要描述:** "); writer.WriteLine(""); //根据参数controlleName得到类型 Type type = Type.GetType($"ReflectionShowDoc.Controllers.{controlleName}"); //根据类型创建该对象的实例 object instance = Activator.CreateInstance(type); //再根据参数apiMethodsName得到对应的方法 MethodInfo method = type.GetMethod($"{apiMethodsName}"); #endregion #region 判断Api方法上是否有DescriptionAttribute这个特性,有就获取值 if (method.IsDefined(typeof(DescriptionAttribute), true)) { //实例化得到一个DescriptionAttribute类型 //通过反射创建对象 DescriptionAttribute attribute = (DescriptionAttribute)method.GetCustomAttribute(typeof(DescriptionAttribute), true); writer.WriteLine($"{attribute._details}"); } else writer.WriteLine($"接口未标明注释"); #endregion #region 根据参数controlleName与apiMethodsName得到请求的url,ip建议写到配置文件中,读也只读配置文件 writer.WriteLine(""); writer.WriteLine($"**请求URL:**"); writer.WriteLine(""); StringBuilder builder = new StringBuilder(@$"- `{_configuration["requestUrl:ip"]}/api/"); builder.Append($"{controlleName}/{apiMethodsName}`"); writer.WriteLine(builder.ToString()); writer.WriteLine(""); writer.WriteLine($"**请求方式:**"); #endregion #region 根据抽象父类HttpMethodAttribute得到接口的请求类型 if (method.IsDefined(typeof(HttpMethodAttribute), true)) { //通过反射创建对象 HttpMethodAttribute attribute = (HttpMethodAttribute)method.GetCustomAttribute(typeof(HttpMethodAttribute), true); writer.WriteLine($"- {attribute.HttpMethods.ToArray()[0]}"); #region 删除的接口 if (attribute.HttpMethods.ToArray()[0].ToString().Equals("DELETE")) { #region 参数,一般删除参数都为id writer.WriteLine(""); writer.WriteLine($"**参数:** "); writer.WriteLine(""); writer.WriteLine($"|参数名|必选|类型|说明|"); writer.WriteLine($"|:----|:---|:-----|-----|"); writer.WriteLine($"|id |是 |int |数据id|"); #endregion #region 返回示例,一般都是定好的 writer.WriteLine(""); writer.WriteLine(" **返回示例**"); writer.WriteLine(""); writer.WriteLine("```"); writer.WriteLine(" msg ='操作成功,'"); writer.WriteLine(" success = true,"); writer.WriteLine(" code = 200"); writer.WriteLine("```"); writer.WriteLine(""); #endregion #region 返回参数说明,一般也是定好的 writer.WriteLine(""); writer.WriteLine($" **返回参数说明** "); writer.WriteLine(""); writer.WriteLine($"|参数名|类型|说明|"); writer.WriteLine($"|:-----|:-----|-----|"); writer.WriteLine($" |code|string|状态码|"); writer.WriteLine($" |success |bool|添加是否成功|"); writer.WriteLine($"|msg |string |返回的消息 |"); #endregion goto action; } #endregion } #endregion #region 查看API方法参数是否有ModelParameterAttribute的特性,有的话就是添加和修改否则是查询 ParameterInfo modelParameter = method.GetParameters()[0]; if (modelParameter.IsDefined(typeof(ModelParameterAttribute), true)) { #region 反射得到Model参数的各类信息 //实例化得到一个CustomAttribute类型 //通过反射创建对象 ModelParameterAttribute attribute = (ModelParameterAttribute)modelParameter.GetCustomAttribute(typeof(ModelParameterAttribute), true); Type modelType = attribute._objectT; writer.WriteLine(""); writer.WriteLine($"**参数:** "); writer.WriteLine(""); writer.WriteLine($"|参数名|必选|类型|说明|"); writer.WriteLine("|:----|:---|:-----|-----|"); //遍历Model中的属性 foreach (var item in modelType.GetProperties()) { bool isRequired = false; //框架自带的验证特性RequiredAttribute=>字段上有就必填否则非必填 if (item.IsDefined(typeof(RequiredAttribute), true)) isRequired = true; //Model的属性字段上是否有DescriptionAttribute特性 if (item.IsDefined(typeof(DescriptionAttribute), true)) { //创建实例=>得到详情 DescriptionAttribute fieldAttribute = (DescriptionAttribute)item.GetCustomAttribute(typeof(DescriptionAttribute), true); writer.WriteLine($"|{item.Name}| {(isRequired ? '是' : '否')} |{item.PropertyType}|{fieldAttribute._details}|"); } else writer.WriteLine($"|{item.Name}| {(isRequired ? '是' : '否')} |{item.PropertyType}|'字段说明数据(详情)数据未添加'|"); } #endregion #region 返回示例,一般也是定好的 writer.WriteLine(""); writer.WriteLine($" **返回示例** "); writer.WriteLine(""); writer.WriteLine($"```"); writer.WriteLine($" msg ='操作成功,'"); writer.WriteLine($" success = true,"); writer.WriteLine($" code = 200"); writer.WriteLine($"```"); #endregion #region 返回参数说明,一般也是定好的 writer.WriteLine(""); writer.WriteLine($" **返回参数说明** "); writer.WriteLine(""); writer.WriteLine($"|参数名|类型|说明|"); writer.WriteLine($"|:-----|:-----|-----|"); writer.WriteLine($" |code|string|状态码|"); writer.WriteLine($" |success |bool|添加是否成功|"); writer.WriteLine($"|msg |string |返回的消息 |"); #endregion } #endregion else { #region 一般分页查询这些参数都是定好的,基本不会变 writer.WriteLine(""); writer.WriteLine($"**参数:** "); writer.WriteLine(""); writer.WriteLine($"|参数名|必选|类型|说明|"); writer.WriteLine($"|:---- |:---|:----- |----- |"); writer.WriteLine($"|search |否 |string |查询的对象|"); writer.WriteLine($"|pageIndex |是 |int | 页码 |"); writer.WriteLine($"|pageSize |是 |int | 页面展示的数据量 |"); #endregion #region 参数search是一个json字符串,这里也通过特性标注,在实例化的时候获取 writer.WriteLine($"**参数search所需参数及传参示例**"); writer.WriteLine("``` "); if (method.IsDefined(typeof(SearchAttribute), true)) { //实例化得到一个SearchAttribute类型 //通过反射创建对象 SearchAttribute attribute = (SearchAttribute)method.GetCustomAttribute(typeof(SearchAttribute), true); writer.WriteLine($"{attribute._details}"); writer.WriteLine(""); } writer.WriteLine("将查询的search对象序列化之后传过来"); writer.WriteLine($"`{builder.ToString().Replace("-", string.Empty).Replace("`", string.Empty)}" + "?pageIndex=1&pageSize=30&search=serializeObject`"); writer.WriteLine("``` "); writer.WriteLine(""); #endregion #region 因为要拿到响应的返回参数,所以这里动态调用一下方法,取得第一页的数据作为返回的数据示例 writer.WriteLine($" **返回示例**"); //这三个参数基本不会变 Type[] paramsType = new Type[3]; paramsType[0] = Type.GetType("System.String"); paramsType[1] = Type.GetType("System.Int32"); paramsType[2] = Type.GetType("System.Int32"); //设置方法中的参数值,如有多个参数可以追加多个 object[] paramsObj = new object[3]; paramsObj[0] = "parameter"; paramsObj[1] = 1; paramsObj[2] = 24; //执行方法 dynamic queryData = type.GetMethod($"{apiMethodsName}", paramsType) .Invoke(instance, paramsObj); //得到Result对象 object value = queryData.GetType() .GetProperty("Result") .GetValue(queryData); //将数据序列化 var methodResult = JsonConvert.SerializeObject(queryData.Result); writer.WriteLine("``` "); //将数据写入到文本中 writer.WriteLine($"{methodResult}"); writer.WriteLine("``` "); #endregion #region 返回(响应)的参数字段说明 writer.WriteLine(""); writer.WriteLine(" **返回参数说明** "); writer.WriteLine(""); writer.WriteLine("|参数名|类型|说明|"); writer.WriteLine("|:----- |:-----|-----|"); //根据查询到的Result对象获取类型 Type messageModelType = Type.GetType(value.GetType().ToString()); //便利Result对象中的各个属性信息 foreach (var itemmessageModelProp in messageModelType.GetProperties()) { //这个response对象里面就是数据 if (itemmessageModelProp.Name.Equals("response")) { //根据value中的response属性得到其类型 Type typeReturnData = Type.GetType(value.GetType().GetProperty("response").GetValue(value).ToString()); //遍历response对象中的属性 foreach (var item in typeReturnData.GetProperties()) { //data中是实体对象 if (item.Name.Equals("data")) { //有可能是数组,将中括号剔除掉 var dataType = item.PropertyType.ToString().Replace("[]", string.Empty); Type propertyType = Type.GetType(dataType);//加载类型 foreach (PropertyInfo propertyInfo in propertyType.GetProperties()) { if (propertyInfo.IsDefined(typeof(DescriptionAttribute), true)) { //通过反射创建对象 DescriptionAttribute attribute = (DescriptionAttribute)propertyInfo.GetCustomAttribute(typeof(DescriptionAttribute), true); writer.WriteLine($"|{propertyInfo.Name} |{propertyInfo.PropertyType} |{attribute._details} |"); } } } else { //拿到与data对象平级的参数,看有没有DescriptionAttribute特性 if (item.IsDefined(typeof(DescriptionAttribute), true)) { //通过反射创建对象 DescriptionAttribute attribute = (DescriptionAttribute)item.GetCustomAttribute(typeof(DescriptionAttribute), true); writer.WriteLine($"|{item.Name} |{item.PropertyType} |{attribute._details} |"); } } } } else { //拿到与response对象平级的参数,看有没有DescriptionAttribute特性 if (itemmessageModelProp.IsDefined(typeof(DescriptionAttribute), true)) { //通过反射创建对象 DescriptionAttribute attribute = (DescriptionAttribute)itemmessageModelProp.GetCustomAttribute(typeof(DescriptionAttribute), true); writer.WriteLine($"|{itemmessageModelProp.Name} |{itemmessageModelProp.PropertyType} |{attribute._details} |"); } } } #endregion } action: #region 错误信息一般也是定好的 writer.WriteLine(" **错误描述** "); writer.WriteLine(" **(错误)返回示例**"); writer.WriteLine(""); writer.WriteLine("``` "); writer.WriteLine(" {"); writer.WriteLine($" {"msg"}: {"服务器异常"},"); writer.WriteLine($" {"success"}: {true},"); writer.WriteLine($" {"exception"}:{""},"); writer.WriteLine($" {"code"}: {500}"); writer.WriteLine(@" }"); writer.WriteLine($"```"); writer.WriteLine($" **(错误)返回参数说明** "); writer.WriteLine($""); writer.WriteLine($"|参数名|类型|说明|"); writer.WriteLine($"|:----- |:-----|-----|"); writer.WriteLine($"|msg |string |消息 |"); writer.WriteLine($"|success |bool |操作是否成功 |"); writer.WriteLine($"|exception |string |具体的错误描述 |"); writer.WriteLine($"|code |string |状态码 |"); #endregion #region GC writer.Close();//释放内存 stream.Close();//释放内存 #endregion #region 输出文件流 FileStream streamFile = new FileStream(webRootPath, FileMode.Open, FileAccess.Read); return File(streamFile, "application/vnd.android.package-archive", webRootPath); #endregion } catch (Exception ex) { writer.Close();//释放内存 stream.Close();//释放内存 throw new Exception(ex.Message); } } }
会生成文件流直接下载即可、
可还行?各位同学也可自行扩展,如有不足,请见谅!