基於vs插件的abp代碼生成器
工作了這么多年,一直都在小公司摸爬滾打,對於小公司而言,開發人員少,代碼風格五花八門。要想用更少的人,更快的速度,開發更規范的代碼,那自然離不開代碼生成器。之前用過動軟的,也用過T4,后面又接觸了力軟。相較而言,力軟的代碼生成做的體驗還是很不錯的(不是給他打廣告哈)。最近在看abp,發現要按他的規范來開發的話,工作量還是蠻大的,所以他們官方也開發了配套的代碼生成器,不過都要收費。國內這塊好像做的好點的就52abp了,還有個Magicodes.Admin。前者是類似於官方的做成了vs插件,還比較好用,后者是線上的,據說是生成后可以同步到git倉庫,咱也沒用過,所以也不清楚好不好用。前段時間稍微空閑點,就參考Magicodes.Admin和52abp搭了個框子,順便也研究了下基於vs插件的代碼生成器,abp的代碼生成器也可以做成力軟那樣的,只不過需要用戶先update-database數據庫而已,代碼生成部分原理都差不多,這里就不提了,這里主要是記錄下vs插件開發代碼生成器的過程。
先上下框子截圖:
開發過程:
新建VS插件項目
1、新建項目
這里我們要新建VSIX Project
2、建好項目后,右鍵添加新建項,這里我們選Custom Command
添加好了后,我們修改Command1Package.vsct這個文件:
這里改的是菜單顯示的文字,然后我們可以F5運行起來瞧瞧。F5運行后,會另外開啟一個vs,如下圖:
默認的菜單會被添加到“工具”這個菜單欄中,如下圖:
咱們要做代碼生成器,肯定不是希望把菜單加在這里的,那要怎么改呢? 還是剛才那個文件,具體位置在:
<Groups> <Group guid="guidCommand1PackageCmdSet" id="MyMenuGroup" priority="0x0600"> <Parent guid="guidSHLMainMenu" id="IDM_VS_MENU_TOOLS"/> </Group> </Groups>
關於這個id,幾個常用的有下面幾個:
IDM_VS_CTXT_SOLNNODE 是指的解決方案資源管理器里的解決方案 IDM_VS_CTXT_SOLNFOLDER 是指的解決方案資源管理器里的 解決方案里的文件夾,不是項目里的哈,這個文件夾是虛擬的,沒有實際的文件夾映射 IDM_VS_CTXT_PROJNODE 是指的解決方案資源管理器里的項目 IDM_VS_CTXT_FOLDERNODE 是指的解決方案資源管理器里的項目里的文件夾 IDM_VS_CTXT_ITEMNODE 是指的解決方案資源管理器里的項目里的項,就例如cs、js文件
我們這里要用的就是"IDM_VS_CTXT_ITEMNODE",改完后我們再F5運行下,這個時候我們要打開一個項目了。右鍵點擊瞧瞧(上面那個abp代碼生成器是我之前做的,忽略哈):
好了,要的就是這個效果,接下來就要開始做代碼生成的了。
代碼生成
代碼生成主要分為三個步驟,1、獲取所選文件以及當前項目基本信息。2、生成后端代碼。3、生成前端代碼
1、獲取所選文件以及當前項目基本信息
做VS插件,離不了DTE2這個類,具體的可參考:https://docs.microsoft.com/en-us/dotnet/api/envdte._dte?view=visualstudiosdk-2017
首先我們要獲取DTE2實例,我們打開Command1Package.cs這個類修改初始化方法:
protected override async Task InitializeAsync(CancellationToken cancellationToken, IProgress<ServiceProgressData> progress) { // When initialized asynchronously, the current thread may be a background thread at this point. // Do any initialization that requires the UI thread after switching to the UI thread. await this.JoinableTaskFactory.SwitchToMainThreadAsync(cancellationToken); DTE2 _dte = await GetServiceAsync(typeof(DTE)) as DTE2; await AbpCustomCommand.InitializeAsync(this, _dte); }
同時修改Command1.cs的初始化方法:
public static DTE2 _dte; /// <summary> /// Initializes the singleton instance of the command. /// </summary> /// <param name="package">Owner package, not null.</param> public static async Task InitializeAsync(AsyncPackage package, DTE2 dte) { _dte = dte; // Switch to the main thread - the call to AddCommand in Command1's constructor requires // the UI thread. await ThreadHelper.JoinableTaskFactory.SwitchToMainThreadAsync(package.DisposalToken); OleMenuCommandService commandService = await package.GetServiceAsync((typeof(IMenuCommandService))) as OleMenuCommandService; Instance = new Command1(package, commandService); }
獲取到了DTE2實例了,我們就可以開始獲取我們要的基本信息了,我們在Command1.cs類的Execute方法中加入下面代碼(注釋寫的都比較清楚,就不多寫了):
#region 獲取出基礎信息 //獲取當前點擊的類所在的項目 Project topProject = selectProjectItem.ContainingProject; //當前類在當前項目中的目錄結構 string dirPath = GetSelectFileDirPath(topProject, selectProjectItem); //當前類命名空間 string namespaceStr = selectProjectItem.FileCodeModel.CodeElements.OfType<CodeNamespace>().First().FullName; //當前項目根命名空間 string applicationStr = ""; if (!string.IsNullOrEmpty(namespaceStr)) { applicationStr = namespaceStr.Substring(0, namespaceStr.IndexOf(".")); } //當前類 CodeClass codeClass = GetClass(selectProjectItem.FileCodeModel.CodeElements); //當前項目類名 string className = codeClass.Name; //當前類中文名 [Display(Name = "供應商")] string classCnName = ""; //當前類說明 [Description("品牌信息")] string classDescription = ""; //獲取類的中文名稱和說明 foreach (CodeAttribute classAttribute in codeClass.Attributes) { switch (classAttribute.Name) { case "Display": if (!string.IsNullOrEmpty(classAttribute.Value)) { string displayStr = classAttribute.Value.Trim(); foreach (var displayValueStr in displayStr.Split(',')) { if (!string.IsNullOrEmpty(displayValueStr)) { if (displayValueStr.Split('=')[0].Trim() == "Name") { classCnName = displayValueStr.Split('=')[1].Trim().Replace("\"", ""); } } } } break; case "Description": classDescription = classAttribute.Value; break; } } //獲取當前解決方案里面的項目列表 List<ProjectItem> solutionProjectItems = GetSolutionProjects(_dte.Solution); #endregion
上面用到了幾個輔助方法:
#region 輔助方法 /// <summary> /// 獲取所有項目 /// </summary> /// <param name="projectItems"></param> /// <returns></returns> private IEnumerable<ProjectItem> GetProjects(ProjectItems projectItems) { foreach (EnvDTE.ProjectItem item in projectItems) { yield return item; if (item.SubProject != null) { foreach (EnvDTE.ProjectItem childItem in GetProjects(item.SubProject.ProjectItems)) if (childItem.Kind == EnvDTE.Constants.vsProjectItemKindSolutionItems) yield return childItem; } else { foreach (EnvDTE.ProjectItem childItem in GetProjects(item.ProjectItems)) if (childItem.Kind == EnvDTE.Constants.vsProjectItemKindSolutionItems) yield return childItem; } } } /// <summary> /// 獲取解決方案里面所有項目 /// </summary> /// <param name="solution"></param> /// <returns></returns> private List<ProjectItem> GetSolutionProjects(Solution solution) { List<ProjectItem> projectItemList = new List<ProjectItem>(); var projects = solution.Projects.OfType<Project>(); foreach (var project in projects) { var projectitems = GetProjects(project.ProjectItems); foreach (var projectItem in projectitems) { projectItemList.Add(projectItem); } } return projectItemList; } /// <summary> /// 獲取類 /// </summary> /// <param name="codeElements"></param> /// <returns></returns> private CodeClass GetClass(CodeElements codeElements) { var elements = codeElements.Cast<CodeElement>().ToList(); var result = elements.FirstOrDefault(codeElement => codeElement.Kind == vsCMElement.vsCMElementClass) as CodeClass; if (result != null) { return result; } foreach (var codeElement in elements) { result = GetClass(codeElement.Children); if (result != null) { return result; } } return null; } /// <summary> /// 獲取當前所選文件去除項目目錄后的文件夾結構 /// </summary> /// <param name="selectProjectItem"></param> /// <returns></returns> private string GetSelectFileDirPath(Project topProject, ProjectItem selectProjectItem) { string dirPath = ""; if (selectProjectItem != null) { //所選文件對應的路徑 string fileNames = selectProjectItem.FileNames[0]; string selectedFullName = fileNames.Substring(0, fileNames.LastIndexOf('\\')); //所選文件所在的項目 if (topProject != null) { //項目目錄 string projectFullName = topProject.FullName.Substring(0, topProject.FullName.LastIndexOf('\\')); //當前所選文件去除項目目錄后的文件夾結構 dirPath = selectedFullName.Replace(projectFullName, ""); } } return dirPath.Substring(1); } /// <summary> /// 添加文件到項目中 /// </summary> /// <param name="folder"></param> /// <param name="content"></param> /// <param name="fileName"></param> private void AddFileToProjectItem(ProjectItem folder, string content, string fileName) { try { string path = Path.GetTempPath(); Directory.CreateDirectory(path); string file = Path.Combine(path, fileName); File.WriteAllText(file, content, System.Text.Encoding.UTF8); try { folder.ProjectItems.AddFromFileCopy(file); } finally { File.Delete(file); } } catch (Exception ex) { } } /// <summary> /// 添加文件到指定目錄 /// </summary> /// <param name="directoryPathOrFullPath"></param> /// <param name="content"></param> /// <param name="fileName"></param> private void AddFileToDirectory(string directoryPathOrFullPath, string content, string fileName = "") { try { string file = string.IsNullOrEmpty(fileName) ? directoryPathOrFullPath : Path.Combine(directoryPathOrFullPath, fileName); File.WriteAllText(file, content, System.Text.Encoding.UTF8); } catch (Exception ex) { } } #endregion
2、生成后端代碼
具體代碼生成這里用到了razor引擎,我們先配置razor引擎:
private void InitRazorEngine() { var config = new TemplateServiceConfiguration { TemplateManager = new EmbeddedResourceTemplateManager(typeof(Template)) }; Engine.Razor = RazorEngineService.Create(config); }
然后在Command1.cs的構造函數里面初始化razor引擎。接着按照我們需要的項目結構來構建生成流程,具體如下:
//1.同級目錄添加 Authorization 文件夾 //2.往新增的 Authorization 文件夾中添加 xxxPermissions.cs 文件 //3.往新增的 Authorization 文件夾中添加 xxxAuthorizationProvider.cs 文件 //4.往當前項目根目錄下文件夾 Authorization 里面的AppAuthorizationProvider.cs類中的SetPermissions方法最后加入 SetxxxPermissions(pages); //5.往xxxxx.Application項目中增加當前所選文件所在的文件夾 //6.往第五步新增的文件夾中增加Dto目錄 //7.往第六步新增的Dto中增加CreateOrUpdatexxxInput.cs xxxEditDto.cs xxxListDto.cs GetxxxForEditOutput.cs GetxxxsInput.cs這五個文件 //8.編輯CustomDtoMapper.cs,添加映射 //9.往第五步新增的文件夾中增加 xxxAppService.cs和IxxxAppService.cs 類 //10.編輯DbContext
用razor引擎,自然少不了模板,這里就貼一個模板出來,其他的兄弟們自己查看源碼哈:
@using CodeBuilder.Models.TemplateModels @inherits RazorEngine.Templating.TemplateBase<CodeBuilder.Models.TemplateModels.ServiceFileModel> using System; using System.Collections.Generic; using System.Linq; using System.Linq.Dynamic.Core; using System.Text; using System.Threading.Tasks; using Abp.Application.Services.Dto; using @Model.Namespace.@(Model.DirName).Dto; using Abp.Domain.Repositories; using Abp.AutoMapper; using Microsoft.EntityFrameworkCore; using Abp.Authorization; using Abp.Linq.Extensions; using abpAngular.Authorization; using Abp.Collections.Extensions; using Abp.Extensions; namespace @Model.Namespace.@Model.DirName { /// <summary> /// @(Model.CnName)服務 /// </summary> [AbpAuthorize(@(Model.Name)Permissions.Node)] public class @(Model.Name)AppService : AbpFrameAppServiceBase, I@(Model.Name)AppService { private readonly IRepository<@(Model.Name), long> _repository; /// <summary> /// 構造函數 /// </summary> /// <param name="repository"></param> public @(Model.Name)AppService(IRepository<@(Model.Name), long> repository) { _repository = repository; } /// <summary> /// 拼接查詢條件 /// </summary> /// <param name="input"></param> /// <returns></returns> private IQueryable<@(Model.Name)> Create@(Model.Name)Query(Get@(Model.Name)sInput input) { var query = _repository.GetAll(); //此處寫自己的查詢條件 //query = query.WhereIf(!input.Filter.IsNullOrEmpty(), //p => p.Name.Contains(input.Filter) || p.DValue.Contains(input.Filter)); //query = query.WhereIf(input.DictionaryItemId.HasValue, p => p.DictionaryItemId == input.DictionaryItemId); return query; } /// <summary> /// 獲取更新@(Model.CnName)的數據 /// </summary> [AbpAuthorize(@(Model.Name)Permissions.Node)] public async Task<PagedResultDto<@(Model.Name)ListDto>> Get@(Model.Name)s(Get@(Model.Name)sInput input) { var query = Create@(Model.Name)Query(input); var count = await query.CountAsync(); var entityList = await query .OrderBy(input.Sorting).AsNoTracking() .PageBy(input) .ToListAsync(); var entityListDtos = entityList.MapTo<List<@(Model.Name)ListDto>>(); return new PagedResultDto<@(Model.Name)ListDto>(count, entityListDtos); } /// <summary> /// 獲取更新@(Model.CnName)的數據 /// </summary> [AbpAuthorize(@(Model.Name)Permissions.Create, @(Model.Name)Permissions.Edit)] public async Task<Get@(Model.Name)ForEditOutput> Get@(Model.Name)ForEdit(NullableIdDto<long> input) { var output = new Get@(Model.Name)ForEditOutput(); @(Model.Name)EditDto editDto; if (input.Id.HasValue) { var entity = await _repository.GetAsync(input.Id.Value); editDto = entity.MapTo<@(Model.Name)EditDto>(); } else { editDto = new @(Model.Name)EditDto(); } output.@(Model.Name) = editDto; return output; } /// <summary> /// 創建或編輯@(Model.CnName) /// </summary> [AbpAuthorize(@(Model.Name)Permissions.Create, @(Model.Name)Permissions.Edit)] public async Task CreateOrUpdate@(Model.Name)(CreateOrUpdate@(Model.Name)Input input) { if (!input.@(Model.Name).Id.HasValue) { await Create@(Model.Name)Async(input); } else { await Update@(Model.Name)Async(input); } } /// <summary> /// 新建 /// </summary> /// <param name="input"></param> /// <returns></returns> [AbpAuthorize(@(Model.Name)Permissions.Create)] public async Task<@(Model.Name)ListDto> Create@(Model.Name)Async(CreateOrUpdate@(Model.Name)Input input) { var entity = input.@(Model.Name).MapTo<@(Model.Name)>(); return (await _repository.InsertAsync(entity)).MapTo<@(Model.Name)ListDto>(); } /// <summary> /// 編輯 /// </summary> /// <param name="input"></param> /// <returns></returns> [AbpAuthorize(@(Model.Name)Permissions.Edit)] public async Task<@(Model.Name)ListDto> Update@(Model.Name)Async(CreateOrUpdate@(Model.Name)Input input) { var entity = input.@(Model.Name).MapTo<@(Model.Name)>(); return (await _repository.UpdateAsync(entity)).MapTo<@(Model.Name)ListDto>(); } /// <summary> /// 刪除@(Model.CnName) /// </summary> [AbpAuthorize(@(Model.Name)Permissions.Delete)] public async Task Delete(EntityDto<long> input) { await _repository.DeleteAsync(input.Id); } /// <summary> /// 批量刪除@(Model.CnName) /// </summary> [AbpAuthorize(@(Model.Name)Permissions.BatchDelete)] public async Task BatchDelete(List<long> input) { await _repository.DeleteAsync(a => input.Contains(a.Id)); } } }
接着我們開始生成,基本方法都差不多,我們貼一個新建和編輯的代碼瞧瞧:
新建:
/// <summary> /// 創建Permissions權限常量類 /// </summary> /// <param name="applicationStr">根命名空間</param> /// <param name="name">類名</param> /// <param name="authorizationFolder">父文件夾</param> private void CreatePermissionFile(string applicationStr, string name, ProjectItem authorizationFolder) { var model = new PermissionsFileModel() { Namespace = applicationStr, Name = name }; string content = Engine.Razor.RunCompile("PermissionsTemplate", typeof(PermissionsFileModel), model); string fileName = $"{name}Permissions.cs"; AddFileToProjectItem(authorizationFolder, content, fileName); }
編輯:
/// <summary> /// 添加權限 /// </summary> /// <param name="topProject"></param> /// <param name="className"></param> private void SetPermission(Project topProject, string className) { ProjectItem AppAuthorizationProviderProjectItem = _dte.Solution.FindProjectItem(topProject.FileName.Substring(0, topProject.FileName.LastIndexOf("\\")) + "\\Authorization\\AppAuthorizationProvider.cs"); if (AppAuthorizationProviderProjectItem != null) { CodeClass codeClass = GetClass(AppAuthorizationProviderProjectItem.FileCodeModel.CodeElements); var codeChilds = codeClass.Members; foreach (CodeElement codeChild in codeChilds) { if (codeChild.Kind == vsCMElement.vsCMElementFunction && codeChild.Name == "SetPermissions") { var insertCode = codeChild.GetEndPoint(vsCMPart.vsCMPartBody).CreateEditPoint(); insertCode.Insert(" Set" + className + "Permissions(pages);\r\n"); insertCode.Insert("\r\n"); } } AppAuthorizationProviderProjectItem.Save(); } }
其他的都自己查看源碼哈
3、生成前端代碼
前端生成流程如下:
//1 往app\\admin文件夾下面加xxx文件夾 //2 往新增的文件夾加xxx.component.html xxx.component.ts create-or-edit-xxx-modal.component.html create-or-edit-xxx-modal.component.ts這4個文件 //3 修改app\\admin\\admin.module.ts文件, import新增的組件 注入組件 //4 修改app\\admin\\admin-routing.module.ts文件 添加路由 //5 修改 app\\shared\\layout\\nav\\app-navigation.service.ts文件 添加菜單 //6 修改 shared\\service-proxies\\service-proxy.module.ts文件 提供服務
前端和后端的生成大部分都差不多,不過修改的因為咱們這是針對vs的插件,所以沒法編輯vscode里的文件,這里我用了笨辦法,對應要改的文件中加了特殊標識,類似於 // {#insert import code#},然后生成了代碼文件后,我們替換掉標識符,貼段代碼出來:
/// <summary> /// 注入服務 /// </summary> /// <param name="frontPath"></param> /// <param name="name"></param> private void AddProxy(string frontPath, string name) { string routesCode = "ApiServiceProxies."+ name + "ServiceProxy,\r\n"; routesCode += " // {#insert routes code#}\r\n"; string proxyFilePath = frontPath + "shared\\service-proxies\\service-proxy.module.ts"; string proxyContent = File.ReadAllText(proxyFilePath); proxyContent = proxyContent.Replace("// {#insert proxy code#}", routesCode); AddFileToDirectory(proxyFilePath, proxyContent); }
至此,代碼生成器基本功能就算是OK了,不過要達到完善水平,要做的事情還很多,這里列出幾點:
1、代碼封裝
2、生成進度條
3、異步提升生成效率
4、添加交互界面
5、根據實體類的字段類型生成對應的前端控件
6、還沒想好。。。
至於框子,要做的就更多了,現在就只是弄了個基本的,后面還考慮下面幾點:
1、完善文章模塊
2、文件存儲模塊(本地,七牛雲,阿里雲)
3、消息模塊
4、短信模塊
5、微信模塊
6、還沒想好。。。
這個項目最后的願景的能基於這個框子做幾套基礎的開源應用出來,比如基礎的商城、ERP、CRM等,DOTNET領域基礎開源應用太少了,2019年再不努力點,DOTNET后面的路就更難了,市場都沒有了,咱們在技術圈里自Hi也沒什么意義了,大家一起加油吧。
最近家里有些事情需要在家辦公,各位有要兼職的或者有項目的可以聊聊哇
Git倉庫
后端倉庫:https://gitee.com/uTu/abpFrame_Angular
前端倉庫:https://gitee.com/uTu/abpFrame_Angular_Front
代碼生成器倉庫:https://gitee.com/uTu/abpCodeBuilder
參考資料:
前端:https://www.cnblogs.com/FocusNet/p/10030749.html?tdsourcetag=s_pcqq_aiomsg
代碼生成器相關:https://github.com/wakuflair/ABPHelper
https://github.com/i542873057/SJNScaffolding
https://www.c-sharpcorner.com/article/visual-studio-extensibility-creating-your-first-visual-studio-vsix-package-d/
https://docs.microsoft.com/zh-cn/visualstudio/extensibility/extensibility-hello-world?view=vs-2017
https://docs.microsoft.com/en-us/dotnet/api/envdte._dte?view=visualstudiosdk-2017