You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.04.28 22:37:27