folder-class
csharp
readme
csharp

Dpz.Core.SourceGenerator

Dpz.Core.SourceGenerator 是 dpz.core 的编译期代码生成项目,当前负责生成两类代码:

  • Repository Service 的依赖注入注册代码。
  • 标记了 [Cache] 的服务实现方法的缓存装饰器代码。

该项目只作为 Analyzer 被业务项目引用,不作为普通程序集引用。源生成器相关特性由独立项目 Dpz.Core.SourceGenerator.Attributes 维护,业务项目通过普通 ProjectReference 使用这些特性。

项目职责

Dpz.Core.SourceGenerator 的职责是读取编译期符号信息,并生成确定性的 C# 源码。它不保存运行时状态,不做运行时扫描,也不依赖动态代理。

当前生成器遵循以下边界:

  • 服务注册生成只针对 Dpz.Core.Service.RepositoryServiceDpz.Core.Service.RepositoryServiceImpl 的约定匹配。
  • 缓存生成只读取实现类方法上的 [Cache],不读取接口方法上的 [Cache]
  • 接口只表达契约;是否缓存属于实现策略。
  • 生成代码必须能被普通 C# 编译器直接编译,避免运行时反射和表达式树编译。

相关项目

Dpz.Core.SourceGenerator.Attributes 是源生成器特性项目,包含业务代码可直接使用的特性类型:

  • DependencyInjectionAttribute
  • DependencyInjectionLifetime
  • HttpClientDependencyInjectionAttribute
  • CacheAttribute

这些类型没有放在 generator 项目中通过字符串注入,是为了让默认值、注释和公开契约由真实源码维护,减少重复修改。

业务项目的引用方式通常是:

<ProjectReference
  Include="..\Dpz.Core.SourceGenerator.Attributes\Dpz.Core.SourceGenerator.Attributes.csproj"
  PrivateAssets="all"
/>
<ProjectReference
  Include="..\Dpz.Core.SourceGenerator\Dpz.Core.SourceGenerator.csproj"
  OutputItemType="Analyzer"
  ReferenceOutputAssembly="false"
  PrivateAssets="all"
/>

目录说明

  • ServiceRegistrationGenerator.cs:Roslyn 增量生成入口,只负责连接生成管线。
  • ServiceRegistrationProvider.cs:扫描接口/实现类型,产出服务注册元数据。
  • InterfaceContractInspector.cs:读取接口方法、属性、事件,供装饰器完整实现接口。
  • ServiceRegistrationSourceBuilder.cs:根据服务注册元数据生成 DI 注册源码。
  • CacheMethodInspector.cs:读取实现方法上的 [Cache],并转换为缓存元数据。
  • CacheDecoratorSourceGenerator.cs:根据缓存元数据生成缓存装饰器。
  • SymbolDisplay.cs:统一 Roslyn 类型名称、nullable 标记和默认值字面量输出。
  • SourceGeneratorAttributeNames.cs:集中维护需要识别的特性完整名称。
  • Models/:源生成器内部使用的元数据模型,命名空间为 Dpz.Core.SourceGenerator.Models,一个文件一个类型,包括服务注册、缓存方法、接口方法、接口属性和接口事件。

服务注册生成

服务注册生成器会扫描:

  • 接口命名空间:Dpz.Core.Service.RepositoryService
  • 实现命名空间:Dpz.Core.Service.RepositoryServiceImpl

每个接口会匹配第一个实现该接口的非抽象实现类。默认生成 Scoped 注册。

可通过特性调整:

  • [DependencyInjection]:覆盖生命周期或忽略自动注册。
  • [HttpClientDependencyInjection]:生成 typed HttpClient 注册。

当服务实现存在可缓存方法时,生成器会改为注册原始实现类和缓存装饰器:

  • 原始实现类按 concrete type 注册。
  • 接口注册到生成的 GeneratedCached{ImplementationName} 装饰器。

缓存生成

缓存属于实现策略,因此 [Cache] 应标记在实现类方法上:

