X-Admin&ABP框架開發-程式碼生成器
在日常開發中,有時會遇到一些相似的程式碼,甚至是隻要CV一次,改幾個名稱,就可以實現功能了,而且總歸起來,都可以由一些公用的頁面更改而來,因此,結合我日常開發中使用到的頁面,封裝一個適合自己的程式碼生成器,僅處於入門階段,包括生成的程式碼結構都僅是把框架展示出來,內部詳細暫時沒得,針對於應用服務中的介面和實現,相關Dto,MVC中的控制器、檢視及檢視模型進行了模板製作及生成相關的檔案。
一、設計思路
方案一:開始想到的是,搞個控制檯,然後給一個.cs檔案,然後控制檯去解析其中的名稱空間,類名,屬性,再用配置好的razor模板去替換,再生成相關的一些檔案出來,但是發現,萬事開頭難,第一步去解析cs檔案就不好搞,找了網上的資料,不太好弄,乾脆想了下,放棄這種方案,因為想到了另一種常用的方案。
方案二:直接在控制檯中,配置控制檯去訪問資料庫,然後給定指定表名,去讀取資料庫中的表和欄位,再反過來去生成相關檔案,但是這裡會遇到一個這樣的問題,比如我使用的是mysql,mysql本身有個配置表名大小寫忽視的,這樣一來,獲取到的表名都將是小寫打頭,儘管可能配置了是區分大小寫,但是,我設計表時,採用Pre_table,形式區分業務表,比如是CRM模組需要用到的CRM_Client,那將用CRM打頭,後面這部分Client才是實際程式碼中的類名,種種問題都有可能,但是作為沒有那麼多可能性下,比如沒得字首,不區分大小寫,形式簡單,那麼可以考慮使用。此時,想到了abp中的Migrator控制檯並想到了方案三。
方案三:如果說直接搞一個控制檯在程式碼中,模仿Abp自帶的Migrator一樣,啟動後,給定類名,通過反射去取得該類的屬性名,豈不是美滋滋,需要哪個類的相關檔案,只需啟動,然後輸入類名,即可得到相關的檔案。這幾種方案的前提都是在Dto檔案中會展示所有實體欄位,如果需要選擇性的使用欄位,則還需藉助人工智慧,以人力去完成更改生成的檔案。
二、Razor引擎的使用
我選擇了方案二作為入手去實現,並且採用Razor引擎作為模板解析的工具。Nuget引入RazorEngine.NetCore包,開始實現依靠模板生成程式碼。
1、先嚐試下Razor引擎,控制檯中CV下Razor引擎提供的Demo,引入相關名稱空間,學習下如何去使用。
string template = "Hello @Model.Name, welcome to RazorEngine!"; var result = Engine.Razor.RunCompile(template, "templateKey", null, new { Name = "World" }); Console.WriteLine(result);
執行完畢,可以獲取到執行結果,需要注意的是,如果是在linux或是mac跑會得到錯誤,該問題是Razor引擎本身的問題,暫時只能在window下跑。
2、熟悉了下Razor的使用方式後,開始使用簡單檔案形式填充一些資料模擬生成過程。
首先,一個檔案作為填充模板,一個檔案記憶體儲Json資料作為資料來源,程式啟動時載入兩個檔案。
var templatePage = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "SimpleCOders\\Templates", "templatePage.txt"); TemplatePage = File.ReadAllText(templatePage); var templatePageJson = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "SimpleCOders\\Templates", "templatePageJson.json"); TemplatePageJson = File.ReadAllText(templatePageJson);
其次,資料來源整理成相應類結構,得到批量待解析資料。
var templatePageJsonList = JsonConvert.DeserializeObject<List<PageDataModel>>(TemplatePageJson); foreach (var templatePageJson in templatePageJsonList) { RazorParse( templatePageJson.Index ?? 1, templatePageJson.Date, templatePageJson.Index - 1, templatePageJson.Index + 1, templatePageJson.Content ); }
最後,設計一下解析器,將讀取到的資料來源,進行解析成相關的類,然後依次按照模板生成檔案
var entityResult = Engine.Razor.RunCompile(TemplatePage, "templatePageKey", null, new { PostData = (date ?? DateTime.Now).ToString("yyyy-MM-dd"), PrevIndex = prev.Value, NextIndex = next.Value, ContentHtml = content });
按照一條資料便是一個模板檔案去生成可以得到批量生成檔案。
三、適合自己的簡單程式碼生成器
開始著手適合自己的簡單程式碼生成器,思路一致,只是增加了需要讀取資料庫這一環節。
1、模板製作,以應用服務介面為例,常用的增刪改查進行封裝,利用Razor語法進行填充處理,此處對於主鍵型別,沒有進行處理,只能支援諸如int、long之類的,後期在繼續優化。
using Abp.Application.Services; using Abp.Application.Services.Dto; using System.Collections.Generic; using System.Threading.Tasks; using @[email protected].@(Model.EntityName)s.Dto; namespace @[email protected].@(Model.EntityName)s { /// <summary> /// @(Model.EntityDescription)應用服務介面 /// </summary> public interface I@(Model.EntityName)AppService : IApplicationService { /// <summary> /// 獲取@(Model.EntityDescription)資料集合 /// </summary> /// <param name="input"></param> /// <returns></returns> Task<PagedResultDto<@(Model.EntityName)ListDto>> GetPaged@(Model.EntityName)(GetPaged@(Model.EntityName)Input input); /// <summary> /// 獲取@(Model.EntityDescription)編輯資訊 /// </summary> /// <param name="input"></param> /// <returns></returns> Task<Get@(Model.EntityName)ForEditOutput> Get@(Model.EntityName)ForEdit(NullableIdDto<@Model.EntityKeyType> input); /// <summary> /// 建立或修改@(Model.EntityDescription)資訊 /// </summary> /// <param name="input"></param> /// <returns></returns> Task CreateOrUpdate@(Model.EntityName)(CreateOrUpdate@(Model.EntityName)Input input); /// <summary> /// 刪除@(Model.EntityDescription) /// </summary> /// <param name="input"></param> /// <returns></returns> Task Delete@(Model.EntityName)(List<EntityDto<@Model.EntityKeyType>> inputs); } }
2、設定相應的解析器,與之前的嘗試不同,這次使用了具體的型別,這是Razor中的另一種方式,解析完畢後將檔案按照指定路徑儲存,儘量符合專案的路徑儲存。
var iRazorAppService = Engine.Razor.RunCompile(IRazorAppService, nameof(IRazorAppService), typeof(TemplateParseModel), templateParseModel); UtilHelper.Save(Path.Combine(AppDomain.CurrentDomain.BaseDirectory, applicationPath, $"I{templateParseModel.EntityName}AppService.cs"), iRazorAppService); builder.Append(iRazorAppService);
3、資料庫連線讀取表結構,控制檯下,採用直接讀取的形式,不走DbContext方式,Nuget中引入MySql.Data包(我本地用的Mysql),增加Appsettings.json檔案並配置好連線字串,用sql語句形式直接讀取資料庫中的資訊,此處封裝了一個DbHelper類及將讀取到的資訊封裝到指定類中。
using (var SqlConnection = new MySqlConnection(connectionStr)) { SqlConnection.Open(); var columsInfo = string.Format(@"select table_name,column_name,ordinal_position,is_nullable,data_type,character_maximum_length,column_key,column_comment from information_schema.COLUMNS where table_schema = '{0}' and table_name = '{1}'", dbschema, tablename); MySqlCommand mySqlCommand = new MySqlCommand(columsInfo, SqlConnection); MySqlDataReader dataReader = mySqlCommand.ExecuteReader(); List<ColumnInfo> sqlDatasList = new List<ColumnInfo>(); while (dataReader.Read()) { var columnInfo = new ColumnInfo() { TableName = dataReader[dataReader.GetName(0)].ToString(), Name = dataReader[dataReader.GetName(1)].ToString(), OrdinalPosition = StringExtension.GetValueOrNull<int>(dataReader[dataReader.GetName(2)].ToString()), IsNullable = dataReader[dataReader.GetName(3)].ToString(), DataType = dataReader[dataReader.GetName(4)].ToString(), CharacterMaximumLength = StringExtension.GetValueOrNull<int>(dataReader[dataReader.GetName(2)].ToString()), ColumnKey = dataReader[dataReader.GetName(6)].ToString(), ColumnComment = dataReader[dataReader.GetName(7)].ToString(), }; sqlDatasList.Add(columnInfo); } dataReader.Close(); SqlConnection.Close(); return sqlDatasList;
4、啟動後輸入表名、實體名、實體描述(並未儲存到資料庫中),再通過手動將其加入到專案中,諸如名稱空間及模組名稱都加入到了配置檔案中,方便配置,至少相對手動去一個個新增來講,減少了部分工作量,也達到了輔助的效果,但是要達到全面輔助,還得在進行繼續優化,針對其中的類等等,暫時沒有加入屬性,只放置了Id、Name等等,之後得考慮把資料庫中欄位也迴圈輸出到模板檔案中。
至此,依靠Razor引擎製作一個簡單的(算是減少了工作量)程式碼生成器初步完成了,年後繼續完善,加入豐富的功能,並移入到框架中作為提高生產力的手段。新年快樂~
倉庫地址:https://gitee.com/530521314/Partner.TreasureChest.git
2020-01-01,望技術有成後能回來看見自己的腳步