Reputation: 141628
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
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