我目前每天主要工作以開發api為主,這都離不開介面文件。如果遠端對接的話前端總說Swagger不清晰,只能重新找一下新的介面文件。ShowDoc就是一個不錯的選擇,簡潔、大方、靈活部署。
但是話說回來,既然是文件每個介面你都得寫。總感覺這樣效率太慢了,能不能自己生成一下,自己只要Ctrl+C、Ctrl+V就萬事大吉了。
早就想寫一下,今天抽空做了一下(後期我會繼續完善,時間、精力有限?)。提前說好,我只寫了一個查詢的。而且也不可能說是生成了就不用改了,裡面的文字資訊全都符合各位同學的預期。但至少百分之八十的介面只要已生成直接copy就ok,還有一些個別介面... ...
一般來說,分頁查詢的響應資訊結構都是一樣的。不同的介面資料不同而已,所以返回的那個實體物件就反射那個物件。我是在屬性上標註特性以獲得相應註釋資訊。
首先新建一個Api專案
定義一個實體類和要返回的資訊類。
public class Products { [DescriptionAttribute("資料id")] public int id { get; set; } [DescriptionAttribute("商品名稱")] public string productNams { get; set; } [DescriptionAttribute("商品價格")] 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除外)。
//類和類中的屬性資訊用這個特性
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; } }
將要請求的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 = 24) { 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 } }; } }
再寫一個介面,專門用來查詢指定介面的資訊。兩個引數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($"**請求方式:**"); 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]}"); } #endregion #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 #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); } } }
會生成檔案流直接下載即可、
可還行?後續我會抽時間再補充修改,各位同學也可自行擴充套件、