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:
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
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é
Informations système
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.
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
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
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();
}
}
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.