M.M
M.M

Reputation: 141628

How to initialize two components sequentially on a Blazor page?

I'm using Blazor server-side (with pre-render disabled) with .NET 6. My page looks like:

<MyComponentA />
<MyComponentB />

and the components both look like:

@inject MyDbService db

...

@code {
    protected override async Task OnParametersSetAsync()
    {
        var v = await db.SomeSelect();
        // ... use v
    }
}

MyDbService is a Scoped service that uses EFCore and has a DbContext member, so there is only one active database connection. It's an external requirement for this task to only have a single database connection -- I don't want to use a Transient service instead.

This code causes a runtime error because the DbContext throws an exception if two components both try to use it concurrently. (i.e. DbContext is not re-entrant).

aside If I understand correctly, the flow is that db.SomeSelect in MyComponentA initiates, and await means the execution of the Page continues and eventually reaches db.SomeSelect in MyComponentB, which makes a second request on the same thread that still has the first call in progress.

My question is: Is there a tidy way to make MyComponentB not make its database call until MyComponentA has finished initializing?


One option would be to make MyComponentB do nothing until a specific function is called; and pass that function as parameter to MyComponentA to call when it's finished loading. But that feels pretty clunky and spaghetti so I wonder if there is a better way.

Upvotes: 0

Views: 575

Answers (1)

MrC aka Shaun Curtis
MrC aka Shaun Curtis

Reputation: 30177

Here's a lightweight scheduler that uses TaskCompletionSource objects to manually control Tasks passed back to the caller.

This is a simple demo, getting time with a configurable delay from a single source that throws an exception if you try and run it in parallel.

You should be able to apply this pattern to schedule sequential requests into your data pipeline.

The Scoped Queue Service:

public class GetTheTime : IAsyncDisposable
{
    private bool _processing;

    private Queue<TimeRequest> _timeQueue = new();
    private Task _queueTask = Task.CompletedTask;
    private bool _disposing;

    // only way to get the time
    public Task<string> GetTime(int delay)
    {
        var value = new TimeRequest(delay);

        // Queues the request
        _timeQueue.Enqueue(value);

        // Checks if the queue service is running and if not run it
        // lock it while we check and potentially start it to ensure thread safety
        lock (_queueTask)
        {
            if (_queueTask.IsCompleted)
                _queueTask = this.QueueService();
        }

        // returns the maunally controlled Task to the caller who can await it
        return value.CompletionSource.Task;
    }

    private async Task QueueService()
    {
        // loop thro the queue and run the enqueued requests till it's empty
        while (_timeQueue.Count > 0)
        {
            if (_disposing)
                break;

            var value = _timeQueue.Dequeue();

            // do the work and wait for it to complete
            var result = await _getTime(value.Delay);

            value.CompletionSource.TrySetResult(result);
        }
    }

    private async Task<string> _getTime(int delay)
    {
        // If more than one of me is running go BANG
        if (_processing)
            throw new Exception("Bang!");

        _processing = true;
        // Emulate an async database call
        await Task.Delay(delay);
        _processing = false;

        return DateTime.Now.ToLongTimeString();
    }

    public async ValueTask DisposeAsync()
    {
        _disposing = true;
        await _queueTask;
    }

    private readonly struct TimeRequest
    {
        public int Delay { get; }
        public TaskCompletionSource<string> CompletionSource { get; } = new TaskCompletionSource<string>();

        public TimeRequest(int delay)
            => Delay = delay;
    }
}

A simple Component:

@inject GetTheTime service
<div class="alert alert-info">
    @this.message
</div>

@code {
    [Parameter] public int Delay { get; set; } = 1000;

    private string message = "Not Started";

    protected async override Task OnInitializedAsync()
    {
        message = "Processing";
        message = await service.GetTime(this.Delay);
    }
}

And a demo page:

@page "/"

<PageTitle>Index</PageTitle>

<h1>Hello, world!</h1>

Welcome to your new app.

<Component Delay="3000" />
<Component Delay="2000" />
<Component Delay="1000" />

Upvotes: 1

Related Questions