You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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中通过符号比较做缓存即可实现零冗余编译开销,核心实现逻辑如下:

  1. 注册编译提供程序,仅在核心符号变化时重新执行筛选:
[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);
}
  1. 关键性能优化点:
    • 不需要遍历所有引用程序集找实体类:所有需要生成代码的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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.29 00:03:20