Проблема параллелизма Blazor с использованием Entity Framework Core - PullRequest
4 голосов
/ 15 января 2020

Моя цель

Я хочу создать нового IdentityUser и показать всех пользователей, уже созданных на одной странице Blazor. На этой странице:

  1. форма, через которую вы создадите IdentityUser
  2. сторонний компонент сетки (Dev Express Blazor DxDataGrid), который показывает всех пользователей, использующих свойство UserManager.Users , Этот компонент принимает IQueryable в качестве источника данных.

Задача

Когда я создаю нового пользователя через форму (1), я получаю следующий параллелизм ошибка:

InvalidOperationException: вторая операция началась в этом контексте до завершения предыдущей операции. Любые члены экземпляра не гарантируют поточно-ориентированность.

Я думаю, что проблема связана с тем, что CreateAsyn c (пользователь IdentityUser) и 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 На стороне сервера
  • Entity Framework Core 3.1.0 на основе SqlServer провайдер

То, что я уже видел

  • * 10 67 * Blazor Вторая операция началась в этом контексте до завершения предыдущей операции : предлагаемое решение не работает для меня, потому что даже если я изменю область действия DbContext с Scoped на Transient Я все еще использую тот же экземпляр UserManager, и он содержит тот же экземпляр DbContext
  • , другие парни из StackOverflow предлагают создать новый экземпляр DbContext для каждого запроса. Мне не нравится это решение, потому что оно противоречит принципам внедрения зависимостей. В любом случае, я не могу применить это решение, потому что DbContext обернут внутри UserManager
  • Создать генератор DbContext : это решение очень похоже на предыдущее.
  • Использование Entity Framework Core с Blazor

Почему я хочу использовать IQueryable

Я хочу передать IQueryable в качестве источника данных для своего третьего компонент -party, поскольку он может применять нумерацию страниц и фильтрацию непосредственно к запросу. Кроме того, IQueryable чувствителен к операциям CUD.

Ответы [ 4 ]

2 голосов
/ 01 мая 2020

Я нашел ваш вопрос, ища ответы о том же сообщении об ошибке, которое у вас было.

Моя проблема с параллелизмом, по-видимому, произошла из-за изменения, которое вызвало повторную визуализацию визуального дерева в том же самом сообщении. время (или из-за того, что) я пытался вызвать DbContext.SaveChangesAsyn c ().

Я решил эту проблему путем переопределения метода ShouldRender () моего компонента, например:

    protected override bool ShouldRender()
    {
        if (_updatingDb)
        { 
            return false; 
        }
        else
        {
            return base.ShouldRender();
        }
    }

Затем я завернул свой вызов SaveChangesAsyn c () в код, который соответствующим образом установил частное поле bool _updatingDb:

        try
        {
            _updatingDb = true;
            await DbContext.SaveChangesAsync();
        }
        finally
        {
            _updatingDb = false;
            StateHasChanged();
        }

Вызов StateHasChanged () может или не может быть необходимым, но я ' включил это на всякий случай.

Это исправило мою проблему, которая была связана с выборочным отображением привязанного тега ввода или просто текста в зависимости от того, редактировалось ли поле данных. Другие читатели могут обнаружить, что их проблема параллелизма также связана с чем-то, что вызывает повторную визуализацию. Если это так, эта техника может быть полезна.

2 голосов
/ 30 марта 2020

Общее решение

Я спрашивал Даниэля Рота BlazorDeskShow - 2: 24: 20 об этой проблеме, и, похоже, это проблема Blazor на стороне сервера по конструкции , Время жизни DbContext по умолчанию установлено на Scoped. Поэтому, если на одной странице есть как минимум два компонента, которые пытаются выполнить запрос asyn c, тогда мы встретим исключение:

InvalidOperationException: вторая операция началось в этом контексте до завершения предыдущей операции. Ни один из членов экземпляра не гарантированно является потокобезопасным.

Существует два обходного пути об этой проблеме:

  • (A) устанавливает время жизни DbContext на Переходный
services.AddDbContext<ApplicationDbContext>(opt =>
    opt.UseSqlServer(Configuration.GetConnectionString("DefaultConnection")), ServiceLifetime.Transient);
  • (B), как предложил Карл Франклин (после моего вопроса): создайте одиночный сервис с методом stati c, который возвращает новый экземпляр DbContext.

в любом случае, каждое решение работает, потому что они создают новый экземпляр DbContext.

О моей проблеме

Моя проблема не была строго связана с DbContext но с UserManager<TUser>, у которого Scoped время жизни. Установка времени жизни DbContext на Transient не решила мою проблему, потому что ASP. NET Ядро создает новый экземпляр UserManager<TUser>, когда я открываю сессию в первый раз, и оно живет до тех пор, пока я не закрою его. UserManager<TUser> находится внутри двух компонентов на одной странице. Тогда у нас есть та же проблема, описанная ранее:

  • два компонента, которые имеют один и тот же экземпляр UserManager<TUser>, который содержит переходный процесс DbContext.

В настоящее время я решил эту проблему проблема с другим обходным путем:

подсказок: обратите внимание на срок службы сервисов

Это то, что я узнал. Я не знаю, все ли это правильно или нет.

1 голос
/ 17 января 2020

Возможно, не лучший подход, но переписывание асинхронного c метода как не асинхронного c устраняет проблему:

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

Это гарантирует, что пользовательский интерфейс обновляется только после создания нового пользователя.


Весь код для Index.razor

@page "/"
@inherits OwningComponentBase<UserManager<IdentityUser>>
<h1>Hello, world!</h1>

number of users: @Users.Count()
<button @onclick="@Add">click me. I work if you use Sqlite</button>

<ul>
@foreach(var user in Users.ToList()) 
{
    <li>@user.UserName</li>
}
</ul>

@code {
    IQueryable<IdentityUser> Users;

    protected override void OnInitialized()
    {
        Users = Service.Users;
    }

    public void Add()
    {
        Task.Run(async () => await Service.CreateAsync(new IdentityUser { UserName = $"test_{Guid.NewGuid().ToString()}" })).Wait();            
    }
}
0 голосов
/ 16 апреля 2020

Ну, у меня есть довольно похожий сценарий с этим, и я «решаю», что мой - переместить все с OnInitializedAsyn c () на

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

Кажется, это решено , но я понятия не имел, чтобы выяснить, доказывает. Я полагаю, что просто пропустите инициализацию, чтобы обеспечить успех компонента, тогда мы можем go дальше.

...