public class HealthCheckService(
    IRepository<HealthCheck> repository,
    IMapper mapper
) : IHealthCheckService
{
    [Cache(ExpirationSeconds = 86400)]
    public async Task<List<HealthCheckResponse>> GetHealthChecksAsync()
    {
        var list = await repository.MongodbQueryable.ToListAsync();
        return mapper.Map<List<HealthCheckResponse>>(list);
    }
}

被缓存的方法目前要求:

  • 返回类型为 Task<T>
  • 参数不能是 ref / out / in
  • 未显式设置 CacheKey 时,参数类型必须可稳定格式化为缓存键。

生成的装饰器会注入原始实现类和 IFusionCache,在调用原始方法外包一层 fusionCache.GetOrSetAsync。 生成缓存会自动写入前缀标签 {prefix} 和方法标签 {prefix}:{method}。如果 CacheAttribute.AdditionalTags 配置了额外标签,这些标签也会一并写入,供跨业务域清理缓存使用。

每个服务实现还会生成一个缓存元数据类型,命名为 Generated{ImplementationName}CacheMetadata。 业务实现需要构造同一缓存域下的额外 key/tag 时,应使用这个生成类型,而不是手写 typeof(Service).FullName。例如 GeneratedArticleServiceCacheMetadata.DefaultPrefixGeneratedArticleServiceCacheMetadata.GetMethodTag(nameof(GetTopArticlesAsync))。针对标记了 [Cache] 的方法,metadata 还会生成强类型的 Build{MethodName}CacheKey(...) 方法,供 装饰器直接调用。

新的 [Cache] 路径不依赖运行时缓存键 builder,也不通过反射读取参数对象属性。缓存键格式化、 转义、集合排序和 tag 选择都由源生成器直接写入生成源码。这条路径是为 AOT 准备的。

缓存检查从实现方法出发:只有实现方法标记 [Cache] 才会参与生成。随后生成器会把该实现方法映射回当前注册的服务接口方法,用于生成装饰器方法签名。若方法不属于注册接口,会产生编译期错误,避免看似配置了缓存但实际没有调用入口。

装饰器会转发接口上的非缓存成员,包括:

  • 未标记 [Cache] 的普通方法。
  • 接口属性。
  • 接口事件。

分页返回值

IPagedList<T> 是接口类型,不适合作为 FusionCache 中的直接序列化载体。生成器检测到被缓存方法的返回值是 Task<IPagedList<T>> 时,会生成以下逻辑:

  1. 调用原始服务方法取得 IPagedList<T>
  2. 写入缓存前转换为 PagedListWarp<T>
  3. 缓存命中后再通过 ToPagedList() 转回 IPagedList<T>

业务方法仍然只需要声明原本的返回类型:

[Cache(ExpirationSeconds = 604800)]
public async Task<IPagedList<TimelineResponse>> GetTimeLinesAsync(int pageIndex, int pageSize)

生成代码中缓存的实际类型是 PagedListWarp<TimelineResponse>,这让分页元数据和分页项都能稳定进入缓存,同时保持接口契约不变。

缓存后处理

部分读取方法在缓存命中后仍需要补充动态字段,例如文章浏览量、评论数、点赞数。此时可以通过 PostProcess 指定实现类上的后处理方法:

[Cache(PostProcess = nameof(ApplyViewCountsAsync))]
public Task<List<ArticleMiniResponse>> GetTopArticlesAsync(...)

生成装饰器会先从缓存读取或写入基础数据,再调用 inner.ApplyViewCountsAsync(result),最后返回处理后的结果。 后处理方法需要能被生成装饰器访问,推荐使用 internal Taskinternal ValueTask,第一个参数为被缓存方法的返回值。生成器会校验后处理方法存在、参数兼容且返回 awaitable;失败时输出 DPZ_CACHE003

缓存失效

写操作可以通过 [InvalidateCache] 在原方法成功返回后清理其它 [Cache] 方法的 method tag:

