使用VS 插件和 Roslyn 从项目实体类分别自动生成DTO 类 接口类 应用类
前言
项目使用的ABP vNext 开发,需要将 Entity 转换成Output, Input 等DTO,接着还要根据模块生成 IAppService 和 AppService,MapProfile 等等一系列的文件和方法,这些重复机械的工作,做下来还没正式写代码,就搞到见到 Entity 都头痛。于是想想,看看网上有没有生成 DTO 的工具,发现有 52ABP 代码生成器,它是做成 VS 的插件形式来工作的,很不幸的,不知道什么原因在 VS2022里面用不到,各种报错;可能年月太久了,到Github上面看到很多留言主理人都没有回复。但是给了我一个思路,要不自己做一个试试。
虽然是没做过 VS 插件,不过学习新知识要比重复机械的工作有趣得多,于是乎就开始上网找资料学习。网上关于 VS 插件的文章真不多,巨硬的官方教程简直就是简陋,不管再少再简陋,比起重复机械的工作,只要有挑战我都能够充满热情,直接开干。
先放最终效果图:
一开始我是使用标准的 VSIX 模板,也完成了,后来发现了写 VS 插件的一个利器 Extensibility Essentials 2022 ,是一个扩展包,在VS - 扩展 - 管理扩展 - 联机 里面,安装一下就好了
安装完之后,在新建 VSIX 项目的时候就会看到有 Community 结尾的项目模板:
如何使用 Extensibility Essentials
//官方方法 DTE2 dte2 = Package.GetGlobalService(typeof(DTE)) as DTE2; // Extensibility 直接调用 VS 这个关键字里面封装好的方法就可以
var fAllProjects = awaite VS.Solutions.GetAllProjectsAsync()
更多关于 Extensibility Essentials,请参考官方网站,另外强烈建议下载里面的 PDF 里面写得很详细,看完基本都懂。
接下来我们开始:
1.新建 VSIX 项目
- 选择 VSIX Project w/Command (Community) 的启动模板。
- 新建一个WPF的窗体
- Packge:不需要修改
1 [PackageRegistration(UseManagedResourcesOnly = true, AllowsBackgroundLoading = true)] 2 [InstalledProductRegistration(Vsix.Name, Vsix.Description, Vsix.Version)] 3 [ProvideMenuResource("Menus.ctmenu", 1)] 4 [Guid(PackageGuids.EntityGenerateString)] 5 public sealed class EntityGeneratePackage : ToolkitPackage 6 { 7 protected override async Task InitializeAsync(CancellationToken cancellationToken, IProgress<ServiceProgressData> progress) 8 { 9 //await (new MainWindowsControl()).ShowDialogAsync(); --- 这样写的话之后在第一次打开初始化的时候触发,可以打开,但因为这是单实例所以关闭窗口之后再打开不能在触发了 10 await this.RegisterCommandsAsync(); 11 12 } 13 }
- Command:添加触发窗体启动
[Command("7a47431e-f20c-4288-9f2b-a7cf847b0a8a", 0x0100)] internal sealed class Command1 : BaseCommand<Command1> { protected override async Task ExecuteAsync(OleMenuCmdEventArgs e) { await (new MainWindowsControl()).ShowDialogAsync(); } }
注意 :[Command("7a47431e-f20c-4288-9f2b-a7cf847b0a8a", 0x0100)] 里面要和 VSCT 文件里 的 GUID 和 Id 一一对应,否则不会触发。
- VSCT :这个文件管理插件放在 VS 哪个位置和图标,我这里是 选择文件夹 - 右键 的菜单里面
<?xml version="1.0" encoding="utf-8"?> <CommandTable xmlns="http://schemas.microsoft.com/VisualStudio/2005-10-18/CommandTable" xmlns:xs="http://www.w3.org/2001/XMLSchema"> <Extern href="stdidcmd.h"/> <Extern href="vsshlids.h"/> <Include href="KnownImageIds.vsct"/> <Include href="VSGlobals.vsct"/> <Commands package="EntityGenerate"> <Groups> <Group guid="EntityGenerate" id="MyMenuGroup" priority="0x0600"> <Parent guid="MainWindowRightClickCmdSet" id="CodeWindowRightClickMenu"/> </Group> </Groups> <Buttons> <Button guid="EntityGenerate" id="MainWindowCommandId" priority="0x0100" type="Button"> <Parent guid="EntityGenerate" id="MyMenuGroup" /> <Icon guid="ImageCatalogGuid" id="StatusInformation" /> <CommandFlag>IconIsMoniker</CommandFlag> <Strings> <ButtonText>生成Dto,Application</ButtonText> </Strings> </Button> </Buttons> </Commands> <Symbols> <GuidSymbol name="EntityGenerate" value="{7a47431e-f20c-4288-9f2b-a7cf847b0a8a}"> <IDSymbol name="MyMenuGroup" value="0x1020" /> <IDSymbol name="MainWindowCommandId" value="0x0100" /> </GuidSymbol> <GuidSymbol name="MainWindowRightClickCmdSet" value="{D309F791-903F-11D0-9EFC-00A0C911004F}"> <IDSymbol name="CodeWindowRightClickMenu" value="0x0431" /> </GuidSymbol> </Symbols> </CommandTable>
菜单效果:
更多关于 VSIX 插件的文章可以看看博友们写的文章。
2.使用 Roslyn
一开始想得很简单反射一下就好了,后面一想,这个只能获取到 Entity 类的文件....那么 Roslyn 这个大杀器就要出场了,Roslyn 我就不介绍了,就是微软出的用来自动分析代码的工具。
安装 Microsoft.CodeAnalysis.CSharp 这个包,200多M......
- 新建一个继承 CSharpSyntaxWalker 的类,里面有很多可以 override 的方法,每个方法对应的是获取节点的时机,把一个 Class 解释成 Tree ,然后不断的访问里面的 node
public class ClassCollector : CSharpSyntaxWalker { public readonly Dictionary<string, List<DtoPropertyTemplate>> Models = new Dictionary<string, List<DtoPropertyTemplate>>(); public readonly List<UsingDirectiveSyntax> Usings = new List<UsingDirectiveSyntax>(); public override void VisitUsingDirective(UsingDirectiveSyntax node) { this.Usings.Add(node); } public override void VisitPropertyDeclaration(PropertyDeclarationSyntax node) { var classnode = node.Parent as ClassDeclarationSyntax; if (!Models.ContainsKey(classnode.Identifier.ValueText)) { Models.Add(classnode.Identifier.ValueText, new List<DtoPropertyTemplate>()); } Models[classnode.Identifier.ValueText].Add(new DtoPropertyTemplate { CSharpType = node.ChildNodesAndTokens().Where(p => p.Kind().ToString()=="NullableType"||p.Kind().ToString()=="PredefinedType"||p.Kind().ToString()=="IdentifierName").FirstOrDefault().ToFullString(), Name = node.Identifier.ValueText, Attributes = node.AttributeLists.ToArray(), }); } }
上面代码把 .cs 文件里面的 Namespace,Using, ClassName,Property Name ,Property Type ,Attributes 等加载出来,这样就可以得到一个我们需要的 Class 的全部信息。
- 生成 DTO
1 public void GenerationDto(string code, DtoTemplate dtoTemplate) 2 { 3 var fCSharpSyntaxTree = CSharpSyntaxTree.ParseText(code); 4 //SyntaxTree的根root 5 var fCodeSyntaxTreeRoot = (CompilationUnitSyntax)fCSharpSyntaxTree.GetRoot(); 6 //member 7 var fTreeRootFirstmember = fCodeSyntaxTreeRoot.Members[0]; 8 //命名空间Namespace 9 var fCodeNamespace = (NamespaceDeclarationSyntax)fTreeRootFirstmember; 10 //类 class 11 var fProgramDeclaration = (ClassDeclarationSyntax)fCodeNamespace.Members[0]; 12 13 #region 生成Class 14 15 var ClassCollector = new ClassCollector(); 16 17 ClassCollector.Visit(fProgramDeclaration); 18 19 foreach (var fClass in ClassCollector.Models) 20 { 21 string fClassName = $"{fClass.Key.Replace("_", "")}{dtoTemplate.DtoName}"; 22 // Create a namespace: (namespace CodeGenerationSample) 23 var fNamespace = SyntaxFactory.NamespaceDeclaration(SyntaxFactory.ParseName($"{dtoTemplate.Namespace}.{dtoTemplate.ApplicationName}.Contracts")).NormalizeWhitespace(); 24 25 // Add System using statement: (using System) 26 fNamespace = fNamespace.AddUsings(SyntaxFactory.UsingDirective(SyntaxFactory.ParseName("System")), 27 SyntaxFactory.UsingDirective(SyntaxFactory.ParseName($"System.ComponentModel.DataAnnotations"))); 28 29 // Create a class: (class Order) 30 var fClassDeclaration = SyntaxFactory.ClassDeclaration(fClassName); 31 32 // Add the public modifier: (public class Order) 33 fClassDeclaration = fClassDeclaration.AddModifiers(SyntaxFactory.Token(SyntaxKind.PublicKeyword)); 34 35 // Inherit BaseEntity<T> and implement IHaveIdentity: (public class Order : BaseEntity<T>, IHaveIdentity) 36 fClassDeclaration = fClassDeclaration.AddBaseListTypes(SyntaxFactory.SimpleBaseType(SyntaxFactory.ParseTypeName(dtoTemplate.BaseClass))); 37 38 foreach (var fProperties in fClass.Value) 39 { 40 // Create a Property: (public int Quantity { get; set; }) 41 var fPropertyDeclaration = SyntaxFactory.PropertyDeclaration(SyntaxFactory.ParseTypeName(fProperties.CSharpType), fProperties.Name) 42 .AddModifiers(SyntaxFactory.Token(SyntaxKind.PublicKeyword)) 43 .AddAccessorListAccessors( 44 SyntaxFactory.AccessorDeclaration(SyntaxKind.GetAccessorDeclaration).WithSemicolonToken(SyntaxFactory.Token(SyntaxKind.SemicolonToken)), 45 SyntaxFactory.AccessorDeclaration(SyntaxKind.SetAccessorDeclaration).WithSemicolonToken(SyntaxFactory.Token(SyntaxKind.SemicolonToken))) 46 .AddAttributeLists(fProperties.Attributes); 47 48 fClassDeclaration = fClassDeclaration.AddMembers(fPropertyDeclaration); 49 } 50 51 fNamespace = fNamespace.AddMembers(fClassDeclaration); 52 53 var fNewClass = fNamespace.NormalizeWhitespace().ToFullString(); 54 55 FileHelper.WriteTo($"{dtoTemplate.TragetPath}\\{ApplicationName}\\Dto", $"{fClassName}.cs", fNewClass); 56 } 57 #endregion 58 }
- 生成 Application
1 public void GenerationApplication(List<string> entities) 2 { 3 #region ApplicationService 4 // Create a namespace: (namespace CodeGenerationSample) 5 var fNamespace = SyntaxFactory.NamespaceDeclaration(SyntaxFactory.ParseName($"{txbNamespace.Text}.{ApplicationName}")).NormalizeWhitespace(); 6 7 // Add System using statement: (using System) 8 fNamespace = fNamespace.AddUsings(SyntaxFactory.UsingDirective(SyntaxFactory.ParseName("System")), 9 SyntaxFactory.UsingDirective(SyntaxFactory.ParseName("System.Threading.Tasks")), 10 SyntaxFactory.UsingDirective(SyntaxFactory.ParseName($"{txbNamespace.Text}.{ApplicationName}.Contracts"))); 11 12 // Create a class: (class Order) 13 var fClassDeclaration = SyntaxFactory.ClassDeclaration($"{ApplicationName}AppService"); 14 // Add the public modifier: (public class Order) 15 fClassDeclaration = fClassDeclaration.AddModifiers(SyntaxFactory.Token(SyntaxKind.PublicKeyword)); 16 17 // Inherit BaseEntity<T> and implement IHaveIdentity: (public class Order : BaseEntity<T>, IHaveIdentity) 18 fClassDeclaration = fClassDeclaration.AddBaseListTypes( 19 SyntaxFactory.SimpleBaseType(SyntaxFactory.ParseTypeName(txbAbpApplicationBase.Text)), 20 SyntaxFactory.SimpleBaseType(SyntaxFactory.ParseTypeName($"I{ApplicationName}AppService"))); 21 22 // Create a stament with the body of a method. 23 var fConstructorStatement = SyntaxFactory.ParseStatement(""); 24 25 // 构造函数 26 var fConstructorDeclaration = SyntaxFactory.MethodDeclaration(SyntaxFactory.ParseTypeName(""), $"{ApplicationName}AppService") 27 .AddModifiers(SyntaxFactory.Token(SyntaxKind.PublicKeyword)) 28 .WithBody(SyntaxFactory.Block(fConstructorStatement)); 29 30 fClassDeclaration = fClassDeclaration.AddMembers(fConstructorDeclaration); 31 32 // Create a stament with the body of a method. 33 var fDefaultStatement = SyntaxFactory.ParseStatement("throw new System.NotImplementedException();"); 34 foreach (var entity in entities) 35 { 36 var fEntity = entity.Replace("_", ""); 37 //新增 38 var fFuntionCreateDeclaration = SyntaxFactory.MethodDeclaration(SyntaxFactory.ParseTypeName($"async Task<v1Result>"), $"Create{fEntity}Async") 39 .AddModifiers(SyntaxFactory.Token(SyntaxKind.PublicKeyword)) 40 .AddParameterListParameters(SyntaxFactory.Parameter(SyntaxFactory.Identifier($"{fEntity.ToCamelCase()}Create")).WithType(SyntaxFactory.ParseTypeName($"{fEntity}Create"))) 41 .WithBody(SyntaxFactory.Block(fDefaultStatement)); 42 43 //修改 44 var fFuntionEditDeclaration = SyntaxFactory.MethodDeclaration(SyntaxFactory.ParseTypeName("async Task<v1Result>"), $"Update{fEntity}Async") 45 .AddModifiers(SyntaxFactory.Token(SyntaxKind.PublicKeyword)) 46 .AddParameterListParameters(SyntaxFactory.Parameter(SyntaxFactory.Identifier($"{fEntity.ToCamelCase()}Update")).WithType(SyntaxFactory.ParseTypeName($"{fEntity}Update"))) 47 .WithBody(SyntaxFactory.Block(fDefaultStatement)); 48 49 //删除 50 var fFuntionDeleteDeclaration = SyntaxFactory.MethodDeclaration(SyntaxFactory.ParseTypeName("async Task<v1Result>"), $"Delete{fEntity}Async") 51 .AddModifiers(SyntaxFactory.Token(SyntaxKind.PublicKeyword)) 52 .AddParameterListParameters(SyntaxFactory.Parameter(SyntaxFactory.Identifier($"{fEntity.ToCamelCase()}Id")).WithType(SyntaxFactory.ParseTypeName($"Guid"))) 53 .WithBody(SyntaxFactory.Block(fDefaultStatement)); 54 55 //查询一个 56 var fFuntionGetDeclaration = SyntaxFactory.MethodDeclaration(SyntaxFactory.ParseTypeName($"async Task<{fEntity}Output>"), $"Get{fEntity}Async") 57 .AddModifiers(SyntaxFactory.Token(SyntaxKind.PublicKeyword)) 58 .AddParameterListParameters(SyntaxFactory.Parameter(SyntaxFactory.Identifier($"{fEntity.ToCamelCase()}Id")).WithType(SyntaxFactory.ParseTypeName($"Guid"))) 59 .WithBody(SyntaxFactory.Block(fDefaultStatement)); 60 61 //分页查询 62 var fFuntionPagedListDeclaration = SyntaxFactory.MethodDeclaration(SyntaxFactory.ParseTypeName($"async Task<PagedList<{fEntity}Output>>"), $"PagedList{fEntity}Async") 63 .AddModifiers(SyntaxFactory.Token(SyntaxKind.PublicKeyword)) 64 .AddParameterListParameters(SyntaxFactory.Parameter(SyntaxFactory.Identifier($"{fEntity.ToCamelCase()}Query")).WithType(SyntaxFactory.ParseTypeName($"{fEntity}Query"))) 65 .WithBody(SyntaxFactory.Block(fDefaultStatement)); 66 67 fClassDeclaration = fClassDeclaration.AddMembers(fFuntionCreateDeclaration, fFuntionEditDeclaration, fFuntionDeleteDeclaration, fFuntionGetDeclaration, fFuntionPagedListDeclaration); 68 } 69 70 fNamespace = fNamespace.AddMembers(fClassDeclaration); 71 72 var fNewClass = fNamespace.NormalizeWhitespace().ToFullString(); 73 74 FileHelper.WriteTo($"{TargetAppServicePath}\\{ApplicationName}", $"{ApplicationName}AppService.cs", fNewClass); 75 #endregion 76 }
- 生成 MappProfile
public void GenerationApplicationMapProfile(List<string> entities) { #region ApplicationService // Create a namespace: (namespace CodeGenerationSample) var fNamespace = SyntaxFactory.NamespaceDeclaration(SyntaxFactory.ParseName($"{txbNamespace.Text}.{ApplicationName}")).NormalizeWhitespace(); // Add System using statement: (using System) fNamespace = fNamespace.AddUsings(SyntaxFactory.UsingDirective(SyntaxFactory.ParseName("AutoMapper")), SyntaxFactory.UsingDirective(SyntaxFactory.ParseName($"{txbNamespace.Text}.data.{ApplicationName}")), SyntaxFactory.UsingDirective(SyntaxFactory.ParseName($"{txbNamespace.Text}.{ApplicationName}.Contracts"))); // Create a class: (class Order) var fClassDeclaration = SyntaxFactory.ClassDeclaration($"{ApplicationName}MapperProfile"); // Add the public modifier: (public class Order) fClassDeclaration = fClassDeclaration.AddModifiers(SyntaxFactory.Token(SyntaxKind.PublicKeyword)); // Inherit BaseEntity<T> and implement IHaveIdentity: (public class Order : BaseEntity<T>, IHaveIdentity) fClassDeclaration = fClassDeclaration.AddBaseListTypes(SyntaxFactory.SimpleBaseType(SyntaxFactory.ParseTypeName($"Profile"))); // 构造函数 var fConstructorDeclaration = SyntaxFactory.MethodDeclaration(SyntaxFactory.ParseTypeName(""), $"{ApplicationName}MapperProfile") .AddModifiers(SyntaxFactory.Token(SyntaxKind.PublicKeyword)); foreach (var entity in entities) { //Entity to Output var fOutputSyntax = SyntaxFactory.ParseStatement($"CreateMap<{entity}, {entity.Replace("_", "")}Output>();"); //Create to Entity var fCreateSyntax = SyntaxFactory.ParseStatement($"CreateMap<{entity.Replace("_", "")}Create, {entity}>();"); //Update to Entity var fUpdateSyntax = SyntaxFactory.ParseStatement($"CreateMap<{entity.Replace("_", "")}Update, {entity}>().ForMember(client => client.Id, opt => opt.Ignore());"); fConstructorDeclaration = fConstructorDeclaration.AddBodyStatements(fOutputSyntax, fCreateSyntax, fUpdateSyntax); } fClassDeclaration = fClassDeclaration.AddMembers(fConstructorDeclaration); fNamespace = fNamespace.AddMembers(fClassDeclaration); var fNewClass = fNamespace.NormalizeWhitespace().ToFullString(); FileHelper.WriteTo($"{TargetAppServicePath}\\{ApplicationName}", $"{ApplicationName}MapperProfile.cs", fNewClass); #endregion }
- 生成 IAppService,标准的增/删/改/查
1 public void GenerationApplicationContract(List<string> entities) 2 { 3 #region ApplicationServiceContracts 4 // Create a namespace: (namespace CodeGenerationSample) 5 var fNamespace = SyntaxFactory.NamespaceDeclaration(SyntaxFactory.ParseName($"{txbNamespace.Text}.{ApplicationName}.Contracts")).NormalizeWhitespace(); 6 7 // Add System using statement: (using System) System.Threading.Tasks 8 fNamespace = fNamespace.AddUsings(SyntaxFactory.UsingDirective(SyntaxFactory.ParseName("System")), 9 SyntaxFactory.UsingDirective(SyntaxFactory.ParseName("System.Threading.Tasks"))); 10 11 // Create a class: (class Order) 12 var fInterfaceDeclaration = SyntaxFactory.InterfaceDeclaration($"I{ApplicationName}AppService"); 13 // Add the public modifier: (public class Order) 14 fInterfaceDeclaration = fInterfaceDeclaration.AddModifiers(SyntaxFactory.Token(SyntaxKind.PublicKeyword)); 15 16 foreach (var entity in entities) 17 { 18 var fEntity = entity.Replace("_", ""); 19 //新增 20 var fInterfaceCreateDeclaration = SyntaxFactory.VariableDeclaration(SyntaxFactory.ParseTypeName($"Task<v1Result> Create{fEntity}Async({fEntity}Create {fEntity.ToCamelCase()}Create)")); 21 var fFieldCreateDeclaration = SyntaxFactory.FieldDeclaration(fInterfaceCreateDeclaration); 22 23 //修改 24 var fInterfaceEditDeclaration = SyntaxFactory.VariableDeclaration(SyntaxFactory.ParseTypeName($"Task<v1Result> Update{fEntity}Async({fEntity}Update {fEntity.ToCamelCase()}Update)")); 25 var fFieldEditDeclaration = SyntaxFactory.FieldDeclaration(fInterfaceEditDeclaration); 26 27 //删除 28 var fInterfaceDeleteDeclaration = SyntaxFactory.VariableDeclaration(SyntaxFactory.ParseTypeName($"Task<v1Result> Delete{fEntity}Async(Guid {fEntity.ToCamelCase()}Id)")); 29 var fFieldDeleteDeclaration = SyntaxFactory.FieldDeclaration(fInterfaceDeleteDeclaration); 30 31 //查询一个 32 var fInterfaceGetDeclaration = SyntaxFactory.VariableDeclaration(SyntaxFactory.ParseTypeName($"Task<{fEntity}Output> Get{fEntity}Async(Guid {fEntity.ToCamelCase()}Id)")); 33 var fFieldGetDeclaration = SyntaxFactory.FieldDeclaration(fInterfaceGetDeclaration); 34 35 //分页查询 36 var fInterfacePagedListDeclaration = SyntaxFactory.VariableDeclaration(SyntaxFactory.ParseTypeName($"Task<PagedList<{fEntity}Output>> PagedList{fEntity}Async({fEntity}Query {fEntity.ToCamelCase()}Query)")); 37 var fFieldPagedListDeclaration = SyntaxFactory.FieldDeclaration(fInterfacePagedListDeclaration); 38 39 fInterfaceDeclaration = fInterfaceDeclaration.AddMembers(fFieldCreateDeclaration, fFieldEditDeclaration, fFieldDeleteDeclaration, fFieldGetDeclaration, fFieldPagedListDeclaration); 40 } 41 42 fNamespace = fNamespace.AddMembers(fInterfaceDeclaration); 43 44 var fNewClass = fNamespace.NormalizeWhitespace().ToFullString(); 45 46 FileHelper.WriteTo($"{TargetContractsPath}\\{ApplicationName}", $"I{ApplicationName}AppService.cs", fNewClass); 47 #endregion 48 }
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· DeepSeek 开源周回顾「GitHub 热点速览」
· 记一次.NET内存居高不下排查解决与启示
· 物流快递公司核心技术能力-地址解析分单基础技术分享
· .NET 10首个预览版发布:重大改进与新特性概览!
· .NET10 - 预览版1新功能体验(一)