INNVTV
INNVTV

Reputation: 3367

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?

Upvotes: 4

Views: 3364

Answers (1)

itminus
itminus

Reputation: 25350

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))
    @(await Html.RenderComponentAsync<App>(RenderMode.Server)) 
</app>

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):

enter image description here

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:

enter image description here

Upvotes: 12

Related Questions