Dpz.Core.Service
Dpz.Core.Service 是 dpz.core 的业务服务层。它承接 MVC/WebApi/Jobs/Auth 等应用层的通用业务能力, 对上暴露 RepositoryService 接口,对下使用 MongoDB Repository、Mapster、FusionCache、RabbitMQ Outbox、Mediator 和 OpenIddict Store 等基础设施。
这个项目里有两类东西:
- 业务服务接口与实现:
RepositoryService/*+RepositoryServiceImpl/* - 服务层基础设施:DI 入口、缓存配置、Outbox、Mongo 索引初始化、OpenIddict Store、Bing 壁纸等
接入入口
应用层通常同时调用两个扩展方法:
services.AddBusinessServices(configuration);
services.AddDefaultServices();
AddBusinessServices(configuration) 位于 ServiceExtensions.cs,负责注册服务层依赖的基础设施:
IRepository<>/IUnitOfWork- Mapster
TypeAdapterConfig/IMapper - FusionCache:内存缓存 + Redis L2 + Redis Backplane
- 分布式锁:DEBUG 使用文件锁,非 DEBUG 使用 Redis 锁
- RabbitMQ 发布者 + MongoDB Outbox
- martinothamar
Mediator - MongoDB 索引初始化后台服务
IBingWallpaper
AddDefaultServices() 位于 ServiceDependencyInjection.cs,负责注册默认业务服务:
IImageFormatDetector- 源生成器生成的
AddGeneratedRepositoryServices()
注意:AddGeneratedRepositoryServices() 是编译期生成的方法,不要手写实现。
目录结构
Dpz.Core.Service/
├─ RepositoryService/ # 业务服务接口,对应用层暴露
├─ RepositoryServiceImpl/ # 业务服务实现,源生成器按约定扫描
├─ OpenIddictStores/ # Auth 项目使用的 OpenIddict MongoDB Store
├─ ServiceExtensions.cs # AddBusinessServices:基础设施注册入口
├─ ServiceDependencyInjection.cs # AddDefaultServices:默认服务注册入口
├─ MessageOutboxExtensions.cs # RabbitMQ Outbox 装饰器注册
├─ MongoIndexInitializerService.cs # 启动时初始化 MongoDB 索引
├─ IBingWallpaper.cs # Bing 壁纸服务
├─ AbstractCacheService.cs # 旧缓存基类,已过时
└─ Usings.cs # 项目级 global using
自动注册规则
业务服务通过 Dpz.Core.SourceGenerator 自动注册。生成器扫描固定命名空间:
- 接口:
Dpz.Core.Service.RepositoryService - 实现:
Dpz.Core.Service.RepositoryServiceImpl
默认匹配规则:
IArticleService -> ArticleService
IAccountService -> AccountService
ISteamGameService -> SteamGameService
新增普通业务服务时:
- 在
RepositoryService增加接口,名称以I开头。 - 在
RepositoryServiceImpl增加实现类,类名去掉接口名前缀I。 - 实现类实现对应接口。
- 不要在
ServiceDependencyInjection.cs里手动AddScoped<IFooService, FooService>()。 - 构建项目,让源生成器生成注册代码。
默认生命周期是 Scoped。可以在接口或实现类上使用:
[DependencyInjection(DependencyInjectionLifetime.Transient)]
如果服务要使用 typed HttpClient,使用:
[HttpClientDependencyInjection("https://api.example.com", TimeoutSeconds = 180)]
public interface IFooService
{
}
当前 ISteamGameService 就使用了这一路径。
缓存装饰器
新的缓存实现基于源生成器和 FusionCache。推荐在实现类方法上标记 [Cache], 而不是继承 AbstractCacheService。
[Cache(ExpirationSeconds = 3 * ExpirationTime.Hour)]
public Task<List<ArticleMiniResponse>> GetLatestAsync(
int range = 5,
CancellationToken cancellationToken = default
)
当实现类存在 [Cache] 或 [InvalidateCache] 时,生成器会改成装饰器注册。等价于:
services.AddScoped<ArticleService>();
services.AddScoped<IArticleService, GeneratedCachedArticleService>();
生成的 GeneratedCachedArticleService 会注入原始实现:
internal sealed class GeneratedCachedArticleService(
ArticleService inner,
IFusionCache fusionCache
) : IArticleService
{
}
所以应用层拿到的是 IArticleService 的缓存包装器;包装器内部再调用 ArticleService inner。 未标记缓存的方法会直接转发,标记了 [Cache] 的方法会包一层 fusionCache.GetOrSetAsync(...)。
缓存方法可以使用 PostProcess 在缓存命中后补充动态字段,例如文章浏览量、评论数:
[Cache(PostProcess = nameof(ApplyViewCountsAsync))]
public Task<List<ArticleMiniResponse>> GetTopArticlesAsync(...)
写操作可以用 [InvalidateCache] 清理指定缓存方法的 tag:
[InvalidateCache(Methods = [nameof(GetFriendsAsync)])]
public Task SaveFriendAsync(...)
生成器还会为每个可缓存服务生成元数据类型,例如:
GeneratedArticleServiceCacheMetadata.DefaultPrefix
GeneratedArticleServiceCacheMetadata.BuildGetArticleAsyncCacheKey(articleId)
GeneratedArticleServiceCacheMetadata.GetMethodTag(nameof(GetArticleAsync))
服务内部需要构造同一缓存域下的额外 key/tag 时,应优先使用这些生成类型。
服务边界
RepositoryService 是应用层使用的服务契约。公共方法应返回 DTO、ViewModel、Response 等公开模型,不要直接返回 Dpz.Core.Public.Entity 实体类型。
常见依赖:
IRepository<T>:MongoDB 数据访问IUnitOfWork:事务工作单元,需要 MongoDB replica set 才能真正使用事务IMapper:Mapster 映射,运行时映射使用注入的IMapperIFusionCache:缓存IMessagePublisher<T>:RabbitMQ 发布IMediator:调用Dpz.Core.Service.Mediator的 CQRS 请求IDistributedLockProvider:跨实例锁IConfiguration/ILogger<T>
业务接口应尽量接收抽象参数,返回具体结果。异步方法以 Async 结尾,并接受 CancellationToken cancellationToken = default。
Outbox
AddBusinessServices(configuration) 会调用:
services.AddRabbitMQ(configuration).AddMessageOutbox();
AddMessageOutbox() 会把 IMessagePublisher<T> 替换为带 Outbox 追踪的 OutboxMessagePublisher<T>,并使用 IMongoMessageOutboxStore 持久化消息状态。
IMongoMessageOutboxStore 是普通业务服务接口,位于 RepositoryService,实现类是 MongoMessageOutboxStore。它同样由源生成器自动注册为 Scoped。为了避免 Singleton 持有 Scoped 依赖,MessageOutboxExtensions 中使用 IServiceScopeFactory 在每次调用时创建 独立 Scope。
重试 Worker 不在本项目自动启动。应用层按需调用 MessageQueue 项目的 AddMessageOutboxRetryWorker(),例如 Web/WebApi/Jobs 各自的 DI 扩展中。
MongoDB 索引初始化
MongoIndexInitializerService 是一个 IHostedService。启动时它会:
- 读取
ConnectionStrings:mongodb。 - 加载
Dpz.Core.Public.Entity程序集。 - 扫描实现
IIndexedEntity<>的实体。 - 通过
IMongoIndexInitializer创建索引。 - 使用分布式锁避免多实例同时初始化。
- 使用 FusionCache 记录上次成功时间,默认 6 小时内跳过重复初始化。
DEBUG 下分布式锁使用 FileLockPath,默认 D:\backup\dpz.core.lock;非 DEBUG 下使用 Redis。
OpenIddict Store
OpenIddictStores/* 包含 Auth 项目使用的 MongoDB Store:
DpzApplicationStoreDpzAuthorizationStoreDpzScopeStore
这些类型不在 RepositoryServiceImpl 命名空间下,因此不会被业务服务源生成器自动注册。 它们由 Dpz.Core.Auth/OpenIddictRegister.cs 显式注册并替换 OpenIddict 默认 Store。
旧缓存类型
ICacheService、AbstractCacheService、GenerateCacheKey 仍保留在项目中,但已经标记 Obsolete。新代码应使用 [Cache]、[InvalidateCache] 和生成的缓存元数据类型。
当前业务域
当前 RepositoryService / RepositoryServiceImpl 覆盖的主要业务域包括:
- 账号、登录历史、会话、两步验证、WebAuthn
- 文章、评论、标签、浏览量和评论数缓存同步
- 音频、音乐、视频、图片记录、弹幕
- 碎碎念、时间线、书签、动态页面、页面元数据
- 代码笔记和代码文件系统条目
- 应用配置、应用日志、健康检查、系统通知历史
- Steam 游戏库和成就资源
- RabbitMQ Outbox 记录查询、筛选、清理和重试状态
新增服务检查清单
- 接口放在
RepositoryService,实现放在RepositoryServiceImpl。 - 接口名和实现名符合
IFooService/FooService约定。 - 不手写业务服务 DI 注册,除非确实要绕过源生成器。
- 公共服务方法不直接返回实体类型。
- 映射使用注入的
IMapper。 - 缓存标记写在实现方法上,不写在接口方法上。
- 写操作需要清理缓存时优先使用
[InvalidateCache]。 - 需要手动 key/tag 时使用
Generated{Service}CacheMetadata。 - 需要 typed
HttpClient时在接口或实现上使用[HttpClientDependencyInjection]。
验证命令
从仓库根目录执行:
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
完整验证可执行:
dotnet build src/Dpz.Core.slnx