.NET 6 WebAPI中MQTT接收消息时访问数据库遇上下文已释放错误
问题分析与解决方案
问题根源
- 生命周期不匹配:
MQTTCheck方法里的using (var scope = scopedFactory.CreateScope())会在service.check()执行后立即释放作用域,而check是async void方法,会直接返回,导致MQTTManager依赖的IUnitOfWork和DatabaseContext被提前销毁。但MQTT客户端是长连接的,后续收到消息时再访问已释放的上下文就会触发报错。 - 异步方法滥用:
check方法使用async void,这种写法在非事件处理程序中会导致异步操作脱离控制,无法追踪执行状态,进一步加剧了作用域提前释放的问题。
解决方案
1. 调整MQTTManager的注入生命周期
将MQTTManager改为单例注入,因为MQTT客户端是长生命周期的服务,需要持续监听消息:
// Program.cs中修改注入方式 builder.Services.AddSingleton<MQTTManager>();
2. 改用IServiceScopeFactory获取数据库上下文
长生命周期的单例服务不能直接依赖短生命周期的IUnitOfWork,需通过IServiceScopeFactory在每次处理消息时创建独立作用域,获取新鲜的上下文实例:
// MQTTManager.cs修改后的代码 public class MQTTManager { IMqttClient client; readonly IServiceScopeFactory _scopeFactory; string serverAddress = "XXXXXX"; int port = 1883; public MQTTManager(IServiceScopeFactory scopeFactory) { _scopeFactory = scopeFactory; } // 替换async void为async Task,避免异步失控 public async Task CheckAsync() { try { client = new MqttFactory().CreateMqttClient(); var options = new MqttClientOptionsBuilder() .WithClientId(Guid.NewGuid().ToString()) .WithTcpServer(serverAddress, port) .WithCleanSession() .WithWillRetain(false) .Build(); client.ApplicationMessageReceivedAsync += Client_ApplicationMessageReceivedAsync; await client.ConnectAsync(options); } catch (Exception ex) { // 建议添加日志记录,不要吞掉异常 } } private async Task Client_ApplicationMessageReceivedAsync(MqttApplicationMessageReceivedEventArgs x) { string topic = x.ApplicationMessage.Topic; string receiveMsg = x.ApplicationMessage.ConvertPayloadToString(); // 每次处理消息时创建独立作用域 using (var scope = _scopeFactory.CreateScope()) { var uow = scope.ServiceProvider.GetRequiredService<IUnitOfWork>(); Home home = uow.HomeRepository.Get(h => h.Name == topic); // ... 执行数据库操作 await uow.SaveChangesAsync(); // 如果有数据修改,记得保存 } await Task.CompletedTask; } }
3. 修正Program中的启动逻辑
确保异步方法执行完成,不再提前释放作用域:
// Program.cs修改后的启动代码 var builder = WebApplication.CreateBuilder(args); builder.Services.AddControllers(); builder.Services.AddDbContext<DatabaseContext>(options => options.UseSqlServer(builder.Configuration.GetSection("ConnectionStrings").GetSection(environment).Value)); builder.Services.AddTransient<IUnitOfWork, UnitOfWork>(); builder.Services.AddSingleton<MQTTManager>(); builder.Services.AddEndpointsApiExplorer(); var app = builder.Build(); // 使用异步方法启动MQTT服务 await MQTTCheckAsync(app); app.MapControllers(); app.Run(); async Task MQTTCheckAsync(IHost app) { var mqttManager = app.Services.GetRequiredService<MQTTManager>(); await mqttManager.CheckAsync(); }
额外注意事项
- 禁止在非事件处理程序中使用
async void,必须改用async Task,这样才能捕获异常、控制异步流程。 - 单例服务依赖短生命周期服务时,必须通过
IServiceScopeFactory创建临时作用域,避免上下文复用或提前释放的问题。 - 处理MQTT消息时,每个消息都使用独立的作用域,确保数据库上下文是全新的实例,用完即释放,符合EF Core的最佳实践。
内容的提问来源于stack exchange,提问作者SajjadZare
相关产品推荐
相关产品推荐

