嵌套 DI 对象范围的 Blazor 服务器问题

Blazor server issue with nested DI objects' scope

我已经使用 Blazor 几个月了,已经习惯了组件渲染和重新渲染的时间和方式。

然而,我正在努力处理 DI 注入对象在其他 DI 注入对象中的范围。

我在 render-mode="Server"

中使用 .NET6 和 Blazor 服务器

我的具体例子如下:

注册为单例的服务,因为我希望能够在任何组件中查询 IMkiiHardwareCommunication 的实例。

public class DeviceRepositoryGroup
{
    public Dictionary<string, IMkiiHardwareCommunication> MkiiDevices { get; }

    public void AddDevice<T>(string ipAddress, T device)
        where T : class
    {
        MkiiDevices.Add(ipAddress, device);
    }

    public void RemoveDevice(string ipAddress)
    {
        MkiiDevices.Remove(ipAddress);
    }

    public void ClearAllDevices()
    {
        MkiiDevices ??= new ();
        MkiiDevices.Clear();
    }
}

然后IMkiiHardwareCommunication接口描述了与Mkii设备通信所需的所有成员。为简洁起见,省略了几行:

public interface IMkiiHardwareCommunication : IAsyncDisposable
{
    Guid Id { get; }

    bool IsConnected { get; }

    string? IpAddress { get; set; }

    Task Connect(CancellationToken cancellationToken);

    Task Disconnect();
}


public class MkiiHardwareCommunication : IMkiiHardwareCommunication
{
    public Guid Id { get; }

    public bool IsConnected {get; private set; }

    public string? IpAddress { get; set; }

    public MkiiHardwareCommunication()
    {
        Id = Guid.NewGuid();
        IpAddress = "10.0.0.1"
        Console.WriteLine($"Created new MKII Instance with ID: {Id}");
    }

    Task Connect(CancellationToken cancellationToken)
    {
        // Code to connect
        IsConnected = true;
    }

    Task Disconnect()
    {
        // Code to disconnect
        IsConnected = false;
    }
    
    public async ValueTask DisposeAsync()
    {
        Console.WriteLine($"Disposed the MKII with ID: {Id}");
        try
        {
            if (IsConnected)
            {
                await Disconnect();
            }

            GC.SuppressFinalize(this);
        }
        catch (Exception)
        {
            throw;
        }
    }

}

所有服务注册如下:

builder.Services.AddTransient<IMkiiHardwareCommunication, MkiiHardwareCommunication>();
builder.Services.AddSingleton<DeviceRepositoryGroup>();

添加实例如下所示。这是在独立的 .razor 页面上完成的。引入循环是为了模拟多台设备被添加,请忽略重复添加的IP地址。

@page "/DeviceManager"

@inject IDeviceRepositoryGroup deviceRepositoryGroup

@while (deviceRepositoryGroup.MkiiDevices.Count() < 2)
{
    // I would expect to have 2 different instances, as it is registered as Transient.
    @inject IMkiiHardwareCommunication hardwareCommunication
                
    hardwareCommunication.IpAddress = IpAddressFromUI;
    deviceRepositoryGroup.AddDevice(hardwareCommunication.IpAddress, hardwareCommunication);
}

现在,当我最终想访问 .razor 组件中的 MkiiHardwareCommunication 实例时,我每次都执行以下操作:

@page "/"

<h1>@HardwareCommunication.IsConnected</h1>

@code {
    [Inject]
    public IDeviceRepositoryGroup DeviceRepositoryGroup { get; set; }

    public IMkiiHardwareCommunication? HardwareCommunication
    {
        get => DeviceRepositoryGroup.MkiiDevices[IpAddressFromUI];
    }
}

第一次导航到请求 MkiiHardwareCommunication 实例的页面(在本例中为 '/' 页面),没有发生任何有趣的事情,我得到了添加到 DeviceManager 中的实例页。

