Blazor Server中嵌套依赖注入(DI)对象的作用域问题
你遇到的核心问题是对Blazor Server中DI服务的生命周期和作用域追踪逻辑理解有偏差:
在Blazor Server里,所有通过DI容器注入的服务(包括Transient)都是绑定到当前用户的Circuit(电路)作用域的。当你手动刷新页面时,旧的Circuit会被销毁,对应的DI作用域也会被释放——DI容器会自动调用这个作用域内所有实现了IAsyncDisposable/IDisposable的对象的释放方法,哪怕这些对象被Singleton服务(比如你的DeviceRepositoryGroup)持有。这就是为什么你刷新页面后会看到实例被释放、设备断开连接的原因。
你的DeviceRepositoryGroup是Singleton,确实不会随Circuit销毁,但你通过DI获取的IMkiiHardwareCommunication实例是在旧Circuit的作用域内创建的,DI容器会追踪这些实例的归属,当Circuit销毁时,不管谁持有它们,都会被自动释放。
要让IMkiiHardwareCommunication实例的生命周期和DeviceRepositoryGroup保持一致,你需要让Singleton服务完全控制这些实例的创建和生命周期,绕过DI容器的作用域追踪:
方案1:直接在Singleton中创建实例(最简单)
修改DeviceRepositoryGroup,让它直接实例化MkiiHardwareCommunication,而不是通过DI获取Transient实例:
public class DeviceRepositoryGroup : IAsyncDisposable { public Dictionary<string, IMkiiHardwareCommunication> MkiiDevices { get; } = new(); public void AddDevice(string ipAddress) { // 直接创建实例,DI容器不会追踪它的生命周期 var device = new MkiiHardwareCommunication(); device.IpAddress = ipAddress; MkiiDevices.Add(ipAddress, device); } public void RemoveDevice(string ipAddress) { if (MkiiDevices.TryGetValue(ipAddress, out var device)) { // 移除时手动释放实例 _ = device.DisposeAsync(); MkiiDevices.Remove(ipAddress); } } public async Task ClearAllDevices() { foreach (var device in MkiiDevices.Values) { await device.DisposeAsync(); } MkiiDevices.Clear(); } // 让Singleton自身实现IAsyncDisposable,应用关闭时清理所有设备 public async ValueTask DisposeAsync() { await ClearAllDevices(); GC.SuppressFinalize(this); } }
然后更新服务注册,让DI容器知道这个Singleton需要被释放:
builder.Services.AddSingleton<DeviceRepositoryGroup>(); builder.Services.AddSingleton<IAsyncDisposable>(sp => sp.GetRequiredService<DeviceRepositoryGroup>());
在DeviceManager.razor中,不再通过注入获取实例,而是调用AddDevice方法:
@page "/DeviceManager" @inject DeviceRepositoryGroup deviceRepositoryGroup @while (deviceRepositoryGroup.MkiiDevices.Count < 2) { deviceRepositoryGroup.AddDevice(IpAddressFromUI); }
方案2:通过IServiceProvider创建无作用域实例(适合有依赖的场景)
如果MkiiHardwareCommunication有其他依赖需要通过DI注入,可以在DeviceRepositoryGroup中注入IServiceProvider,用它来创建不绑定到Circuit作用域的实例:
public class DeviceRepositoryGroup : IAsyncDisposable { private readonly IServiceProvider _serviceProvider; public Dictionary<string, IMkiiHardwareCommunication> MkiiDevices { get; } = new(); public DeviceRepositoryGroup(IServiceProvider serviceProvider) { _serviceProvider = serviceProvider; } public void AddDevice(string ipAddress) { // 使用ActivatorUtilities创建实例,自动注入依赖,且不属于任何Circuit作用域 var device = ActivatorUtilities.CreateInstance<MkiiHardwareCommunication>(_serviceProvider); device.IpAddress = ipAddress; MkiiDevices.Add(ipAddress, device); } // 其余Remove/Clear/DisposeAsync方法和方案1一致 }
这样创建的实例不会被DI容器的Circuit作用域追踪,只有当DeviceRepositoryGroup被销毁(应用关闭)时,才会通过它的DisposeAsync方法被释放。
- 页面刷新后,根页面重新获取实例时,只要实例没被释放,连接状态会保持,不需要重新连接。
- 一定要记得在移除设备或Singleton销毁时手动调用
DisposeAsync,避免资源泄漏。
内容的提问来源于stack exchange,提问作者vdWes