[InvalidateCache(Methods = [nameof(GetTopArticlesAsync), nameof(GetPagesAsync)])]
public Task DeleteAsync(string id, CancellationToken cancellationToken = default)

Methods 里的方法名必须是同一实现类上已经标记 [Cache] 的方法,生成器会在编译期校验;失败时输出 DPZ_CACHE004。失效生成代码只调用 fusionCache.RemoveByTagAsync,不会扫描缓存键,也不会依赖运行时反射。

注意:ArticleServiceMumbleService 这类存在浏览量、点赞数、评论数动态叠加逻辑的服务,读取方法通过 [Cache(PostProcess = ...)] 走装饰器缓存;细粒度计数缓存仍保留在服务内部,用生成的 Generated{Service}CacheMetadata 构造 key/tag。

缓存键规则

自动生成的缓存键格式为:

{namespace}.{className}:{method}:{key1=value1&key2=value2}

示例:

Dpz.Core.Service.RepositoryServiceImpl.ArticleService:GetArticleAsync:id=abc

值格式化规则:

  • 中文、英文、数字等普通文字保持原样。
  • 会破坏键结构的字符会被转义:%:&=,
  • 集合和数组格式为 key=value1,value2,并按格式化后的字符串进行稳定排序。
  • string 不按集合处理。
  • 数值、日期等 IFormattable 使用 InvariantCulture

显式 CacheKey 会生成:

{prefix}:{method}:{CacheKey}

常用配置示例:

[Cache(
    ExpirationSeconds = 3600,
    AdditionalTags = ["home", "article"]
)]
public Task<List<ArticleMiniResponse>> GetLatestAsync(CancellationToken cancellationToken = default)

诊断规则

当前缓存生成器包含以下编译期错误:

  • DPZ_CACHE001:方法签名暂不支持缓存生成,例如返回类型不是 Task<T> 或参数不可稳定格式化。
  • DPZ_CACHE002:预留的 IFusionCache 依赖诊断,当前生成装饰器自行注入 IFusionCache,业务实现不需要为了 [Cache] 注入它。
  • DPZ_CACHE003PostProcess 方法不存在、参数不兼容,或返回类型不是 awaitable。
  • DPZ_CACHE004[InvalidateCache] 标记的方法无效,例如 Methods 为空或引用了非 [Cache] 方法。

这些诊断使用 Error 级别,避免缓存配置错误拖到运行时才暴露。

项目启用了 Roslyn 的扩展分析规则。RS2008 是诊断规则发布跟踪检查:当 Analyzer/Source Generator 定义新的 DiagnosticDescriptor 时,官方推荐在 AnalyzerReleases.Shipped.mdAnalyzerReleases.Unshipped.md 里记录规则变更。当前生成器仍是内部项目,缓存诊断暂时用局部 #pragma 压制该检查;如果未来作为公开 Analyzer 发布,应改为维护 release tracking 文件。

开发约定

  • 保持职责单一:发现、检查、生成、元数据模型分开维护。
  • 原则上一个 .cs 文件只包含一个类型;强关联内部类型除外。
  • 新增公开特性时优先放到 Dpz.Core.SourceGenerator.Attributes
  • 新增生成器内部模型时放到 Models/,命名空间使用 Dpz.Core.SourceGenerator.Models,不要继续在生成器入口类里堆嵌套结构。
  • 修改特性完整名称时,同步更新 SourceGeneratorAttributeNames

验证命令

dotnet build src/Dpz.Core.SourceGenerator.Attributes/Dpz.Core.SourceGenerator.Attributes.csproj
dotnet build src/Dpz.Core.SourceGenerator/Dpz.Core.SourceGenerator.csproj
dotnet build src/Dpz.Core.Service/Dpz.Core.Service.csproj
dotnet test src/Dpz.Core.ServiceTest/Dpz.Core.ServiceTest.csproj --filter ServiceDependencyInjectionTest
dotnet test src/Dpz.Core.ServiceTest/Dpz.Core.ServiceTest.csproj --filter CacheSourceGenerator
评论加载中...