基於vs外掛的abp程式碼生成器

L_tommy發表於2019-05-16

  工作了這麼多年,一直都在小公司摸爬滾打,對於小公司而言,開發人員少,程式碼風格五花八門。要想用更少的人,更快的速度,開發更規範的程式碼,那自然離不開程式碼生成器。之前用過動軟的,也用過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

相關文章