You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

依赖注入循环依赖排查:CleanArchitecture时区配置栈溢出问题

自定义时区实现导致栈溢出问题分析

我基于CleanArchitecture模板开发项目,想要实现用户自定义时区(存储在数据库),但运行时Visual Studio无报错,线程窗口提示「The maximum number of stack frames supported by Visual Studio has been exceeded」(栈溢出)。


相关代码

日期时间服务与接口

namespace MyApp.Infrastructure.Services;

public class DateTimeService : IDateTime
{
    private readonly IAppDefaultServices _appDefaultServices;
    public DateTimeService(IAppDefaultServices appDefaultServices)
    {
        _appDefaultServices = appDefaultServices;
    }
    public DateTime Now()
    {
        DateTime utcTime = DateTime.UtcNow;
        TimeZoneInfo defaultZone = TimeZoneInfo.FindSystemTimeZoneById(_appDefaultServices.DefaultTimeZone);
        DateTime appDateTime = TimeZoneInfo.ConvertTimeFromUtc(utcTime, defaultZone);
        return appDateTime;
    }
}

namespace MyApp.Application.Common.Interfaces;

public interface IDateTime
{
    DateTime Now();
}

应用默认配置服务与接口

namespace MyApp.Infrastructure.Services;

public class AppDefaultServices : IAppDefaultServices
{
    private readonly IApplicationDbContext _context;
    public AppDefaultServices(IApplicationDbContext context)
    {
        _context = context;
    }
    private AppDefault _appDefault => _context.AppDefaults.First();

    public string DefaultTimeZone => _appDefault.TimeZoneId == null ? "Some Standard Time" : _appDefault.TimeZoneId;

}

namespace MyApp.Application.Common.Interfaces;
public interface IAppDefaultServices
{
    public string DefaultTimeZone { get; }
}

依赖注入配置

namespace Microsoft.Extensions.DependencyInjection;

public static class ConfigureServices
{
    public static IServiceCollection AddInfrastructureServices(this IServiceCollection services, IConfiguration configuration)
    {
        services.AddTransient<IAppDefaultServices, AppDefaultServices>();
        services.AddTransient<IDateTime, DateTimeService>();
    }
}

ApplicationDbContext代码

namespace MyApp.Infrastructure.Persistence;

public class ApplicationDbContext : ApiAuthorizationDbContext<ApplicationUser>, IApplicationDbContext
{
    private readonly AuditableEntitySaveChangesInterceptor _auditableEntitySaveChangesInterceptor;

    public ApplicationDbContext(
        DbContextOptions<ApplicationDbContext> options,
        IOptions<OperationalStoreOptions> operationalStoreOptions,
        AuditableEntitySaveChangesInterceptor auditableEntitySaveChangesInterceptor) 
        : base(options, operationalStoreOptions)
    {
        _auditableEntitySaveChangesInterceptor = auditableEntitySaveChangesInterceptor;
    }

    protected override void OnModelCreating(ModelBuilder builder)
    {
        builder.ApplyConfigurationsFromAssembly(Assembly.GetExecutingAssembly());

        base.OnModelCreating(builder);
    }

    protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
    {
        optionsBuilder.AddInterceptors(_auditableEntitySaveChangesInterceptor);
    }

    public override async Task<int> SaveChangesAsync(CancellationToken cancellationToken = default)
    {
        return await base.SaveChangesAsync(cancellationToken);
    }
}

审计拦截器代码

namespace MyApp.Infrastructure.Persistence.Interceptors;
    
public class AuditableEntitySaveChangesInterceptor : SaveChangesInterceptor
{
    private readonly IDateTime _dateTime;

    public AuditableEntitySaveChangesInterceptor(
        IDateTime dateTime)
    {
        _dateTime = dateTime;
    }

    public override InterceptionResult<int> SavingChanges(DbContextEventData eventData, InterceptionResult<int> result)
    {
        UpdateEntities(eventData.Context);

        return base.SavingChanges(eventData, result);
    }

    public override ValueTask<InterceptionResult<int>> SavingChangesAsync(DbContextEventData eventData, InterceptionResult<int> result, CancellationToken cancellationToken = default)
    {
        UpdateEntities(eventData.Context);

        return base.SavingChangesAsync(eventData, result, cancellationToken);
    }

    public void UpdateEntities(DbContext? context)
    {
        if (context == null) return;

        foreach (var entry in context.ChangeTracker.Entries<BaseAuditableEntity>())
        {
            if (entry.State == EntityState.Added)
            {
                entry.Entity.Created = _dateTime.Now();
            } 

            if (entry.State == EntityState.Added || entry.State == EntityState.Modified)
            {
                entry.Entity.LastModified = _dateTime.Now();
            }
        }
    }
}

关键依赖关系

  • DateTimeService被AuditableEntitySaveChangesInterceptor使用,而该拦截器是ApplicationDbContext的构造参数之一。

问题根源分析

这是循环依赖+递归触发SaveChanges导致的栈溢出:

  1. 调用SaveChanges时,审计拦截器触发,调用_dateTime.Now()
  2. Now()方法调用_appDefaultServices.DefaultTimeZone,进而访问_context.AppDefaults.First()——触发EF Core查询操作
  3. EF Core执行查询时,需初始化ApplicationDbContext相关组件,而ApplicationDbContext构造依赖AuditableEntitySaveChangesInterceptor,该拦截器又依赖IDateTime,形成循环依赖
  4. 若AppDefault属于BaseAuditableEntity(审计实体),查询AppDefaults.First()时,EF Core会触发变更追踪,再次调用审计拦截器逻辑,形成无限递归,最终栈溢出

解决方向

  1. 缓存时区配置:
    • 启动时将时区配置加载到内存(如IMemoryCache),避免每次获取时间都查询数据库;或改用配置文件存储默认时区
  2. 打破循环依赖:
    • 调整服务生命周期,将IAppDefaultServices和IDateTime改为单例(需保证线程安全);或让DateTimeService不直接依赖数据库上下文,改用独立配置源
  3. 排除审计实体的查询操作:
    • 修改AuditableEntitySaveChangesInterceptor,在UpdateEntities中排除AppDefault实体;或判断当前操作类型,避免查询触发审计逻辑
  4. 使用无跟踪查询:
    • 在AppDefaultServices中用_context.AppDefaults.AsNoTracking().First(),避免EF Core对AppDefault实体做变更追踪,从而不触发审计拦截器

内容的提问来源于stack exchange,提问作者ecasper

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.21 04:36:13