IIncrementalGenerator如何为多引用项目生成目标程序集代码
C# IIncrementalGenerator 跨项目导航属性生成方案
核心设计认知纠正
源生成器(包括增量生成器)的作用域为直接引用它的项目的单次编译过程,不会顺着项目依赖链向下传递,这是Roslyn平台的官方设计,不是bug:
- 运行时类库引用会沿着依赖链传递(比如WebApi/DesktopApp引用Library,就可以直接使用Library里的类型)
- 作为Analyzer引用的源生成器不会传递,必须在所有需要执行代码生成的项目中直接引用
你之前将SourceGenerator仅挂载到Library项目,自然无法给下游的WebApi、DesktopApp注入生成代码。
步骤1:正确配置项目引用
不需要使用“遍历引用程序集找注入点”的hack方案,按标准Analyzer引用规则配置即可:
SourceGenerator项目本身配置为分析器项目:设置<OutputType>Analyzer</OutputType>,不要将生成逻辑打包为运行时依赖Library项目仅存放公共定义:NavigationPropertyAttribute、业务接口、GetValue<T>()扩展方法,不要引用SourceGenerator- 需要生成代码的
WebApi、DesktopApp项目,直接引用SourceGenerator,引用时配置两个属性避免生成器被当作运行时依赖:
<ProjectReference Include="..\SourceGenerator\SourceGenerator.csproj" OutputItemType="Analyzer" ReferenceOutputAssembly="false" />
如果后续将生成器打包为NuGet分发,按照Roslyn分析器NuGet目录结构打包即可,安装时会自动应用上述配置,不需要手动修改项目文件。
步骤2:高性能增量元数据采集(保留全量增量构建优势)
你之前使用非增量生成器时性能差,核心原因是没有做增量缓存,在IIncrementalGenerator中通过符号比较做缓存即可实现零冗余编译开销,核心实现逻辑如下:
- 注册编译提供程序,仅在核心符号变化时重新执行筛选:
[Generator] public class NavigationPropertyGenerator : IIncrementalGenerator { private const string NavigationAttrFullName = "NavigationPropertyAttribute"; private const string EfCoreDbContextFullName = "Microsoft.EntityFrameworkCore.DbContext"; public void Initialize(IncrementalGeneratorInitializationContext context) { // 筛选需要生成代码的实体类 IncrementalValuesProvider<EntityGenerationMeta> entityProvider = context.CompilationProvider .Select((compilation, ct) => { // 快速判断当前编译是否引用了必要的公共定义,没有直接返回空 INamedTypeSymbol? navigationAttrSymbol = compilation.GetTypeByMetadataName(NavigationAttrFullName); if (navigationAttrSymbol == null) return ImmutableArray<EntityGenerationMeta>.Empty; bool isEfCoreProject = compilation.GetTypeByMetadataName(EfCoreDbContextFullName) != null; List<EntityGenerationMeta> result = new(); // 仅遍历当前项目自己的语法树找partial实现类——不需要遍历引用程序集找实现 foreach (SyntaxTree syntaxTree in compilation.SyntaxTrees) { ct.ThrowIfCancellationRequested(); SemanticModel semanticModel = compilation.GetSemanticModel(syntaxTree); IEnumerable<TypeDeclarationSyntax> typeDecls = syntaxTree.GetRoot(ct) .DescendantNodes() .OfType<TypeDeclarationSyntax>(); foreach (TypeDeclarationSyntax typeDecl in typeDecls) { ct.ThrowIfCancellationRequested(); // 跳过非partial类,没有生成空间 if (!typeDecl.Modifiers.Any(m => m.IsKind(SyntaxKind.PartialKeyword))) continue; if (semanticModel.GetDeclaredSymbol(typeDecl, ct) is not INamedTypeSymbol typeSymbol) continue; // 筛选实现了带NavigationPropertyAttribute标记的接口的类 List<NavigationPropertyMeta> navProps = new(); foreach (INamedTypeSymbol iface in typeSymbol.AllInterfaces) { foreach (IPropertySymbol prop in iface.GetMembers().OfType<IPropertySymbol>()) { AttributeData? navAttr = prop.GetAttributes() .FirstOrDefault(a => SymbolEqualityComparer.Default.Equals(a.AttributeClass, navigationAttrSymbol)); if (navAttr == null) continue; if (navAttr.ConstructorArguments.FirstOrDefault().Value is not INamedTypeSymbol navType) continue; // 从属性名推导导航属性名:去掉Id后缀,比如BarId -> Bar string propName = prop.Name.EndsWith("Id") ? prop.Name[..^2] : prop.Name; navProps.Add(new NavigationPropertyMeta(propName, navType)); } } if (navProps.Count > 0) { result.Add(new EntityGenerationMeta(typeSymbol, navProps.ToImmutableArray(), isEfCoreProject)); } } } return result.ToImmutableArray(); }) .WithComparer(EntityGenerationMeta.Comparer); // 自定义比较器,仅当核心元数据变化时触发后续生成 // 注册代码生成逻辑 context.RegisterSourceOutput(entityProvider, (spc, meta) => { // 按isEfCoreProject标识生成差异化代码:桌面端生成带缓存的GetValue属性,WebApi端生成EF Core自动属性+反向集合 string source = GeneratePartialClassCode(meta); spc.AddSource($"{meta.TypeSymbol.Name}.NavProperties.g.cs", source); }); } // 省略元数据记录定义、代码生成模板方法,按业务需求实现即可 private record EntityGenerationMeta(INamedTypeSymbol TypeSymbol, ImmutableArray<NavigationPropertyMeta> NavProps, bool IsEfCoreProject) { public static IEqualityComparer<EntityGenerationMeta> Comparer { get; } = new EntityMetaComparer(); // 自定义比较器实现,对比类符号、导航属性列表、项目类型是否变化,无变化就复用缓存 } private record NavigationPropertyMeta(string PropertyName, INamedTypeSymbol NavigationType); }
- 关键性能优化点:
- 不需要遍历所有引用程序集找实体类:所有需要生成代码的partial类都定义在当前正在编译的项目(WebApi/DesktopApp)本地,Library中仅存接口定义,只需要从引用程序集获取接口、特性的符号即可
- 使用
SymbolEqualityComparer和自定义元数据比较器做增量缓存,只要类定义、接口配置、项目引用没有变化,就不会重复执行生成逻辑,增量构建性能比旧版非增量生成器高90%以上
步骤3:差异化代码生成逻辑
根据采集阶段拿到的IsEfCoreProject标识生成对应代码即可:
- 桌面端项目:生成带缓存的导航属性,和预期的
Bar => bar ??= GetValue<Bar>()格式完全一致 - WebApi项目:生成EF Core兼容的自动get/set属性,同时为关联类型生成
ICollection<T>类型的反向导航属性,配置外键特性满足EF Core映射要求,不需要手动写FluentAPI配置。
常见问题排查
- 不要在生成器中使用反射加载Library程序集,所有类型判断必须通过
Compilation.GetTypeByMetadataName获取符号,避免程序集加载冲突 - 生成的代码必须和partial类使用完全一致的命名空间,避免找不到类型的编译错误
- 如果出现重复生成属性的错误,检查是否同时给Library项目挂载了生成器,Library中没有实体实现类,不需要执行生成
- 处理循环导航属性(比如IFoo的ParentId/ChildId指向自身)时,判断属性是否已经存在,避免重复定义
内容的提问来源于stack exchange,提问作者Raymond Osterbrink
相关产品推荐
相关产品推荐

