ASP.NET Core 6如何实现Transient服务代码仅首次请求执行
问题场景
在ASP.NET Core 6 MVC项目中,定义了Transient生存期服务ContentService,该服务继承自IContentService接口,在Program.cs中通过如下代码注册:
builder.Services.AddTransient<IContentService, ContentService>();
服务实现代码如下:
public interface IContentService { public string GetText(int id); } public class ContentService : IContentService { private readonly ContentDbContext _context; private List<string> textList; public ContentService(ContentDbContext context) { _context = context; ReadTexts(); } public void ReadTexts() { // 从数据库读取文本内容填充textList // 该段代码仅允许在首次请求时执行 } public string GetText(int id) { return textList[id]; } }
当前存在的问题:每次请求ContentService实例时都会执行构造函数,调用重负载方法ReadTexts(),但该方法仅需在首次请求时执行一次。约束条件:无法使用AddSingleton注册单例服务,需要实现Transient服务内指定代码块仅在首次请求时运行。
解决方案
核心思路是把需要一次性执行的重负载逻辑、需要跨实例共享的缓存数据,抽离到线程安全的单例级状态容器中,Transient服务每次实例化时仅访问这个共享容器,不需要重复执行读库逻辑,同时保持ContentService本身的Transient生命周期不变。
推荐实现(符合DI设计规范)
- 定义独立的线程安全缓存容器,用
Lazy<T>保证加载逻辑全程只执行一次,天然支持多线程并发场景,不需要手动编写锁逻辑:public class ContentTextCache { private readonly Lazy<List<string>> _texts; public ContentTextCache(Func<List<string>> loadFunc) { // ExecutionAndPublication模式保证多线程下委托只执行一次 _texts = new Lazy<List<string>>(loadFunc, LazyThreadSafetyMode.ExecutionAndPublication); } public List<string> Texts => _texts.Value; } - 在
Program.cs中把缓存容器注册为单例,注册时通过IServiceScopeFactory手动创建作用域获取Scoped生命周期的ContentDbContext,规避单例依赖Scoped服务的生命周期异常:builder.Services.AddSingleton(sp => { var scopeFactory = sp.GetRequiredService<IServiceScopeFactory>(); return new ContentTextCache(() => { using var scope = scopeFactory.CreateScope(); var dbContext = scope.ServiceProvider.GetRequiredService<ContentDbContext>(); // 迁移原ReadTexts方法内的读库逻辑 return dbContext.Contents.Select(c => c.Text).ToList(); }); }); // 原有Transient服务注册逻辑保持不变 builder.Services.AddTransient<IContentService, ContentService>(); - 修改
ContentService实现,移除构造函数内的ReadTexts()调用,直接注入缓存容器获取已加载好的数据:public class ContentService : IContentService { private readonly ContentDbContext _context; private readonly List<string> _textList; public ContentService(ContentDbContext context, ContentTextCache textCache) { _context = context; _textList = textCache.Texts; } public string GetText(int id) { return _textList[id]; } }
轻量实现(无需额外注册服务)
如果不想额外定义缓存类,也可以直接在ContentService类中使用静态Lazy<T>字段存储共享数据,所有Transient实例会共享同一个类级别的静态字段:
public class ContentService : IContentService { private readonly ContentDbContext _context; // 静态字段属于类级别,所有服务实例共享 private static readonly Lazy<List<string>> _textListLazy = new(() => { var httpContextAccessor = new HttpContextAccessor(); using var scope = httpContextAccessor.HttpContext?.RequestServices.CreateScope(); var dbContext = scope?.ServiceProvider.GetRequiredService<ContentDbContext>(); return dbContext?.Contents.Select(c => c.Text).ToList() ?? new List<string>(); }, LazyThreadSafetyMode.ExecutionAndPublication); private readonly List<string> _textList; public ContentService(ContentDbContext context) { _context = context; _textList = _textListLazy.Value; } public string GetText(int id) { return _textList[id]; } }
该写法不需要额外注册服务,但静态逻辑强依赖IHttpContextAccessor,可测试性差,仅适合快速实现简单场景。
方案说明
- 原有
ContentService始终保持Transient生命周期,每次注入都会生成新实例,完全符合注册约束 - 读库加载逻辑仅会在首次访问缓存时执行一次,后续所有Transient实例直接复用已加载的缓存数据,不会重复触发重负载操作
- 如果后续需要支持内容更新后刷新缓存,只需要在缓存容器中增加线程安全的刷新方法,在内容更新逻辑中调用即可,不需要调整服务生命周期配置
内容的提问来源于stack exchange,提问作者Jaber
相关产品推荐
相关产品推荐

