私の目標
新しいIdentityUserを作成し、同じBlazorページですでに作成されているすべてのユーザーを表示したいと思います。このページには:
問題
(1)のフォームで新しいユーザーを作成すると、次の同時実行エラーが発生します。
InvalidOperationException:前の操作が完了する前に、このコンテキストで2番目の操作が開始されました。インスタンスメンバーは、スレッドセーフであるとは限りません。
問題は、CreateAsync(IdentityUser user)およびUserManager.Users同じDbContextを参照している
同じ問題を単純なリストに置き換えて再現したので、問題はサードパーティのコンポーネントとは関係ありません。
問題を再現する手順
次のコードでIndex.razorを変更します。
@page "/"
<h1>Hello, world!</h1>
number of users: @Users.Count()
<button @onclick="@(async () => await Add())">click me</button>
<ul>
@foreach(var user in Users)
{
<li>@user.UserName</li>
}
</ul>
@code {
[Inject] UserManager<IdentityUser> UserManager { get; set; }
IQueryable<IdentityUser> Users;
protected override void OnInitialized()
{
Users = UserManager.Users;
}
public async Task Add()
{
await UserManager.CreateAsync(new IdentityUser { UserName = $"test_{Guid.NewGuid().ToString()}" });
}
}
気づいたこと
システム情報
私がすでに見たこと
IQueryableを使用する理由
IQueryableはページネーションとフィルタリングを直接クエリに適用できるため、サードパーティのコンポーネントのデータソースとしてIQueryableを渡したいと思います。さらに、IQueryableはCUD操作に敏感です。
サンプルをダウンロードしましたが、問題を再現することができました。この問題は、await
から呼び出されたコードでEventCallback
(つまりAdd
メソッド)を実行するとすぐにBlazorがコンポーネントを再レンダリングするために発生します。
_public async Task Add()
{
await UserManager.CreateAsync(new IdentityUser { UserName = $"test_{Guid.NewGuid().ToString()}" });
}
_
_System.Diagnostics.WriteLine
_をAdd
の先頭とAdd
の末尾に追加し、Razorページの上部と下部に1つずつ追加すると、ボタンをクリックすると、次の出力が表示されます。
_//First render
Start: BuildRenderTree
End: BuildRenderTree
//Button clicked
Start: Add
(This is where the `await` occurs`)
Start: BuildRenderTree
Exception thrown
_
このように、このメソッドの途中での再表示を防ぐことができます。
_protected override bool ShouldRender() => MayRender;
public async Task Add()
{
MayRender = false;
try
{
await UserManager.CreateAsync(new IdentityUser { UserName = $"test_{Guid.NewGuid().ToString()}" });
}
finally
{
MayRender = true;
}
}
_
これにより、メソッドの実行中に再レンダリングされなくなります。 Users
を_IdentityUser[] Users
_として定義した場合、await
が完了して遅延評価されない限り配列が設定されないため、この問題は発生しないことに注意してください。 tこの再入可能性の問題が発生します。
サードパーティのコンポーネントに渡す必要があるので、_IQueryable<T>
_を使用したいと思います。問題は、異なるコンポーネントが異なるスレッドでレンダリングされる可能性があるため、他のコンポーネントに_IQueryable<T>
_を渡すと、
IQueryable<T>
_を使用するコードにawait
が含まれている可能性が高く、同じ問題が再び発生します。理想的には、必要なのは、サードパーティコンポーネントがデータを要求するイベントを持ち、何らかのクエリ定義(ページ番号など)を提供することです。他の人と同様に、Telerik Gridがこれを行うことを知っています。
そうすれば、次のことができます
非同期コードではlock()
を使用できないため、SpinLock
のようなものを使用してリソースをロックする必要があります。
_private SpinLock Lock = new SpinLock();
private async Task<WhatTelerikNeeds> ReadData(SomeFilterFromTelerik filter)
{
bool gotLock = false;
while (!gotLock) Lock.Enter(ref gotLock);
try
{
IUserIdentity result = await ApplyFilter(MyDbContext.Users, filter).ToArrayAsync().ConfigureAwait(false);
return new WhatTelerikNeeds(result);
}
finally
{
Lock.Exit();
}
}
_
まあ、私はこれと非常によく似たシナリオを持っています。私が「解決」するのは、すべてをOnInitializedAsync()から
protected override async Task OnAfterRenderAsync(bool firstRender)
{
if(firstRender)
{
//Your code in OnInitializedAsync()
StateHasChanged();
}
{
それは解決したようですが、私は証拠を見つけることを考えていませんでした。初期化からスキップしてコンポーネントの成功を構築させれば、さらに先に進むことができると思います。
/******************************更新****************** ************** /
私はまだ問題に直面しています、私は行くために間違った解決策を与えているようです。これで確認したところ Blazor前の操作が完了する前にこのコンテキストで2番目の操作が開始されました 問題が解決しました。原因私は実際に、dbContext操作を使用して多くのコンポーネントの初期化を処理しています。 @dani_herreraによると、一度に複数のコンポーネントを使用してInitを実行すると、問題が発生する可能性があります。私が彼の忠告に従って私のdbContextサービスを Transient に変更すると、問題は回避されます。