嵌套 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
实例的范围相同?
如果您不需要在您的对象上实施 IDisposable
或 IAsyncDisposable
,您的方法就会奏效。通常瞬态服务是创建后遗忘的。 Blazor Hub 会话中的 SPA 实例 Scoped 容器创建实例并将其移交,不维护对它的引用。但是对于一次性实例,有人需要 运行 处置对象。当 Scoped DI 容器被销毁时,该工作由 Scoped DI 容器本身完成。为此,容器维护它创建的所有一次性对象的列表。
您的代码中发生的事情是,当您按下 F5 键时,您正在重置 Hub 会话。作为清理过程的一部分,Scoped DI 容器 运行 对您的临时对象进行处置。
当您在 Singleton 服务中维护对对象的引用时,垃圾收集器不会删除它,但是 DisposeAsync
已经 运行。
这是一种方法(我还没有测试代码,所以可能会有一些拼写错误等)。
- 在需要时创建
IMkiiHardwareCommunication
个实例,然后将它们添加到服务中。
- 在服务中实现 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();
}
我已经使用 Blazor 几个月了,已经习惯了组件渲染和重新渲染的时间和方式。
然而,我正在努力处理 DI 注入对象在其他 DI 注入对象中的范围。
我在 render-mode="Server"
我的具体例子如下:
注册为单例的服务,因为我希望能够在任何组件中查询 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
实例的范围相同?
如果您不需要在您的对象上实施 IDisposable
或 IAsyncDisposable
,您的方法就会奏效。通常瞬态服务是创建后遗忘的。 Blazor Hub 会话中的 SPA 实例 Scoped 容器创建实例并将其移交,不维护对它的引用。但是对于一次性实例,有人需要 运行 处置对象。当 Scoped DI 容器被销毁时,该工作由 Scoped DI 容器本身完成。为此,容器维护它创建的所有一次性对象的列表。
您的代码中发生的事情是,当您按下 F5 键时,您正在重置 Hub 会话。作为清理过程的一部分,Scoped DI 容器 运行 对您的临时对象进行处置。
当您在 Singleton 服务中维护对对象的引用时,垃圾收集器不会删除它,但是 DisposeAsync
已经 运行。
这是一种方法(我还没有测试代码,所以可能会有一些拼写错误等)。
- 在需要时创建
IMkiiHardwareCommunication
个实例,然后将它们添加到服务中。 - 在服务中实现 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();
}