简体   繁体   中英

Blazor (Server) scoped object in dependency injection creating multiple instances

For demonstration purposes let's say I have a class called 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); 
        }
    }
}

It has a method that runs after it is initiated that writes it's Id to the console every 5 seconds.

In my Startup class I add it as a Scoped service:

services.AddScoped<StateManager>();

Maybe I am using the wrong location but in my MainLayout.razor file I am initializing it on OnInitializedAsync()

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

When running the application after it renders the first page the console output is showing that there are 2 instances running:

bcf76a96-e343-4186-bda8-f7622f18fb27 checking in...

e5c9824b-8c93-45e7-a5c3-6498b19ed647 checking in...

If I run Dispose() on the object it ends the KeepSession() while loop on one of the instances but the other keeps running. If I run Initialize() a new instance appears and every time I run Initialize() new instances are generated and they are all writing to the console with their unique id's. I am able to create as many as I want without limit.

I thought injecting a Scoped<> service into the DI guaranteed a single instance of that object per circuit? I also tried initializing within the OnAfterRender() override in case the pre-rendering process was creating dual instances (although this does not explain why I can create so many within a page that has the service injected).

Is there something I am not handling properly? Is there a better location to initialize the StateManager aside from MainLayout?

I also tried initializing within the OnAfterRender() override in case the pre-rendering process was creating dual instances

It is caused by pre-rendering & the StateManager is not disposed.

But you cannot avoid it by putting the initialization within OnAfterRender() . An easy way is to use the RenderMode.Server instead.

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

Since your StateManager requires a knowledge on StateManagerEx , let's firstly take a dummy StateManagerEx as an example, which is easier than your scenario:

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

When you render it in Layout in RenderMode.Server Mode:

<p> @StateManagerEx.Id </p>

You'll get the Id only once. However, if you render it in RenderMode.ServerPrerendered mode, you'll find that:

  1. When browser sends a request to server ( but before Blazor connection has been established), the server pre-renders the App and returns a HTTP response. This is the first time the StateManagerEx is created.
  2. And then after the Blazor connection is established, another StateManagerEx is created.

I create a screen recording and increase the duration of each frame by +100ms , you can see that its behavior is exactly the same as what we describe above (The Id gets changed):

在此处输入图像描述

The same goes for the StateManager . When you render in ServerPrerendered mode, there will be two StateManager , one is created before the Blazor connection has been established, and the other one resides in the circuit. So you'll see two instances running.

If I run Initialize() a new instance appears and every time I run Initialize() new instances are generated and they are all writing to the console with their unique id's.

Whenever you run Initialize() , a new Guid is created. However, the StateManager instance keeps the same ( while StateManager.Id is changed by Initialize() ).

Is there something I am not handling properly?

Your StateManager did not implements the IDisposable . If I change the class as below:

public class StateManager : IDisposable
{
    ...
}

even if I render the App in ServerPrerendered mode, there's only one 91238a28-9332-4860-b466-a30f8afa5173 checking in... per connection at the same time:

在此处输入图像描述

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM