Blazor и ядро EF — разные потоки одновременно используют один и тот же экземпляр DbContext — проблема
Я пытаюсь перейти к компоненту Blazor, но компонент загружается дважды.
@page "/MySettings"
@using eKurser_Blazor.Data.Services;
@using eKurser_Blazor.DataDB;
@inject UserService UserService
@inject AuthenticationStateProvider AuthenticationStateProvider
<h1>Get profile</h1>
@if (ErrorMessage != null)
{
<div class = "alert alert-danger">@ErrorMessage</div>
}
<form>
<div class = "form-group">
<label for = "firstName">Fname</label>
<input type = "text" class = "form-control" id = "firstName" @bind = "@Fname" @value = "@Fname">
</div>
<div class = "form-group">
<label for = "lastName">Lname</label>
<input type = "text" class = "form-control" id = "lastName" @bind = "@Lname" @value = "@Lname">
</div>
<div class = "form-group">
<label for = "email">E-mail</label>
<input type = "email" class = "form-control" id = "email" @bind = "@Email" @value = "@Email">
</div>
</form>
Этот код будет выполнен дважды.
protected override async Task OnInitializedAsync()
{
var authState = await AuthenticationStateProvider.GetAuthenticationStateAsync();
var user = authState.User;
string userIdClaim = user.FindFirst("UserID")?.Value;
if (!string.IsNullOrEmpty(userIdClaim))
{
var _user = await UserService.GetUserByIdAsync(userIdClaim);
if (_user != null)
{
}
}
}
Я изменил _Host.cshtml в соответствии с:
<component type = "typeof(App)" render-mode = "Server" />
Blazor дважды отрисовывает содержимое
Но я закончу с ошибкой:
Ошибка: System.InvalidOperationException: уже существует открытое DataReader, связанное с этим соединением, которое необходимо сначала закрыть. в Microsoft.Data.SqlClient.SqlCommand.<>c.b__208_0(Task
1 result) at System.Threading.Tasks.ContinuationResultTaskFromResultTask
2.InnerInvoke() в System.Threading.ExecutionContext.RunInternal (ExecutionContext executeContext, обратный вызов ContextCallback, состояние объекта)
UserService.cs
public class UserService
{
private readonly MyDbContext _dbContext;
public UserService(MyDbContext dbContext)
{
_dbContext = dbContext;
}
public async Task<TblUser> AuthenticateAsync(string email, string password)
{
if (string.IsNullOrEmpty(email) || string.IsNullOrEmpty(password))
{
return null;
}
TblUser user = null;
try
{
user = await _dbContext.TblUsers.SingleOrDefaultAsync(x => x.Email == email);
}
catch (Exception ex)
{
Console.WriteLine($"Error: {ex.Message}");
return null;
}
if (user == null)
{
}
if (user == null || !PasswordHash.ValidatePassword(password, user.Pwd))
{
return null;
}
return user;
}
public async Task<TblUser> GetUserByIdAsync(string userId)
{
if (string.IsNullOrEmpty(userId))
{
return null;
}
var user = await _dbContext.TblUsers.SingleOrDefaultAsync(x => x.UserId == userId);
return user;
}
private bool UserExists(string userId)
{
return _dbContext.TblUsers.Any(e => e.UserId == userId);
}
Программа.cs
builder.Services.AddRazorPages();
builder.Services.AddServerSideBlazor();
builder.Services.AddScoped<UserService>();
builder.Services.AddScoped<CustomAuthenticationStateProvider>();
builder.Services.AddScoped<AuthenticationStateProvider>(provider => provider.GetRequiredService<CustomAuthenticationStateProvider>());
var connString = builder.Configuration.GetConnectionString("DefaultConnection");
builder.Services.AddDbContextFactory<MyDbContext>((DbContextOptionsBuilder option
) => option.UseSqlServer(connString));
// Add services to the container.
builder.Services.AddControllersWithViews();
Если я вызову метод вручную после загрузки компонента, проблем не возникнет, но я, конечно, хотел бы загружать и представлять данные так же автоматически, как загружается компонент.
<button type = "submit" class = "btn btn-primary" @onclick = "GetUserInfo">Ladda</button>
@code {
private async Task GetUserInfo()
{
var authState = await AuthenticationStateProvider.GetAuthenticationStateAsync();
var user = authState.User;
string userIdClaim = user.FindFirst("UserID")?.Value;
if (!string.IsNullOrEmpty(userIdClaim))
{
var _user = await UserService.GetUserByIdAsync(userIdClaim);
if (_user != null)
{
}
}
}
}
ОБНОВЛЕНИЕ 1
Отредактированный UserService.cs
private readonly IDbContextFactory<MyDbContext> _factory;
public UserService(IDbContextFactory<MyDbContext> factory)
{
_factory = factory;
}
Это устранило ошибку, но компонент по-прежнему дважды срабатывает OnInitializedAsync()
. И это покажет данные в первый раз, но после перехода на другую страницу и обратно var authState = await AuthenticationStateProvider.GetAuthenticationStateAsync();
будет пустым.
Обновление 2
Проблема отсутствия данных при навигации или обновлении заключалась в неправильной реализации сохранения AuthenticationState. Я установил Blazored.LocalStorage, чтобы сохранить состояние в localStorage.
OnInitializedAsync()
по-прежнему срабатывает дважды
Обновление 3
Я не совсем уверен, что я сделал, но исправление AuthenticationState и перезапуск VS решили мою проблему!
Ваши проблемы могут быть не связаны напрямую.
Давайте сначала разберемся с проблемой DbContext.
Вы пытаетесь использовать один и тот же DbContext
в двух операциях одновременно. Это распространенная проблема в асинхронных операциях. У вас уже есть IDbContextFactory
, загруженный в DI, вам просто нужно начать использовать его для создания контекстов транзакций/единиц работы.
Вот ваш UserService
:
public class UserService
{
private readonly IDbContextFactory _factory;
public UserService(IDbContextFactory factory)
{
_factory = factory;
}
//...
public async ValueTask SomeWorkAsync()
{
using var dbContext = _factory.CreateDbContext();
// await do something with dbContext
}
}
При двойной загрузке есть три возможные причины, по которым OnInitializedAsync
загружается дважды:
Куда вы направляетесь MySettings
и как?
Вам нужно установить, загружается ли код дважды или вы загружаете два разных компонента.
Добавьте это в MySettings
и проверьте
@code {
public readonly Guid ComponentUid = Guid.NewGuid();
protected override void OnInitialized()
{
Debug.WriteLine($"XXX OnInitialized called on Component Uid = {ComponentUid.ToString()}");
}
protected override Task OnInitializedAsync()
{
Debug.WriteLine($"XXX OnInitializedAsync called on Component Uid = {ComponentUid.ToString()}");
//...
}
}
И проверьте вывод.
Обновил мой пост. Как я могу продолжить и выяснить, как избежать двойного запуска загрузки OnInitializedAsync в моем компоненте?
Вы устранили проблему? Смотрите мой обновленный ответ, который показывает вам, как проверить последовательность загрузки.
Это решено, смотрите мой обновленный пост
Переход осуществляется из NavMenu.razor с помощью <NavLink class = "nav-link" href = "MySettings"> <span class = "oi oi-list-rich" aria-hidden = "true"></span> Min Sida </ навигационная ссылка>