首页 > 解决方案 > 依赖注入中的 Blazor(服务器)范围对象创建多个实例

问题描述

出于演示目的,假设我有一个名为 StateManager 的类:

public class StateManager
{
    public StateManager()
    {
        IsRunning = false;
    }

    public void Initialize()
    {
        Id = Guid.NewGuid().ToString();
        IsRunning = true;
        KeepSession();
    }

    public void Dispose()
    {
        Id = null;
        IsRunning = false;
    }

    public string Id { get; private set; }
    public bool IsRunning { get; private set; }

    private async void KeepSession()
    {
        while(IsRunning)
        {
            Console.WriteLine($"{Id} checking in...");
            await Task.Delay(5000); 
        }
    }
}

它有一个在启动后运行的方法,该方法每 5 秒将其 Id 写入控制台。

在我的 Startup 类中,我将其添加为 Scoped 服务:

services.AddScoped<StateManager>();

也许我使用了错误的位置,但在我的MainLayout.razor文件中,我在OnInitializedAsync()上对其进行了初始化

@inject Models.StateManager StateManager
...
@code{
    protected override async Task OnInitializedAsync()
    {
        StateManager.Initialize();
    }
}

在呈现第一页后运行应用程序时,控制台输出显示有 2 个实例正在运行:

bcf76a96-e343-4186-bda8-f7622f18fb27 正在登记...

e5c9824b-8c93-45e7-a5c3-6498b19ed647 正在登记...

如果我在对象上运行Dispose() ,它会在其中一个实例上结束KeepSession() while 循环,但另一个实例继续运行。如果我运行Initialize()会出现一个新实例,并且每次运行Initialize()都会生成新实例,并且它们都使用其唯一 ID 写入控制台。我可以无限制地创建任意数量的内容。

我认为将 Scoped<> 服务注入 DI 可以保证每个电路有一个该对象的单个实例?我还尝试在OnAfterRender()覆盖中进行初始化,以防预渲染过程创建双实例(尽管这不能解释为什么我可以在注入服务的页面中创建这么多实例)。

有什么我没有正确处理的吗?除了 MainLayout 之外,还有更好的位置来初始化 StateManager 吗?

标签: c#.netasp.net-coreblazorblazor-server-side

解决方案


我还尝试在 OnAfterRender() 覆盖中进行初始化,以防预渲染过程创建双实例

它是由预渲染和未处理引起的StateManager

但是你不能通过将初始化放在OnAfterRender(). 一个简单的方法是改用RenderMode.Server

<app>
     @(await Html.RenderComponentAsync<App>(RenderMode.ServerPrerendered)) 
    @(await Html.RenderComponentAsync<App>(RenderMode.Server))
</app>

由于您StateManager需要了解StateManagerEx,所以我们首先以一个假人StateManagerEx为例,这比您的场景更容易:

public class StateManagerEx
{
    public StateManagerEx()
    {
        this.Id = Guid.NewGuid().ToString();
    }
    public string Id { get; private set; }
}

Layout在模式下渲染它时RenderMode.Server

<p> @StateManagerEx.Id </p>

您只会获得一次 ID。但是,如果您在RenderMode.ServerPrerendered模式下渲染它,您会发现:

  1. 当浏览器向服务器发送请求时(但在 Blazor 连接建立之前),服务器会预先呈现应用程序并返回 HTTP 响应。这是第一次StateManagerEx创建。
  2. 然后在Blazor建立连接后,创建另一个StateManagerEx

我创建了一个屏幕录制并将每帧的持续时间增加+100ms,您可以看到它的行为与我们上面描述的完全相同(Id 发生了变化):

在此处输入图像描述

对于StateManager. 在ServerPrerendered模式下渲染时,将有两个StateManager,一个在 Blazor 连接建立之前创建,另一个驻留在电路中。因此,您将看到两个实例正在运行。

如果我运行 Initialize() 会出现一个新实例,并且每次运行 Initialize() 都会生成新实例,并且它们都使用其唯一 ID 写入控制台。

每当您运行Initialize()时,都会创建一个新Guid的。但是,StateManager实例保持不变( whileStateManager.Id由 更改Initialize())。

有什么我没有正确处理的吗?

StateManager 没有实现IDisposable. 如果我改变类如下:

public class StateManager : IDisposable
{
    ...
}

即使我渲染Appin模式,每个连接同时ServerPrerendered只有一个:91238a28-9332-4860-b466-a30f8afa5173 checking in...

在此处输入图像描述


推荐阅读