folder
csharp
csharp
visualstudio
csharp
csharp
csharp
csharp
csharp
csharp
readme
csharp
csharp
csharp
csharp

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

新增普通业务服务时:

  1. RepositoryService 增加接口,名称以 I 开头。
  2. RepositoryServiceImpl 增加实现类,类名去掉接口名前缀 I
  3. 实现类实现对应接口。
  4. 不要在 ServiceDependencyInjection.cs 里手动 AddScoped<IFooService, FooService>()
  5. 构建项目,让源生成器生成注册代码。

默认生命周期是 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 映射,运行时映射使用注入的 IMapper
  • IFusionCache:缓存
  • 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。启动时它会:

  1. 读取 ConnectionStrings:mongodb
  2. 加载 Dpz.Core.Public.Entity 程序集。
  3. 扫描实现 IIndexedEntity<> 的实体。
  4. 通过 IMongoIndexInitializer 创建索引。
  5. 使用分布式锁避免多实例同时初始化。
  6. 使用 FusionCache 记录上次成功时间,默认 6 小时内跳过重复初始化。

DEBUG 下分布式锁使用 FileLockPath,默认 D:\backup\dpz.core.lock;非 DEBUG 下使用 Redis。

OpenIddict Store

OpenIddictStores/* 包含 Auth 项目使用的 MongoDB Store:

  • DpzApplicationStore
  • DpzAuthorizationStore
  • DpzScopeStore

这些类型不在 RepositoryServiceImpl 命名空间下,因此不会被业务服务源生成器自动注册。 它们由 Dpz.Core.Auth/OpenIddictRegister.cs 显式注册并替换 OpenIddict 默认 Store。

旧缓存类型

ICacheServiceAbstractCacheServiceGenerateCacheKey 仍保留在项目中,但已经标记 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
评论加载中...