依赖注入循环依赖排查: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导致的栈溢出:
- 调用
SaveChanges时,审计拦截器触发,调用_dateTime.Now() Now()方法调用_appDefaultServices.DefaultTimeZone,进而访问_context.AppDefaults.First()——触发EF Core查询操作- EF Core执行查询时,需初始化
ApplicationDbContext相关组件,而ApplicationDbContext构造依赖AuditableEntitySaveChangesInterceptor,该拦截器又依赖IDateTime,形成循环依赖 - 若
AppDefault属于BaseAuditableEntity(审计实体),查询AppDefaults.First()时,EF Core会触发变更追踪,再次调用审计拦截器逻辑,形成无限递归,最终栈溢出
解决方向
- 缓存时区配置:
- 启动时将时区配置加载到内存(如
IMemoryCache),避免每次获取时间都查询数据库;或改用配置文件存储默认时区
- 启动时将时区配置加载到内存(如
- 打破循环依赖:
- 调整服务生命周期,将
IAppDefaultServices和IDateTime改为单例(需保证线程安全);或让DateTimeService不直接依赖数据库上下文,改用独立配置源
- 调整服务生命周期,将
- 排除审计实体的查询操作:
- 修改
AuditableEntitySaveChangesInterceptor,在UpdateEntities中排除AppDefault实体;或判断当前操作类型,避免查询触发审计逻辑
- 修改
- 使用无跟踪查询:
- 在
AppDefaultServices中用_context.AppDefaults.AsNoTracking().First(),避免EF Core对AppDefault实体做变更追踪,从而不触发审计拦截器
- 在
内容的提问来源于stack exchange,提问作者ecasper
相关产品推荐
相关产品推荐

