.NET源码生成器开发指南:提升C#开发效率

发布时间:2026/10/28 23:37:26
.NET源码生成器开发指南:提升C#开发效率 1. .NET源码生成器开发背景与核心价值在.NET生态中源码生成器(Source Generator)正逐渐成为提升开发效率的利器。它能在编译期间动态生成C#代码与手动编写的代码无缝融合。这种技术特别适合解决重复性编码问题比如DTO转换、API客户端生成等场景。我最近在一个电商平台项目中就用源码生成器自动生成了200多个实体类的GraphQL类型定义节省了近两周的手动编码时间。partial类(部分类)是这项技术的黄金搭档。它允许我们将一个类的定义分散在多个文件中源码生成器生成的代码可以作为一个独立的部分类文件存在。这种范式既保持了代码整洁又避免了生成代码与手写代码的冲突。想象一下你有一个User类手动编写的业务逻辑放在User.cs而自动生成的序列化代码放在User.g.cs两者通过partial机制在编译时合并就像拼图一样完美契合。NuGet打包则是分享源码生成器的最佳方式。通过NuGet包我们可以将生成器分发给团队其他成员或开源社区使用者只需安装包就能获得代码生成能力无需关心实现细节。这比直接复制源代码更规范也便于版本管理。最近帮一个金融客户打包的报文生成器通过NuGet发布后团队协作效率提升了40%。2. 源码生成器开发环境准备2.1 开发工具选型工欲善其事必先利其器。我习惯使用Visual Studio 2022进行源码生成器开发它提供了最完整的.NET开发体验。特别是17.3版本后对源码生成器的调试支持更加完善。如果更喜欢轻量级环境VSCode配合C#插件也是不错的选择但需要手动配置tasks.json来支持生成器调试。项目模板选择很关键。推荐使用dotnet new classlib创建类库项目然后修改项目文件为SDK风格。这是我常用的项目文件配置Project SdkMicrosoft.NET.Sdk PropertyGroup TargetFrameworknetstandard2.0/TargetFramework LangVersion9.0/LangVersion EnforceExtendedAnalyzerRulestrue/EnforceExtendedAnalyzerRules /PropertyGroup ItemGroup PackageReference IncludeMicrosoft.CodeAnalysis.CSharp Version4.3.1 PrivateAssetsall / PackageReference IncludeMicrosoft.CodeAnalysis.Analyzers Version3.3.3 PrivateAssetsall / /ItemGroup /Project注意几个关键点TargetFramework设为netstandard2.0确保最大兼容性EnforceExtendedAnalyzerRules启用更严格的分析器规则分析器包使用PrivateAssetsall避免传递依赖2.2 项目结构规划良好的项目结构能提升代码可维护性。我通常采用这样的目录结构/src /MyGenerator # 生成器主项目 /Models # 数据模型 /Templates # 代码模板 /Utilities # 工具类 MyGenerator.cs # 生成器主类 /MyGenerator.Tests # 单元测试项目在Templates文件夹中我会存放Razor模板或字符串模板用于代码生成。对于复杂模板Razor引擎是不错的选择虽然需要引入Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation包但模板可读性更好。简单场景下直接使用字符串插值或StringBuilder更轻量。3. 核心生成器实现详解3.1 ISourceGenerator接口实现每个源码生成器都必须实现ISourceGenerator接口。这是我的典型实现骨架[Generator] public class MySourceGenerator : ISourceGenerator { public void Initialize(GeneratorInitializationContext context) { // 注册语法接收器 context.RegisterForSyntaxNotifications(() new MySyntaxReceiver()); } public void Execute(GeneratorExecutionContext context) { if (!(context.SyntaxContextReceiver is MySyntaxReceiver receiver)) return; // 1. 收集类型信息 var targetTypes GetTargetTypes(context, receiver); // 2. 生成源代码 foreach (var type in targetTypes) { var sourceCode GenerateSource(type); context.AddSource(${type.Name}.g.cs, SourceText.From(sourceCode, Encoding.UTF8)); } } }关键点说明[Generator]特性标记这是源码生成器Initialize方法中注册语法接收器收集编译信息Execute方法是生成核心通过context.AddSource输出代码3.2 语法分析与类型收集语法接收器(Syntax Receiver)是生成器的眼睛用于在编译过程中收集我们需要处理的类型信息。这是我常用的增强版接收器class MySyntaxReceiver : ISyntaxReceiver { public ListClassDeclarationSyntax CandidateClasses { get; } new(); public void OnVisitSyntaxNode(SyntaxNode syntaxNode) { if (syntaxNode is ClassDeclarationSyntax classDecl classDecl.Modifiers.Any(m m.IsKind(SyntaxKind.PartialKeyword))) { CandidateClasses.Add(classDecl); } } }这个接收器会收集所有partial类定义。在实际项目中我通常会添加更多过滤条件比如带有特定特性的类继承特定基类的类位于特定命名空间的类收集到语法节点后还需要获取完整的语义模型信息INamedTypeSymbol GetTypeSymbol(GeneratorExecutionContext context, ClassDeclarationSyntax classDecl) { var model context.Compilation.GetSemanticModel(classDecl.SyntaxTree); return model.GetDeclaredSymbol(classDecl) as INamedTypeSymbol; }3.3 代码生成策略代码生成有多种实现方式根据复杂度不同我通常分三个层级字符串拼接适合简单场景string GenerateSimpleClass(string className) { return $ namespace Generated {{ public partial class {className} {{ public string GeneratedProperty {{ get; set; }} }} }}; }模板引擎中等复杂度string GenerateWithTemplate(TypeInfo type) { var template public partial class Model.TypeName { foreach(var prop in Model.Properties) { public prop.Type prop.Name { get; set; } } }; return Engine.Razor.RunCompile(template, type); }SyntaxFactory完全控制语法树CompilationUnitSyntax CreateCompilationUnit(string className) { return SyntaxFactory.CompilationUnit() .AddMembers( SyntaxFactory.NamespaceDeclaration(SyntaxFactory.ParseName(Generated)) .AddMembers( SyntaxFactory.ClassDeclaration(className) .AddModifiers(SyntaxFactory.Token(SyntaxKind.PublicKeyword)) .AddMembers( SyntaxFactory.PropertyDeclaration( SyntaxFactory.ParseTypeName(string), GeneratedProperty) .AddAccessorListAccessors( SyntaxFactory.AccessorDeclaration(SyntaxKind.GetAccessorDeclaration) .WithSemicolonToken(SyntaxFactory.Token(SyntaxKind.SemicolonToken)), SyntaxFactory.AccessorDeclaration(SyntaxKind.SetAccessorDeclaration) .WithSemicolonToken(SyntaxFactory.Token(SyntaxKind.SemicolonToken)) ) ) ) ); }在最近的一个ORM项目中我混合使用了这三种方式用SyntaxFactory构建基础结构模板引擎生成常规属性字符串拼接处理特殊场景取得了很好的灵活性和可维护性平衡。4. NuGet打包与分发策略4.1 项目配置优化正确的项目配置是打包成功的前提。这是我的典型配置Project SdkMicrosoft.NET.Sdk PropertyGroup TargetFrameworknetstandard2.0/TargetFramework IsPackabletrue/IsPackable PackageIdMy.Awesome.Generator/PackageId Version1.0.0/Version IncludeBuildOutputfalse/IncludeBuildOutput DevelopmentDependencytrue/DevelopmentDependency NoPackageAnalysistrue/NoPackageAnalysis /PropertyGroup ItemGroup None Include$(OutputPath)\$(AssemblyName).dll Packtrue PackagePathanalyzers/dotnet/cs Visiblefalse / /ItemGroup /Project关键配置解析IncludeBuildOutputfalse避免将生成器本身作为常规程序集打包DevelopmentDependencytrue标记为开发依赖将DLL放入analyzers/dotnet/cs目录这是源码生成器的标准位置4.2 多目标框架支持为了扩大兼容范围我通常会支持多目标框架TargetFrameworksnetstandard2.0;netcoreapp3.1;net5.0;net6.0/TargetFrameworks同时需要在代码中使用条件编译#if NETSTANDARD2_0 // 兼容性代码 #elif NET5_0_OR_GREATER // 使用新特性 #endif4.3 依赖管理依赖处理需要特别注意分析器依赖必须设为PrivateAssetsall如果生成器依赖第三方库需要仔细考虑是否要传递这些依赖ItemGroup PackageReference IncludeNewtonsoft.Json Version13.0.1 PrivateAssetsall / PackageReference IncludeMicrosoft.CodeAnalysis.CSharp Version4.3.1 PrivateAssetsall / /ItemGroup4.4 符号包与源码链接为了更好的调试体验我会配置符号包和源码链接PropertyGroup PublishRepositoryUrltrue/PublishRepositoryUrl EmbedUntrackedSourcestrue/EmbedUntrackedSources IncludeSymbolstrue/IncludeSymbols SymbolPackageFormatsnupkg/SymbolPackageFormat /PropertyGroup ItemGroup PackageReference IncludeMicrosoft.SourceLink.GitHub Version1.1.1 PrivateAssetsall / /ItemGroup这样使用者调试时能看到生成器的源码极大简化问题排查。5. 高级应用场景与性能优化5.1 增量生成策略大规模项目中增量生成能显著提升性能。.NET 6引入了IncrementalGenerator这是我在当前项目中的实现[Generator] public class MyIncrementalGenerator : IIncrementalGenerator { public void Initialize(IncrementalGeneratorInitializationContext context) { var provider context.SyntaxProvider .CreateSyntaxProvider( predicate: (node, _) node is ClassDeclarationSyntax c c.Modifiers.Any(m m.IsKind(SyntaxKind.PartialKeyword)), transform: (ctx, _) (ClassDeclarationSyntax)ctx.Node) .Where(c c ! null); context.RegisterSourceOutput(provider, (spc, syntax) { var source GenerateSource(syntax); spc.AddSource(${syntax.Identifier.Text}.g.cs, source); }); } }实测在一个包含2000个类的项目中增量生成将编译时间从28秒降到了9秒。5.2 跨项目生成策略当需要在一个解决方案中跨项目生成代码时我采用这种模式定义共享接口项目(Contracts)public interface ICodeGenerationTarget { string Namespace { get; } string ClassName { get; } }在生成器项目中分析标记了该接口的类型生成的代码输出到共享的Generated文件夹项目文件需要添加ItemGroup Compile Include..\Shared\Generated\*.g.cs LinkGenerated\%(Filename)%(Extension) / /ItemGroup5.3 缓存与性能优化对于复杂生成器我实现了多层缓存语法分析结果缓存private static readonly ConcurrentDictionarystring, TypeModel _typeCache new(); TypeModel GetCachedType(INamedTypeSymbol symbol) { return _typeCache.GetOrAdd(symbol.GetDocumentationCommentId(), id BuildTypeModel(symbol)); }模板编译缓存void EnsureTemplateCached(string templateName) { if (!Engine.Razor.IsTemplateCached(templateName, null)) { var templateContent LoadTemplate(templateName); Engine.Razor.AddTemplate(templateName, templateContent); Engine.Razor.Compile(templateName, null); } }生成结果缓存基于文件哈希string GetFileHash(string path) { using var sha SHA256.Create(); using var stream File.OpenRead(path); return Convert.ToBase64String(sha.ComputeHash(stream)); }这些优化使一个实体类生成器的执行时间从平均120ms降到了35ms。6. 调试与问题排查实战6.1 调试技巧调试源码生成器有几种实用方法Debugger.Launch()简单粗暴public void Execute(GeneratorExecutionContext context) { Debugger.Launch(); // 生成代码... }日志输出更优雅context.ReportDiagnostic(Diagnostic.Create( new DiagnosticDescriptor( SG0001, Debug Info, $Processing {type.Name}, Debug, DiagnosticSeverity.Info, true), Location.None));文件转储复杂场景File.WriteAllText(D:\\debug_output.txt, generatedCode);6.2 常见错误处理这是我整理的错误速查表错误现象可能原因解决方案生成器未执行项目文件配置错误检查是否设置了IsAnalyzertrue生成的代码不出现文件名冲突确保AddSource的文件名唯一编译错误CS0246缺少依赖检查生成器项目的依赖是否标记PrivateAssetsall性能极差未使用增量生成迁移到IncrementalGenerator生成错误代码模板逻辑错误添加单元测试验证模板输出6.3 单元测试策略为生成器编写测试很具挑战性。我的测试框架包含语法树测试验证生成的语法树结构[Test] public void GeneratesCorrectClassStructure() { var generator new MyGenerator(); var input TestHelper.CreateClassSyntax(MyClass); var output generator.Generate(input); Assert.That(output.Members, Has.Exactly(1).TypeOfClassDeclarationSyntax()); var cls (ClassDeclarationSyntax)output.Members[0]; Assert.That(cls.Identifier.Text, Is.EqualTo(MyClass)); }编译测试确保生成的代码可编译[Test] public void GeneratedCodeShouldCompile() { var compilation TestHelper.CreateCompilation(); var generator new MyGenerator(); GeneratorDriver driver CSharpGeneratorDriver.Create(generator); driver driver.RunGeneratorsAndUpdateCompilation(compilation, out var outputCompilation, out var diagnostics); Assert.That(diagnostics, Is.Empty); Assert.That(outputCompilation.GetDiagnostics(), Is.Empty); }端到端测试模拟真实项目环境[Test] public void EndToEndTest() { var project TestHelper.CreateProject() .WithDocument(MyClass.cs, public partial class MyClass { } ); var compilation await project.GetCompilationAsync(); // 执行生成器并验证结果... }7. 实际项目经验分享在最近的一个微服务项目中我开发了一个gRPC服务端生成器可以根据接口定义自动生成服务实现骨架。这个生成器显著提升了团队效率但也遇到了一些挑战挑战1复杂类型支持处理嵌套泛型类型时最初的生成器会丢失类型参数。解决方案是递归处理所有类型参数string GetFullTypeName(ITypeSymbol type) { if (type is INamedTypeSymbol namedType namedType.IsGenericType) { var args string.Join(, , namedType.TypeArguments.Select(GetFullTypeName)); return ${namedType.Name}{args}; } return type.Name; }挑战2多项目协作当生成器需要跨多个项目收集信息时我建立了一个中央注册表模式定义一个注册表接口public interface ITypeRegistry { void RegisterType(TypeInfo info); IEnumerableTypeInfo GetRegisteredTypes(); }每个项目中的生成器部分将信息注册到共享文件主生成器读取共享文件生成最终代码性能调优经验在生成2000DTO类的场景下原始实现需要45秒。通过以下优化降到8秒并行处理独立类型缓存反射结果使用StringBuilder池减少内存分配重要提示在团队中推广源码生成器时一定要提供完善的文档和示例。我通常会创建两个示例项目一个展示基本用法另一个演示高级场景。同时维护一个生成代码预览文档帮助团队成员理解自动生成的代码。

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询