using Dpz.Core.Entity.Base.MapperConfig;
using Dpz.Core.Infrastructure.Intercept;
using Dpz.Core.MessageQueue.Extensions;
using MailKit;
using Mapster;
using Medallion.Threading;
using Medallion.Threading.FileSystem;
using Medallion.Threading.Redis;
using Microsoft.Extensions.Caching.StackExchangeRedis;
using Microsoft.Extensions.DependencyInjection;
using StackExchange.Redis;
using ZiggyCreatures.Caching.Fusion.Backplane.StackExchangeRedis;
using ZiggyCreatures.Caching.Fusion.Serialization.SystemTextJson;
#pragma warning disable CS0162
namespace Dpz.Core.Service;
public static class ServiceExtensions
{
public static void AddBusinessServices(
this IServiceCollection services,
IConfiguration configuration
)
{
// 邮件日志
services.AddTransient<IProtocolLogger, SerilogProtocolLogger>();
services.AddTransient<IUnitOfWork, UnitOfWork>();
services.AddScoped(typeof(IRepository<>), typeof(Repository<>));
// 索引初始化服务
services.AddSingleton<IMongoIndexInitializer, MongoIndexInitializer>();
// 索引初始化后台任务
services.AddHostedService<MongoIndexInitializerService>();
#if DEBUG
services.AddScoped<Func<IRepository<Music>>>(x =>
{
var config = x.GetService<IConfiguration>();
var connectionString = config?.GetConnectionString("mongodb-test");
return () => new Repository<Music>(connectionString);
});
#endif
services.AddSingleton<TypeAdapterConfig>(_ => GlobalConfigMapper.GetTypeAdapterConfig());
services.AddSingleton<IMapper>(_ => GlobalConfigMapper.GetMapper());
var redisConnectionStr = configuration.GetConnectionString("redis") ?? "";
// 分布式锁
services.AddSingleton<IDistributedLockProvider>(_ =>
{
#if DEBUG
var fileLockPath =
configuration.GetValue("FileLockPath", @"D:\backup\dpz.core.lock")
?? throw new InvalidConfigurationException("FileLockPath Invalid Config");
var lockFileDirectory = new DirectoryInfo(fileLockPath);
return new FileDistributedSynchronizationProvider(lockFileDirectory);
#endif
var connectionMultiplexer = ConnectionMultiplexer.Connect(redisConnectionStr);
var database = connectionMultiplexer.GetDatabase();
return new RedisDistributedSynchronizationProvider(database);
});
services.AddScoped<IBingWallpaper, BingWallpaper>();
services.AddFusionCacheServices(redisConnectionStr);
services.AddScoped<IInterceptRuleService, InterceptRuleService>();
// 注册 RabbitMQ 消息队列基础服务(发布者)
// 各应用层根据需要注册具体的消费者
services.AddRabbitMQ(configuration).AddMessageOutbox();
// 注册中介者,确保处理器可使用已注册的缓存/消息队列等组件。
services.AddDpzMediator();
services.AddSingleton<OwnerService>();
}
/// <summary>
/// 添加 FusionCache 缓存服务配置
/// </summary>
/// <param name="services">服务集合</param>
/// <param name="redisConnectionString">Redis 连接字符串</param>
private static void AddFusionCacheServices(
this IServiceCollection services,
string redisConnectionString
)
{
// 添加基础内存缓存服务
services.AddMemoryCache();
services
.AddFusionCache()
.WithOptions(options =>
{
// 分布式缓存熔断器持续时间:当Redis连接失败时,等待3秒后重试
options.DistributedCacheCircuitBreakerDuration = TimeSpan.FromSeconds(3);
/*
* 自定义日志级别配置
* 大幅减少 FusionCache 的日志噪音,特别是新版本的详细操作日志
*/
// 故障安全激活:重要事件,应该记录
options.FailSafeActivationLogLevel = LogLevel.Warning;
// 序列化错误:需要关注
options.SerializationErrorsLogLevel = LogLevel.Warning;
// 分布式缓存超时:只记录Warning级别
options.DistributedCacheSyntheticTimeoutsLogLevel = LogLevel.Warning;
// 分布式缓存错误:必须记录
options.DistributedCacheErrorsLogLevel = LogLevel.Error;
// 工厂方法超时:只记录Warning级别
options.FactorySyntheticTimeoutsLogLevel = LogLevel.Warning;
// 工厂方法错误:必须记录
options.FactoryErrorsLogLevel = LogLevel.Error;
})
.WithDefaultEntryOptions(
new FusionCacheEntryOptions
{
// ===== 故障安全配置 =====
// 启用故障安全模式:当Redis不可用时,使用本地缓存中的旧数据
IsFailSafeEnabled = true,
// 故障安全最大持续时间:最多使用1天的旧数据
FailSafeMaxDuration = TimeSpan.FromDays(1),
// 故障安全节流持续时间:30秒内不重复尝试访问故障的分布式缓存
FailSafeThrottleDuration = TimeSpan.FromSeconds(30),
// ===== 工厂方法超时配置 =====
// 工厂方法软超时:500ms后开始使用故障安全数据(如果有)
FactorySoftTimeout = TimeSpan.FromMilliseconds(500),
// 工厂方法硬超时:2秒后强制中断工厂方法执行
FactoryHardTimeout = TimeSpan.FromMilliseconds(2000),
// ===== 分布式缓存配置 =====
// Redis 软超时:1秒后开始使用本地缓存(如果有)
DistributedCacheSoftTimeout = TimeSpan.FromSeconds(1),
// Redis 硬超时:2秒后放弃Redis操作,完全依赖本地缓存
DistributedCacheHardTimeout = TimeSpan.FromSeconds(2),
// 允许后台分布式缓存操作:写入Redis操作异步执行,不阻塞主请求
AllowBackgroundDistributedCacheOperations = true,
// ===== 抖动配置 =====
// 最大抖动时间:在缓存过期时间上随机添加0-2秒,防止缓存雪崩
JitterMaxDuration = TimeSpan.FromSeconds(2),
}
)
// 使用 System.Text.Json 序列化器
.WithSerializer(
new FusionCacheSystemTextJsonSerializer(
new JsonSerializerOptions { IncludeFields = true }
)
)
// 配置 Redis 作为分布式缓存(L2缓存)
.WithDistributedCache(
new RedisCache(new RedisCacheOptions { Configuration = redisConnectionString })
)
// 配置 Redis 背板:用于多服务器间的缓存失效通知
.WithBackplane(
new RedisBackplane(
new RedisBackplaneOptions { Configuration = redisConnectionString }
)
);
}
}
文件报告:ServiceExtensions.cs
定位与职责
该文件是 Dpz.Core.Service(业务服务层基础设施项目)的依赖注入总入口。它对外暴露唯一扩展方法 AddBusinessServices(IServiceCollection, IConfiguration),把业务服务层运行所需的基础设施(MongoDB 仓储/工作单元、Mapster 映射、FusionCache 多级缓存、分布式锁、RabbitMQ+Outbox、Mediator、后台索引初始化等)一次性注册进容器;文件内部还包含一个私有的 FusionCache 装配方法。
调用方是各个应用层的 Program.cs / 测试基类,例如 Dpz.Core.Web/Program.cs、Dpz.Core.Auth/Program.cs、Dpz.Core.WebApi/Program.cs,以及 ServiceDependencyInjection.cs 的 AddDefaultServices() 配套使用(后者只负责图片格式探测与源生成器生成的仓储服务注册)。
AddBusinessServices 注册内容
按代码顺序分组如下。
1. 邮件与数据访问基础
IProtocolLogger→ SerilogProtocolLogger.cs(Transient),把 MailKit 的 SMTP 协议日志接入 Serilog(需自行确认 Serilog 的注册位置,本文件只做映射)。IUnitOfWork→ UnitOfWork.cs(Transient),事务工作单元,依赖 MongoDB replica set 才能真正开启事务。IRepository<>→Repository<>(Scoped),泛型仓储,二者均来自 Dpz.Core.MongodbAccess。
2. MongoDB 索引初始化
IMongoIndexInitializer→MongoIndexInitializer(Singleton),实现在 MongodbAccess。- 注册后台托管服务 MongoIndexInitializerService.cs(
IHostedService),启动时借助IDistributedLockProvider抢分布式锁、扫描Dpz.Core.Public.Entity程序集中实现IIndexedEntity<>的实体并同步索引,且通过 FusionCache 记录最近一次成功时间(默认 6 小时内跳过)。
3. DEBUG 专用测试仓储工厂
#if DEBUG 分支下注册 Func<IRepository<Music>>(Scoped),从 mongodb-test 连接串构造测试库仓储,便于调试/测试场景隔离。
4. Mapster 对象映射
以 Singleton 注册 TypeAdapterConfig 与 IMapper,实现均委托给 GlobalConfigMapper.cs(内部 Lazy<T> 缓存配置:扫描所有 Dpz.Core* 程序集、装配 IHaveCustomMapping 静态映射并内置 string→ObjectId 规则)。
5. 分布式锁(按编译环境切换实现)
Singleton 注册 IDistributedLockProvider(Medallion.Threading):
#if DEBUG:用配置文件FileLockPath(默认D:\backup\dpz.core.lock)创建FileDistributedSynchronizationProvider,适合单机开发;- 非 DEBUG(生产):用
ConnectionStrings:redis连接ConnectionMultiplexer后创建RedisDistributedSynchronizationProvider,支持多实例互斥。
6. 业务服务与缓存
IBingWallpaper→BingWallpaper(Scoped),见 IBingWallpaper.cs。AddFusionCacheServices(redisConnectionStr):见下文。IInterceptRuleService→ InterceptRuleService.cs(Scoped),按后台配置的拦截规则判定请求是否命中(规则加载失败时降级放行)。
7. 消息队列与 Outbox
services.AddRabbitMQ(configuration).AddMessageOutbox();
AddRabbitMQ定义于 MessageQueueServiceExtensions.cs,注册 RabbitMQ 配置、连接工厂、路由约定与发布者(此时IMessagePublisher<>指向RabbitMQPublisher<>,Outbox 为 Null 实现)。AddMessageOutbox定义于同项目 MessageOutboxExtensions.cs,随后执行:单独注册具体类型RabbitMQPublisher<>绕过装饰器死循环、用OutboxMessagePublisher<>覆盖IMessagePublisher<>、启用真实重发发布者,并以ScopedOutboxStoreAdapter(内部每次经IServiceScopeFactory新建 scope)把 Singleton 的IMessageOutboxStore委托给 Scoped 的IMongoMessageOutboxStore,规避 captive dependency。
8. 中介者与业务单例
services.AddDpzMediator(),来自 MediatorExtensions.cs:以 Scoped 生命周期注册martinothamar Mediator,并追加日志、耗时、异常包装三个管道行为(IPipelineBehavior)。代码注释点明其必须在缓存/消息队列之后注册,保证 Handler 能直接注入这些组件。OwnerService(Singleton),见 OwnerService.cs,读取upyun:Host/S3:CdnHost配置并提供 CDN 图片归属判断。
AddFusionCacheServices(私有方法)
为应用装配 FusionCache 多级缓存:
- L1 内存缓存:
AddMemoryCache()。 - 核心配置:分布式缓存熔断器 3 秒;显式收敛各事件日志级别以减少噪音(如序列化/超时降为 Warning,缓存与工厂错误保持 Error);开启故障安全(可用最多 1 天旧数据、30 秒节流);工厂软/硬超时 500ms/2s;Redis 软/硬超时 1s/2s,允许后台写 Redis 不阻塞主请求;过期时间上加 0–2s 随机抖动防缓存雪崩。
- 序列化与后端:
System.Text.Json序列化器(IncludeFields = true)→ Redis L2(RedisCache)→ Redis Backplane(RedisBackplane,基于 Pub/Sub 的跨实例缓存失效通知)。L2 与 Backplane 均复用同一份redis连接字符串。
执行流程
flowchart TD
A[应用层 Program.cs 调用 AddBusinessServices<br/>services.AddBusinessServicesconfiguration] --> B[注册 MailKit 日志 / IUnitOfWork / IRepository<>]
B --> C[注册 IMongoIndexInitializer + 后台 HostedService<br/>MongoIndexInitializerService]
C --> D{DEBUG 编译?}
D -- 是 --> E[注册 mongodb-test 的 Music 仓储工厂]
D -- 否 --> F[注册 Mapster TypeAdapterConfig / IMapper]
E --> F
F --> G[注册分布式锁 IDistributedLockProvider]
G --> H{DEBUG 编译?}
H -- 是 --> I[FileDistributedSynchronizationProvider<br/>FileLockPath]
H -- 否 --> J[RedisDistributedSynchronizationProvider<br/>redis 连接串]
I --> K[注册 IBingWallpaper / FusionCache 缓存 / IInterceptRuleService]
J --> K
K --> L[AddRabbitMQ 注册发布者<br/>RabbitMQPublisher + Null Outbox]
L --> M[AddMessageOutbox 覆盖为<br/>OutboxMessagePublisher + Mongo Outbox 存储]
M --> N[AddDpzMediator 注册中介者与治理管道<br/>需在缓存/队列之后]
N --> O[注册 OwnerService 单例]
O --> P[返回,容器组装完成]
flowchart LR
subgraph AddFusionCacheServices
M1[AddMemoryCache L1] --> F1[AddFusionCache 选项: 熔断/日志/故障安全/超时/抖动]
F1 --> S1[System.Text.Json 序列化器]
S1 --> D1[RedisCache L2 分布式缓存]
D1 --> B1[RedisBackplane 失效通知背板]
end
编码细节与注意点
- 文件顶部
#pragma warning disable CS0162的作用:在 DEBUG 编译下,分布式锁工厂里#if DEBUG分支先return了文件锁实现,其后紧接的 Redis 实现代码会被判为“不可达代码”,该指令用于屏蔽由此触发的 CS0162 警告。 - DEBUG/Release 通过条件编译呈现两套截然不同的锁与连接语义,编译期而非配置期切换,生产代码中不包含文件锁/测试库分支。
- 注册顺序有依赖约束(详见文件注释与 Dpz.Core.Service.Mediator/README.md):基础仓储/缓存/队列先注册,Mediator 与各治理管道后注册,使请求处理器可解析到完整设施。
- FusionCache 的 L2 与 Backplane 均直接使用
redis连接字符串;若ConnectionStrings:redis为空则整个多级缓存退化为本地内存缓存(代码按空串容忍,不会抛异常),实际连接失败时由熔断器与故障安全机制兜底。 - 文件所属项目完整接入说明可对照 Dpz.Core.Service/README.md:
AddBusinessServices负责基础设施,AddDefaultServices负责源生成器生成的业务仓储服务,二者叠加后才构成业务服务层的完整 IoC 容器。