web-dev-qa-db-fra.com

Problème de concurrence Blazor à l'aide d'Entity Framework Core

Mon objectif

Je souhaite créer un nouvel IdentityUser et afficher tous les utilisateurs déjà créés via la même page Blazor. Cette page contient:

  1. un formulaire à travers vous créera un IdentityUser
  2. un composant de grille tiers (DevExpress Blazor DxDataGrid) qui montre tous les utilisateurs utilisant la propriété UserManager.Users. Ce composant accepte un IQueryable comme source de données.

Problème

Lorsque je crée un nouvel utilisateur via le formulaire (1), j'obtiens l'erreur de concurrence suivante:

InvalidOperationException: une deuxième opération a démarré sur ce contexte avant la fin d'une opération précédente. Les membres d'instance ne sont pas garantis d'être thread-safe.

Je pense que le problème est lié au fait que CreateAsync (IdentityUser user) et UserManager.Users font référence au même DbContext

Le problème n'est pas lié au composant tiers car je reproduis le même problème en le remplaçant par une simple liste.

Étape pour reproduire le problème

  1. créer un nouveau projet côté serveur Blazor avec authentification
  2. changez Index.razor avec le code suivant:

    @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()}" });
        }
    }
    

Ce que j'ai remarqué

  • Si je change le fournisseur Entity Framework de SqlServer à Sqlite, l'erreur ne s'affichera jamais.

Informations système

  • ASP.NET Core 3.1.0 Blazor côté serveur
  • Entity Framework Core 3.1.0 basé sur le fournisseur SqlServer

Ce que j'ai déjà vu

Pourquoi je veux utiliser IQueryable

Je souhaite transmettre un IQueryable en tant que source de données pour le composant de mon tiers, car il peut appliquer la pagination et le filtrage directement à la requête. De plus, IQueryable est sensible aux opérations CUD.

5
Leonardo Lurci

J'ai téléchargé votre échantillon et j'ai pu reproduire votre problème. Le problème est dû au fait que Blazor effectuera un nouveau rendu du composant dès que vous await dans le code appelé à partir de EventCallback (c'est-à-dire votre méthode Add).

public async Task Add()
{
    await UserManager.CreateAsync(new IdentityUser { UserName = $"test_{Guid.NewGuid().ToString()}" });
}

Si vous ajoutez un System.Diagnostics.WriteLine Au début de Add et à la fin de Add, puis en ajoutez un en haut de votre page Razor et un en bas, vous verrez la sortie suivante lorsque vous cliquez sur votre bouton.

//First render
Start: BuildRenderTree
End: BuildRenderTree

//Button clicked
Start: Add
(This is where the `await` occurs`)
Start: BuildRenderTree
Exception thrown

Vous pouvez empêcher ce rendu de mi-méthode comme ça ...

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;
    }
}

Cela empêchera le nouveau rendu pendant l'exécution de votre méthode. Notez que si vous définissez Users comme IdentityUser[] Users, Vous ne verrez pas ce problème car le tableau n'est défini qu'une fois le await terminé et n'est pas évalué paresseusement, vous ne pas ce problème de réentrance.

Je pense que vous souhaitez utiliser IQueryable<T> Car vous devez le transmettre à des composants tiers. Le problème est que différents composants peuvent être rendus sur différents threads, donc si vous passez IQueryable<T> À d'autres composants alors

  1. Ils peuvent rendre sur différents threads et causer le même problème.
  2. Ils auront très probablement un await dans le code qui consomme le IQueryable<T> Et vous aurez à nouveau le même problème.

Idéalement, ce dont vous avez besoin est que le composant tiers ait un événement qui vous demande des données, vous donnant une sorte de définition de requête (numéro de page, etc.). Je sais que Telerik Grid fait cela, comme d'autres.

De cette façon, vous pouvez faire ce qui suit

  1. Acquérir une serrure
  2. Exécutez la requête avec le filtre appliqué
  3. Relâchez le verrou
  4. Transmettre les résultats au composant

Vous ne pouvez pas utiliser lock() dans le code asynchrone, vous devez donc utiliser quelque chose comme SpinLock pour verrouiller une ressource.

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

Eh bien, j'ai un scénario assez similaire avec cela, et je `` résous '' le mien est de tout déplacer de OnInitializedAsync () à

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

Cela semble résolu, mais je n'avais aucune idée de trouver les preuves. Je suppose qu'il suffit de sauter de l'initialisation pour laisser le succès du composant se construire, alors nous pouvons aller plus loin.

/******************************Mettre à jour****************** ************** /

Je suis toujours confronté au problème, il semble que je donne une mauvaise solution. Quand j'ai vérifié avec ceci Blazor Une deuxième opération a commencé sur ce contexte avant qu'une opération précédente ne soit terminée J'ai clarifié mon problème. Parce que je suis en train de gérer de nombreuses initialisations de composants avec les opérations dbContext. Selon @dani_herrera, mentionnez que si vous avez plus d'un composant exécutant Init à la fois, le problème apparaît probablement. Comme j'ai suivi son conseil de changer mon service dbContext en Transient , et je m'éloigne du problème.

0
Leo Vun