基于Roslyn的.NET代码生成技术实践
1. 项目概述当.NET遇上Roslyn代码生成在.NET生态中代码生成一直是个既基础又关键的环节。传统方式如T4模板虽然能用但总有种隔靴搔痒的感觉——开发体验不连贯、性能开销大、工具链支持弱。直到Roslyn编译器开放了它的API我们终于能像手术刀般精准地操作代码了。这个开源项目就是基于Roslyn的全新代码生成方案。它不像传统工具那样粗暴地拼接字符串而是把代码当作结构化数据来处理。想象一下你正在写的代码能实时分析项目结构自动生成配套的DTO、API路由甚至单元测试就像有个懂你心思的编程助手。我在实际项目中用它减少了40%的样板代码特别适合需要大量重复建模的WebAPI和微服务场景。2. 核心设计解析2.1 Roslyn编译管道深度集成Roslyn提供的SyntaxTree和SemanticModel是这个生成器的两大支柱。当你在VS里敲下保存键时生成器会获取当前项目的编译上下文Compilation分析特定标记的类/方法通过[GenerateDto]等特性标注用SyntaxFactory构建目标代码的语法节点通过增量生成器IncrementalGenerator输出.cs文件关键优势在于整个过程发生在编译初期生成的代码会参与后续的完整编译流程。这意味着完美支持代码导航和智能提示类型安全有编译器保证可以基于现有代码的语义进行分析比如自动提取接口方法2.2 声明式代码生成模式项目采用了标注即生成的设计哲学。例如要给User类生成对应的UserDto[GenerateDto] public class User { public int Id { get; set; } public string Name { get; set; } }生成器会扫描所有带[GenerateDto]的类然后解析原始类的属性签名创建去除了导航属性的扁平化结构自动添加DataContract序列化特性在obj/Debug/net8.0/generated目录输出UserDto.g.cs实测发现这种模式比传统T4模板快3-5倍因为Roslyn直接操作语法树而非文本。3. 实战应用场景3.1 WebAPI开发加速套件针对ASP.NET Core项目特别提供了自动控制器生成根据Service层接口智能FromBody/FromRoute参数推断响应包装模板统一Result 格式典型工作流[AutoController] public interface IUserService { UserDto GetUser(int id); } // 自动生成 [ApiController] [Route(api/[controller])] public partial class UserController : ControllerBase { [HttpGet({id})] public ResultUserDto GetUser(int id) _service.GetUser(id); }3.2 领域驱动设计支持对于复杂领域模型可以自动生成值对象ValueObject的相等性实现为聚合根AggregateRoot生成仓储接口创建领域事件的派发代码示例配置[ValueObject] public record Address { public string Street { get; init; } public string City { get; init; } } // 生成Equals/GetHashCode等样板代码4. 高级定制技巧4.1 生成策略配置通过继承BasicGenerator可以重写关键行为class CustomGenerator : BasicGenerator { protected override void ProcessProperty( IPropertySymbol prop, ClassBuilder builder) { if(prop.Name.EndsWith(Id)) builder.AddAttribute([JsonIgnore]); } }4.2 多文件协同生成处理复杂场景时可以用SyntaxTree的WithFilePath控制输出位置context.AddSource( hintName: SpecialCases.cs, sourceText: SyntaxFactory.ParseSyntaxTree(...) .WithFilePath(Features/Special/));5. 性能优化实践5.1 增量生成策略通过实现IIncrementalGenerator接口可以确保只有被影响的文件会重新生成支持跨项目引用分析缓存中间分析结果实测在200类的大型项目中增量生成能将耗时从6s降至800ms。5.2 并行处理技巧对于独立单元的生成任务var compilation context.Compilation; var symbols GetSymbolsToProcess(compilation); Parallel.ForEach(symbols, symbol { lock(context) { context.AddSource(/*...*/); } });注意需要处理线程竞争问题特别是当多个生成器同时工作时。6. 常见问题排查6.1 生成代码不可见检查步骤确认项目文件包含PropertyGroup EmitCompilerGeneratedFilestrue/EmitCompilerGeneratedFiles /PropertyGroup在VS中显示所有文件清理obj文件夹后重新编译6.2 类型解析失败当遇到类型未找到错误时确保相关程序集已通过[RegisterMetadata]标注检查#nullable enable是否影响类型推断使用compilation.GetTypeByMetadataName()时指定完整名称7. 扩展开发指南7.1 开发自定义生成器推荐项目结构/MyGenerator ├── MyGenerator.csproj ├── MyGenerator.cs └── extensions/ └── MyExtensions.cs关键NuGet依赖PackageReference IncludeMicrosoft.CodeAnalysis.CSharp Version4.7.0 PrivateAssetsall / PackageReference IncludeMicrosoft.CodeAnalysis.Analyzers Version3.3.4 PrivateAssetsall /7.2 调试技巧在launch.json中添加args: [--compiler-generated-files, --debug-generators]然后在生成器中设置调试断点System.Diagnostics.Debugger.Launch(); // 会弹出调试器选择窗口8. 生态整合方案8.1 与Swagger集成自动生成的XML注释可以通过以下方式同步到Swaggerservices.ConfigureSwaggerGen(c { c.IncludeXmlComments(Path.Combine( AppContext.BaseDirectory, obj/Debug/net8.0/generated/MyGenerator.xml)); });8.2 单元测试支持为生成的代码创建验证测试[Test] public void Dto_HasAllProperties() { var user new UserDto(); Assert.That(user, Has.Property(nameof(UserDto.Id))); // 使用反射验证所有属性 }我在实际项目中最喜欢的一个技巧是通过生成器自动创建测试用的Mock数据构建器这样能确保测试数据始终与模型保持同步。例如对于User类生成器会产出public class UserBuilder { private int _id 1; private string _name test; public User Build() new() { Id _id, Name _name }; public UserBuilder WithId(int id) { _id id; return this; } }这种模式特别适合领域驱动设计项目能显著减少测试维护成本。当模型新增属性时构建器会自动更新虽然需要重新编译但至少不会漏掉任何必填字段。