工作了這麼多年,一直都在小公司摸爬滾打,對于小公司而言,開發人員少,代碼風格五花八門。要想用更少的人,更快的速度,開發更規範的代碼,那自然離不開代碼生成器。之前用過動軟的,也用過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 @[email protected]
{
/// <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、微信子產品
這個項目最後的願景的能基于這個框子做幾套基礎的開源應用出來,比如基礎的商城、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