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

源生成器无法识别依赖项目中的类,如何解决?

模块化单体+整洁架构下的源生成器自动生成删除端点问题

问题背景

我基于模块化单体结合整洁架构开发:Application项目依赖Domain项目,端点定义在Application层,实体类存放在Domain层。由于删除端点逻辑高度一致,我尝试通过源生成器自动生成删除端点,但遇到了识别问题:

  • 实体的抽象基类AuditableEntity定义在Common.Domain项目:
public abstract class AuditableEntity : IAuditableEntity
{
....
}
  • 所有模块的Domain项目均依赖Common.Domain,实体类都继承自Common.Domain.Entities.AuditableEntity(例如Inventory模块的Product实体,通过AggregateRoot<ProductId>间接继承):
public class Product : AggregateRoot<ProductId> // AggregateRoot继承自AuditableEntity
{
....
}
  • 当前的DeleteEndpointSourceGenerator在Domain项目运行时,能正常识别所有继承AuditableEntity的实体;但在Application项目运行时(这是符合需求的场景),却无法识别Domain层的实体,导致无法生成对应删除端点。

问题根源

当前源生成器的SyntaxReceiver仅收集**当前编译项目(Application项目)**内的ClassDeclarationSyntax语法节点,但实体类存在于Domain项目中,Application项目的编译上下文不包含Domain项目的语法树,因此CandidateClasses集合中不会有Domain层的实体类。

此外,虽然Application项目依赖Domain项目,但源生成器默认只能访问当前编译项目的语法节点,不会自动遍历引用程序集中的类型符号。

解决方案

1. 替换语法节点收集为符号遍历

放弃SyntaxReceiver的语法节点收集逻辑,改为在Execute阶段直接遍历编译上下文的所有类型符号(包括引用程序集里的):

[Generator]
public class DeleteEndpointSourceGenerator : ISourceGenerator
{
    private const string AuditableEntityFullName = "Common.Domain.Entities.AuditableEntity";

    public void Initialize(GeneratorInitializationContext context)
    {
        // 不再需要SyntaxReceiver,移除注册逻辑
    }

    public void Execute(GeneratorExecutionContext context)
    {
        var compilation = context.Compilation;

        var auditableEntitySymbol = compilation.GetTypeByMetadataName(AuditableEntityFullName);
        if (auditableEntitySymbol == null)
        {
            context.ReportDiagnostic(Diagnostic.Create(_auditableEntityNotFoundDescriptor, Location.None));
            return;
        }

        // 遍历引用程序集的类型
        foreach (var assemblySymbol in compilation.SourceModule.ReferencedAssemblySymbols)
        {
            TraverseNamespace(assemblySymbol.GlobalNamespace, auditableEntitySymbol, context);
        }

        // 遍历当前项目的类型
        TraverseNamespace(compilation.SourceModule.GlobalNamespace, auditableEntitySymbol, context);
    }

    private void TraverseNamespace(INamespaceSymbol namespaceSymbol, INamedTypeSymbol baseTypeSymbol, GeneratorExecutionContext context)
    {
        foreach (var member in namespaceSymbol.GetMembers())
        {
            if (member is INamespaceSymbol nestedNamespace)
            {
                TraverseNamespace(nestedNamespace, baseTypeSymbol, context);
                continue;
            }

            if (member is INamedTypeSymbol classSymbol && classSymbol.TypeKind == TypeKind.Class && !classSymbol.IsAbstract)
            {
                if (IsDerivedFrom(classSymbol, baseTypeSymbol))
                {
                    context.ReportDiagnostic(Diagnostic.Create(_classInheritsDescriptor, Location.None, classSymbol.Name, baseTypeSymbol.Name));

                    var namespaceName = classSymbol.ContainingNamespace.ToDisplayString();
                    var className = classSymbol.Name;

                    var source = GenerateDeleteEndpointCode(namespaceName, className);
                    context.AddSource($"{className}_DeleteEndpoint.g.cs", SourceText.From(source, Encoding.UTF8));
                    context.ReportDiagnostic(Diagnostic.Create(_endpointGeneratedDescriptor, Location.None, className));
                }
            }
        }
    }

    // 保留原有的IsDerivedFrom、GenerateDeleteEndpointCode、Pluralize等方法
    // 保留原有的DiagnosticDescriptor定义
}

2. 过滤目标模块实体(可选)

如果不需要为所有模块的实体生成端点,可以通过命名空间过滤目标模块,比如只处理Inventory.Domain下的实体:

// 在TraverseNamespace的类型判断中添加
if (classSymbol.ContainingNamespace.ToDisplayString().StartsWith("Inventory.Domain"))
{
    // 执行生成逻辑
}

3. 修正源生成器项目配置

  • 源生成器项目必须引用Common.Domain项目,确保能获取AuditableEntity的类型符号
  • 在Application项目中引用源生成器时,设置PrivateAssets="all"避免依赖污染:
<ProjectReference Include="..\YourSourceGeneratorProject.csproj" PrivateAssets="all" />

4. 移除模块硬编码(可选)

生成代码中的[FromKeyedServices(nameof(Inventory))]是硬编码的模块名,可以通过实体命名空间动态提取模块名:

private static string GetModuleName(string namespaceName)
{
    var parts = namespaceName.Split('.');
    return parts.Length >= 1 ? parts[0] : "Default";
}

然后在生成代码时替换:

[FromKeyedServices(nameof({GetModuleName(namespaceName)}))] IUnitOfWork unitOfWork,

内容的提问来源于stack exchange,提问作者Baran

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.20 05:42:08