然而,当我手动刷新页面(F5)时,控制台的输出表明该实例已被销毁。然后断开设备。但是我仍然可以使用该实例,我只需要再次连接(我不想这样做,我希望它保持连接状态)。

我怀疑我对嵌套在具有自己生命周期的其他对象中的对象的生命周期了解不够,因为 DeviceRepositoryGroup 实例未在页面刷新时处理,所以我认为Dictionary<string, IMkiiHardwareCommunication> 会将所有 IMkiiHardwareCommunication 实例保留在范围内,只要 DeviceRepositoryGroup 实例在范围内。

我在这里错过了什么?有没有办法让 IMkiiHardwareCommunication 范围与 DeviceRepositoryGroup 实例的范围相同?

如果您不需要在您的对象上实施 IDisposableIAsyncDisposable,您的方法就会奏效。通常瞬态服务是创建后遗忘的。 Blazor Hub 会话中的 SPA 实例 Scoped 容器创建实例并将其移交,不维护对它的引用。但是对于一次性实例,有人需要 运行 处置对象。当 Scoped DI 容器被销毁时,该工作由 Scoped DI 容器本身完成。为此,容器维护它创建的所有一次性对象的列表。

您的代码中发生的事情是,当您按下 F5 键时,您正在重置 Hub 会话。作为清理过程的一部分,Scoped DI 容器 运行 对您的临时对象进行处置。

当您在 Singleton 服务中维护对对象的引用时,垃圾收集器不会删除它,但是 DisposeAsync 已经 运行。

这是一种方法(我还没有测试代码,所以可能会有一些拼写错误等)。

  1. 在需要时创建 IMkiiHardwareCommunication 个实例,然后将它们添加到服务中。
  2. 在服务中实现 dispose。
public class DeviceRepositoryGroup 
    : IAsyncDisposable
{
    // make sure you always have a dictionary
    public Dictionary<string, IMkiiHardwareCommunication> MkiiDevices { get; } = new Dictionary<string, IMkiiHardwareCommunication>();

    // Define TDevice needs to implement the IMkiiHardwareCommunication interface
    public void AddDevice<TDevice>(string ipAddress, TDevice device) where TDevice : IMkiiHardwareCommunication
        => MkiiDevices.Add(ipAddress, device);

    public async ValueTask RemoveDeviceAsync(string ipAddress)
    {
        var device = MkiiDevices[ipAddress];
        if (device != null)
        {
            await device.DisposeAsync();
            MkiiDevices.Remove(ipAddress);
        }
    }

    public async ValueTask ClearAllDevicesAsync()
    {
        foreach (var item in MkiiDevices)
        {
            await item.Value.DisposeAsync();
        }
        MkiiDevices.Clear();
    }

    public async ValueTask DisposeAsync()
        => await this.ClearAllDevicesAsync();
}

// Fixes the type of IMkiiHardwareCommunication at Instantiation
public class DeviceRepositoryGroup<TDevice> 
    : IAsyncDisposable
     where TDevice : IMkiiHardwareCommunication
{
    // make sure you always have a dictionary
    public Dictionary<string, TDevice> MkiiDevices { get; } = new Dictionary<string, TDevice>();

    // Define TDevice needs to implement the IMkiiHardwareCommunication interface
    public void AddDevice(string ipAddress, TDevice device)
        => MkiiDevices.Add(ipAddress, device);

    public async ValueTask RemoveDeviceAsync(string ipAddress)
    {
        var device = MkiiDevices[ipAddress];
        if (device != null)
        {
            await device.DisposeAsync();
            MkiiDevices.Remove(ipAddress);
        }
    }

    public async ValueTask ClearAllDevicesAsync()
    {
        foreach (var item in MkiiDevices)
        {
            await item.Value.DisposeAsync();
        }
        MkiiDevices.Clear();
    }

    public async ValueTask DisposeAsync()
        => await this.ClearAllDevicesAsync();
}