如何在中间件设置后维持HospitalContext的连接字符串状态?
问题分析与解决方案
当前问题的核心是:中间件与控制器获取的HospitalContext并非同一实例,且ConnectionsProvider存在线程安全隐患和赋值bug,导致连接字符串状态无法在请求内维持。以下是针对性的修复方案:
第一步:修复ConnectionsProvider的基础问题
原代码中SetConnection方法存在参数名与字段名冲突的bug,且注册为Singleton会导致多请求共享连接字符串,引发并发问题。
修改后的ConnectionsProvider代码
public interface IConnectionsProvider { string GetConnection(); void SetConnection(string tagConnection); string TagConnection { get; set; } } public class ConnectionsProvider : IConnectionsProvider { // 使用自动属性简化代码,避免字段与参数名冲突 public string TagConnection { get; set; } public ConnectionsProvider(){} public ConnectionsProvider(string tagConnection) { TagConnection = tagConnection; } public string GetConnection() => TagConnection; public void SetConnection(string tagConnection) { // 明确赋值给实例字段 this.TagConnection = tagConnection; } }
第二步:重构HospitalContext的配置方式
让DbContext通过依赖注入获取当前请求的连接信息,而非手动传入字符串,确保请求内的上下文实例共享同一连接配置。
方案1:通过OnConfiguring动态获取连接字符串
public class HospitalContext : DbContext { private readonly IConnectionsProvider _connectionsProvider; private DataConnection _dataConnection; // 依赖注入构造函数,接收请求级别的IConnectionsProvider public HospitalContext(IConnectionsProvider connectionsProvider) { _connectionsProvider = connectionsProvider; } protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) { // 仅在未配置时执行,避免重复配置 if (!optionsBuilder.IsConfigured) { var connectionTag = _connectionsProvider.GetConnection(); _dataConnection = connectionProvider.GetConnectionProvider(connectionTag); // 使用字符串插值简化拼接 var connectionString = $"Server={_dataConnection.serverName};Database={_dataConnection.dataBaseDefault};User ID={_dataConnection.userName};Password={_dataConnection.userPasword}"; optionsBuilder.UseSqlServer(connectionString); } base.OnConfiguring(optionsBuilder); } }
方案2(推荐):通过DbContextOptions预配置
更符合EF Core最佳实践,直接在DI注册时完成上下文配置:
// HospitalContext构造函数改为接收DbContextOptions public class HospitalContext : DbContext { public HospitalContext(DbContextOptions<HospitalContext> options) : base(options) { } }
第三步:修改中间件与DI注册
中间件调整(仅负责设置连接字符串)
无需手动创建HospitalContext,由DI容器管理实例生命周期:
public class ConnectionMiddleware { private readonly RequestDelegate _next; public ConnectionMiddleware(RequestDelegate next) { _next = next; } public async Task Invoke(HttpContext context, IConnectionsProvider connectionsProvider) { // 从请求头获取连接标识 if (!context.Request.Headers.TryGetValue("Conexion", out var connectionTag)) { context.Response.StatusCode = StatusCodes.Status400BadRequest; await context.Response.WriteAsync("缺少Conexion请求头"); return; } // 设置当前请求的连接标识 connectionsProvider.SetConnection(connectionTag); await _next(context); } }
Startup服务注册调整
public void ConfigureServices(IServiceCollection services) { services.AddControllers(); services.AddCors(); // 将ConnectionsProvider注册为Scoped,每个请求一个独立实例 services.AddScoped<IConnectionsProvider, ConnectionsProvider>(); // 方案1:注册HospitalContext(配合OnConfiguring方式) services.AddScoped<HospitalContext>(); // 方案2:注册HospitalContext(配合DbContextOptions方式) // services.AddScoped<HospitalContext>(provider => // { // var connectionsProvider = provider.GetRequiredService<IConnectionsProvider>(); // var connectionTag = connectionsProvider.GetConnection(); // var dataConnection = connectionProvider.GetConnectionProvider(connectionTag); // var options = new DbContextOptionsBuilder<HospitalContext>() // .UseSqlServer($"Server={dataConnection.serverName};Database={dataConnection.dataBaseDefault};User ID={dataConnection.userName};Password={dataConnection.userPasword}") // .Options; // return new HospitalContext(options); // }); services.AddAuthentication("BasicAuthentication") .AddScheme<AuthenticationSchemeOptions, BasicAuthenticationHandler>("BasicAuthentication", null); services.AddScoped<IUserService, UserService>(); }
中间件管道配置
确保中间件在路由和控制器之前执行:
public void Configure(IApplicationBuilder app, IWebHostEnvironment env) { if (env.IsDevelopment()) { app.UseDeveloperExceptionPage(); } app.UseHttpsRedirection(); app.UseMiddleware<ConnectionMiddleware>(); // 放在路由前 app.UseRouting(); app.UseAuthentication(); app.UseAuthorization(); app.UseEndpoints(endpoints => { endpoints.MapControllers(); }); }
第四步:控制器调整
直接注入DI提供的HospitalContext即可,无需手动处理连接:
[ApiController] [Route("[controller]")] public class WeatherForecastController : ControllerBase { private readonly HospitalContext _hospitalContext; public WeatherForecastController(HospitalContext dbContext) { _hospitalContext = dbContext; } [HttpGet] public async Task<IActionResult> Get() { // 直接使用配置好的上下文查询数据 var data = await _hospitalContext.SomeTable.ToListAsync(); return Ok(data); } }
核心原理说明
- Scoped生命周期:
IConnectionsProvider和HospitalContext都注册为Scoped,确保每个请求拥有独立的实例,避免并发冲突。 - DI实例共享:中间件设置的连接字符串会被当前请求的
IConnectionsProvider实例持有,后续DI创建的HospitalContext会自动使用该实例的连接信息,保证请求内上下文状态一致。 - 避免手动实例化:由DI容器统一管理
HospitalContext的创建,确保控制器和中间件使用同一实例。
内容的提问来源于stack exchange,提问作者Emerson Rios
相关产品推荐
相关产品推荐

