【问题标题】:Blazor (Server) scoped object in dependency injection creating multiple instances依赖注入中的 Blazor(服务器)范围对象创建多个实例
【发布时间】:2020-03-02 10:00:16
【问题描述】:

出于演示目的,假设我有一个名为 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# .net asp.net-core blazor blazor-server-side


    【解决方案1】:

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

    这是由预渲染引起的,StateManager 没有被释放。

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

    @(等待 Html.RenderComponentAsync(RenderMode.ServerPrerendered)) @(等待 Html.RenderComponentAsync(RenderMode.Server))

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

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

    当您在RenderMode.Server 模式下以Layout 渲染它时:

    <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 实例保持不变(而StateManager.IdInitialize() 更改)。

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

    您的StateManager 没有实现IDisposable。如果我改变类如下:

    public class StateManager : IDisposable
    {
        ...
    }
    

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

    【讨论】:

    • 谢谢 itminus - 您的建议使我走上了正确的道路。我确实需要实现 IDisposable 以正确处理预渲染场景(能够保持预渲染)。至于多个 ID,它现在都像预期的那样引用同一个对象。你是对的,只是 ID 发生了变化。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2017-03-20
    • 1970-01-01
    • 2019-07-02
    • 2017-02-09
    • 1970-01-01
    • 2016-10-23
    相关资源
    最近更新 更多