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 }
                )
            );
    }
}
⚠⚠    以下内容为AI分析的结果,请根据实际情况进行判断。

文件报告:ServiceExtensions.cs

定位与职责

该文件是 Dpz.Core.Service(业务服务层基础设施项目)的依赖注入总入口。它对外暴露唯一扩展方法 AddBusinessServices(IServiceCollection, IConfiguration),把业务服务层运行所需的基础设施(MongoDB 仓储/工作单元、Mapster 映射、FusionCache 多级缓存、分布式锁、RabbitMQ+Outbox、Mediator、后台索引初始化等)一次性注册进容器;文件内部还包含一个私有的 FusionCache 装配方法。

调用方是各个应用层的 Program.cs / 测试基类,例如 Dpz.Core.Web/Program.csDpz.Core.Auth/Program.csDpz.Core.WebApi/Program.cs,以及 ServiceDependencyInjection.csAddDefaultServices() 配套使用(后者只负责图片格式探测与源生成器生成的仓储服务注册)。

AddBusinessServices 注册内容

按代码顺序分组如下。

1. 邮件与数据访问基础

  • IProtocolLoggerSerilogProtocolLogger.cs(Transient),把 MailKit 的 SMTP 协议日志接入 Serilog(需自行确认 Serilog 的注册位置,本文件只做映射)。
  • IUnitOfWorkUnitOfWork.cs(Transient),事务工作单元,依赖 MongoDB replica set 才能真正开启事务。
  • IRepository<>Repository<>(Scoped),泛型仓储,二者均来自 Dpz.Core.MongodbAccess

2. MongoDB 索引初始化

  • IMongoIndexInitializerMongoIndexInitializer(Singleton),实现在 MongodbAccess
  • 注册后台托管服务 MongoIndexInitializerService.csIHostedService),启动时借助 IDistributedLockProvider 抢分布式锁、扫描 Dpz.Core.Public.Entity 程序集中实现 IIndexedEntity<> 的实体并同步索引,且通过 FusionCache 记录最近一次成功时间(默认 6 小时内跳过)。

3. DEBUG 专用测试仓储工厂

#if DEBUG 分支下注册 Func<IRepository<Music>>(Scoped),从 mongodb-test 连接串构造测试库仓储,便于调试/测试场景隔离。

4. Mapster 对象映射

以 Singleton 注册 TypeAdapterConfigIMapper,实现均委托给 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. 业务服务与缓存

  • IBingWallpaperBingWallpaper(Scoped),见 IBingWallpaper.cs
  • AddFusionCacheServices(redisConnectionStr):见下文。
  • IInterceptRuleServiceInterceptRuleService.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.mdAddBusinessServices 负责基础设施,AddDefaultServices 负责源生成器生成的业务仓储服务,二者叠加后才构成业务服务层的完整 IoC 容器。
评论加载中...