(给DotNet加星标,提升.Net技能)
转自:L_tommycnblogs.com/L_tommy/p/10872389.html 工作了这么多年,一直都在小公司摸爬滚打,对于小公司而言,开发人员少,代码风格五花八门。要想用更少的人,更快的速度,开发更规范的代码,那自然离不开代码生成器。之前用过动软的,也用过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,如下图:
默认的菜单会被添加到“工具”这个菜单栏中,如下图:
咱们要做代码生成器,肯定不是希望把菜单加在这里的,那要怎么改呢? 还是刚才那个文件,具体位置在:
关于这个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 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;/// /// Initializes the singleton instance of the command./// /// Owner package, not null.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().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 solutionProjectItems = GetSolutionProjects(_dte.Solution);#endregion
复制代码 上面用到了几个辅助方法:
- #region 辅助方法 /// /// 获取所有项目 /// /// /// private IEnumerable 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; } } } /// /// 获取解决方案里面所有项目 /// /// /// private List GetSolutionProjects(Solution solution) { List projectItemList = new List(); var projects = solution.Projects.OfType(); foreach (var project in projects) { var projectitems = GetProjects(project.ProjectItems) foreach (var projectItem in projectitems) { projectItemList.Add(projectItem); } } return projectItemList; } /// /// 获取类 /// /// /// private CodeClass GetClass(CodeElements codeElements) { var elements = codeElements.Cast().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; } /// /// 获取当前所选文件去除项目目录后的文件夹结构 /// /// /// 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); } /// /// 添加文件到项目中 /// /// /// /// 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) { } } /// /// 添加文件到指定目录 /// /// /// /// 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.TemplateBaseusing 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{ /// /// @(Model.CnName)服务 /// [AbpAuthorize(@(Model.Name)Permissions.Node)] public class @(Model.Name)AppService : AbpFrameAppServiceBase, I@(Model.Name)AppService { private readonly IRepository _repository; /// /// 构造函数 /// /// public @(Model.Name)AppService(IRepository repository) { _repository = repository; } /// /// 拼接查询条件 /// /// /// private IQueryable 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; } /// /// 获取更新@(Model.CnName)的数据 /// [AbpAuthorize(@(Model.Name)Permissions.Node)] public async Task 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(); return new PagedResultDto(count, entityListDtos); } /// /// 获取更新@(Model.CnName)的数据 /// [AbpAuthorize(@(Model.Name)Permissions.Create, @(Model.Name)Permissions.Edit)] public async Task Get@(Model.Name)ForEdit(NullableIdDto 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(); } else { editDto = new @(Model.Name)EditDto(); } output.@(Model.Name) = editDto; return output; } /// /// 创建或编辑@(Model.CnName) /// [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); } } /// /// 新建 /// /// /// [AbpAuthorize(@(Model.Name)Permissions.Create)] public async Task Create@(Model.Name)Async(CreateOrUpdate@(Model.Name)Input input) { var entity = input.@(Model.Name).MapTo(); return (await _repository.InsertAsync(entity)).MapTo(); } /// /// 编辑 /// /// /// [AbpAuthorize(@(Model.Name)Permissions.Edit)] public async Task Update@(Model.Name)Async(CreateOrUpdate@(Model.Name)Input input) { var entity = input.@(Model.Name).MapTo(); return (await _repository.UpdateAsync(entity)).MapTo(); } /// /// 删除@(Model.CnName) /// [AbpAuthorize(@(Model.Name)Permissions.Delete)] public async Task Delete(EntityDto input) { await _repository.DeleteAsync(input.Id); } /// /// 批量删除@(Model.CnName) /// [AbpAuthorize(@(Model.Name)Permissions.BatchDelete)] public async Task BatchDelete(List input) { await _repository.DeleteAsync(a => input.Contains(a.Id)); } }}
复制代码 接着我们开始生成,基本方法都差不多,我们贴一个新建和编辑的代码瞧瞧:
新建
- /// /// 创建Permissions权限常量类/// /// 根命名空间/// 类名/// 父文件夹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);}
复制代码 编辑
- /// /// 添加权限/// /// /// 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#},然后生成了代码文件后,我们替换掉标识符,贴段代码出来:
- /// /// 注入服务/// /// /// 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
推荐阅读
(点击标题可跳转阅读)
.NET Core中依赖注入服务使用总结
ASP.NET Core MVC 模块化
.NET Core 3.0 可回收程序集加载上下文
看完本文有收获?请转发分享给更多人
关注「DotNet」加星标,提升.Net技能
好文章,我在看
|
|