You need to enable JavaScript to run this app.
最新活动
大模型
产品
解决方案
定价
生态与合作
支持与服务
开发者
了解我们

.NET Core依赖注入:会话级、请求级生命周期及会话单实例注入问询

ASP.NET Core依赖注入:请求级与会话级生命周期详解

嘿,关于ASP.NET Core里DI的生命周期和会话级实例复用的问题,我来给你梳理清楚——这可是很多开发者在处理用户会话状态时都会踩的点~

一、先搞懂原生的请求级(Scoped)生命周期

首先,ASP.NET Core原生DI里的Scoped就是我们常说的请求级生命周期,核心逻辑很直白:

  • 每个HTTP请求进来时,DI容器会为这个请求单独创建该服务的全新实例
  • 请求处理完成、响应发送后,这个实例会被自动释放回收
  • 注册方式超简单:
    services.AddScoped<IMyRequestScopedService, MyRequestScopedService>();
    

这种生命周期特别适合处理请求内的临时数据,比如每个请求的用户上下文、单次请求的业务处理上下文,就连默认的DbContext都是Scoped的——毕竟没人想在多个请求里共享同一个数据库上下文,那可是并发问题的重灾区。

二、实现会话级服务:跨请求复用同一实例

可惜原生DI并没有直接提供“会话级”的生命周期选项,但如果你需要用户登录后,跨多个HTTP请求复用同一个服务实例(比如跟踪用户的会话状态、购物车数据等),我们可以通过结合Session和内存缓存来实现自定义的会话级服务。

步骤1:先配置Session

要跟踪用户会话,首先得确保你的项目已经启用了Session:
Program.cs里添加:

// 注册Session服务
builder.Services.AddSession(options =>
{
    options.IdleTimeout = TimeSpan.FromMinutes(30); // 会话超时时间,按需调整
    options.Cookie.HttpOnly = true; // 提升安全性,防止前端脚本访问
    options.Cookie.IsEssential = true; // 确保在用户未同意Cookie时也能使用(必要场景)
});

// 注册必要的辅助服务
builder.Services.AddHttpContextAccessor(); // 用于获取当前请求的HttpContext
builder.Services.AddMemoryCache(); // 用来存储会话级服务实例

然后在中间件管道里启用Session(注意顺序,要放在UseRouting之后,UseEndpoints之前):

app.UseSession();

步骤2:自定义会话级服务的注册逻辑

我们可以写一个扩展方法,让DI容器能够根据当前用户的SessionId来提供同一个服务实例:

public static class SessionScopedServiceExtensions
{
    public static IServiceCollection AddSessionScoped<TService, TImplementation>(this IServiceCollection services)
        where TService : class
        where TImplementation : class, TService
    {
        // 注册一个工厂委托,用来根据SessionId创建/获取服务实例
        services.AddScoped<Func<IServiceProvider, TService>>(sp =>
        {
            var memoryCache = sp.GetRequiredService<IMemoryCache>();
            var httpContextAccessor = sp.GetRequiredService<IHttpContextAccessor>();
            
            return provider =>
            {
                var httpContext = httpContextAccessor.HttpContext ?? 
                    throw new InvalidOperationException("无法获取当前HttpContext");
                
                // 确保Session已初始化(第一次请求时SessionId可能为空)
                if (string.IsNullOrEmpty(httpContext.Session.Id))
                {
                    httpContext.Session.SetString("SessionInit", "true");
                }
                var sessionId = httpContext.Session.Id;
                
                // 构建缓存键,确保每个服务类型+SessionId唯一
                var cacheKey = $"SessionScoped_{typeof(TService).FullName}_{sessionId}";
                
                // 从缓存获取实例,不存在则创建并缓存
                return memoryCache.GetOrCreate(cacheKey, entry =>
                {
                    // 缓存过期时间和Session超时保持一致
                    entry.AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(30);
                    // 使用ActivatorUtilities创建实例,支持构造函数注入
                    return ActivatorUtilities.CreateInstance<TImplementation>(provider, sessionId);
                });
            };
        });

        // 注册服务本身,使用上面的工厂委托来获取实例
        services.AddScoped<TService>(sp =>
        {
            var factory = sp.GetRequiredService<Func<IServiceProvider, TService>>();
            return factory(sp);
        });

        return services;
    }
}

步骤3:定义并注册你的会话级服务

比如我们创建一个用来跟踪用户会话请求次数的服务:

public interface ISessionTrackingService
{
    string SessionId { get; }
    int TotalRequests { get; }
    void IncrementRequestCount();
}

public class SessionTrackingService : ISessionTrackingService
{
    public string SessionId { get; }
    public int TotalRequests { get; private set; } = 0;

    // 构造函数注入SessionId(可选,用来验证实例归属)
    public SessionTrackingService(string sessionId)
    {
        SessionId = sessionId;
    }

    public void IncrementRequestCount()
    {
        TotalRequests++;
    }
}

然后在Program.cs里注册这个会话级服务:

builder.Services.AddSessionScoped<ISessionTrackingService, SessionTrackingService>();

步骤4:在Controller中使用

现在你可以在控制器里注入这个服务,同一个用户的多次请求都会拿到同一个实例:

public class HomeController : Controller
{
    private readonly ISessionTrackingService _sessionTrackingService;

    public HomeController(ISessionTrackingService sessionTrackingService)
    {
        _sessionTrackingService = sessionTrackingService;
    }

    public IActionResult Index()
    {
        _sessionTrackingService.IncrementRequestCount();
        ViewBag.SessionId = _sessionTrackingService.SessionId;
        ViewBag.TotalRequests = _sessionTrackingService.TotalRequests;
        return View();
    }
}

关键注意事项

  • 线程安全:如果你的会话级服务有状态,要注意并发请求的问题——同一个用户可能同时发起多个请求,这时要确保服务的方法是线程安全的(比如用lock或者线程安全的数据结构)
  • 主动清理实例:我们设置了缓存过期时间和Session一致,但如果用户主动注销,最好手动清除缓存里的实例,避免内存浪费:
    public IActionResult Logout()
    {
        var cacheKey = $"SessionScoped_{typeof(ISessionTrackingService).FullName}_{HttpContext.Session.Id}";
        var memoryCache = HttpContext.RequestServices.GetRequiredService<IMemoryCache>();
        memoryCache.Remove(cacheKey);
        
        // 其他注销逻辑,比如清除Cookie等
        return RedirectToAction("Login");
    }
    
  • 避免滥用:会话级服务适合存储用户会话期间的临时状态,但不要用来存储大量数据,避免造成不必要的内存压力。

三、总结:三种生命周期对比

生命周期类型实例创建时机适用场景
Singleton第一次请求时创建,全局唯一无状态的工具类、配置服务等
Scoped(请求级)每个HTTP请求创建一个新实例请求内的临时上下文、DbContext等
自定义会话级同一个用户会话的第一次请求创建,跨请求复用用户会话状态跟踪、购物车、会话级业务上下文等

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

火山引擎 最新活动