web-dev-qa-db-ja.com

Entity Framework Coreを使用したBlazor同時実行問題

私の目標

新しいIdentityUserを作成し、同じBlazorページですでに作成されているすべてのユーザーを表示したいと思います。このページには:

  1. identityUserを作成するフォーム
  2. userManager.Usersプロパティを使用してすべてのユーザーを表示するサードパーティのグリッドコンポーネント(DevExpress Blazor DxDataGrid)。このコンポーネントは、データソースとしてIQueryableを受け入れます。

問題

(1)のフォームで新しいユーザーを作成すると、次の同時実行エラーが発生します。

InvalidOperationException:前の操作が完了する前に、このコンテキストで2番目の操作が開始されました。インスタンスメンバーは、スレッドセーフであるとは限りません。

問題は、CreateAsync(IdentityUser user)およびUserManager.Users同じDbContextを参照している

同じ問題を単純なリストに置き換えて再現したので、問題はサードパーティのコンポーネントとは関係ありません。

問題を再現する手順

  1. 認証を使用して新しいBlazorサーバー側プロジェクトを作成する
  2. 次のコードで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()}" });
        }
    }
    

気づいたこと

  • Entity FrameworkプロバイダーをSqlServerからSqliteに変更すると、エラーは表示されません。

システム情報

  • ASP.NET Core 3.1.0 Blazorサーバー側
  • SqlServerプロバイダーに基づくEntity Framework Core 3.1.0

私がすでに見たこと

IQueryableを使用する理由

IQueryableはページネーションとフィルタリングを直接クエリに適用できるため、サードパーティのコンポーネントのデータソースとしてIQueryableを渡したいと思います。さらに、IQueryableはCUD操作に敏感です。

5
Leonardo Lurci

サンプルをダウンロードしましたが、問題を再現することができました。この問題は、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>_を渡すと、

  1. それらは異なるスレッドでレンダリングされ、同じ問題を引き起こす可能性があります。
  2. _IQueryable<T>_を使用するコードにawaitが含まれている可能性が高く、同じ問題が再び発生します。

理想的には、必要なのは、サードパーティコンポーネントがデータを要求するイベントを持ち、何らかのクエリ定義(ページ番号など)を提供することです。他の人と同様に、Telerik Gridがこれを行うことを知っています。

そうすれば、次のことができます

  1. ロックを取得する
  2. フィルターを適用してクエリを実行する
  3. ロックを解除する
  4. 結果をコンポーネントに渡す

非同期コードでは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();
  }
}
_
1
Peter Morris

まあ、私はこれと非常によく似たシナリオを持っています。私が「解決」するのは、すべてをOnInitializedAsync()から

protected override async Task OnAfterRenderAsync(bool firstRender)
{
    if(firstRender)
    {
        //Your code in OnInitializedAsync()
        StateHasChanged();
    }
{

それは解決したようですが、私は証拠を見つけることを考えていませんでした。初期化からスキップしてコンポーネントの成功を構築させれば、さらに先に進むことができると思います。

/******************************更新****************** ************** /

私はまだ問題に直面しています、私は行くために間違った解決策を与えているようです。これで確認したところ Blazor前の操作が完了する前にこのコンテキストで2番目の操作が開始されました 問題が解決しました。原因私は実際に、dbContext操作を使用して多くのコンポーネントの初期化を処理しています。 @dani_herreraによると、一度に複数のコンポーネントを使用してInitを実行すると、問題が発生する可能性があります。私が彼の忠告に従って私のdbContextサービスを Transient に変更すると、問題は回避されます。

0
Leo Vun