Вопрос или проблема
Я разрабатываю веб-приложение на Blazor и сталкиваюсь с необычным поведением. Почти все страницы являются копиями, они имеют операции CRUD и предыдущий список различных элементов (сезоны, игроки, матчи и т. д.), но только на одной из них у меня возникают проблемы. Когда я нажимаю на кнопку ‘Редактировать’ игрока, я получаю сообщение ‘Невозможно получить доступ к удаленному объекту’, ссылаясь на _dbContext.
Добавив точку останова в метод DisposeAsync(), я обнаружил, что он срабатывает, когда я перехожу на страницу редактирования. Но это происходит только на странице игрока, а не на каких-либо других страницах.
Вот страница игроков:
@page "/players"
@attribute [Authorize]
@using Models = Biwenger.Models
@using Biwenger.Services
@inject PlayersService service;
@inject NavigationManager navigationManager;
<h3>Игроки</h3>
<a class="btn btn-primary" href="/player" role="button">Добавить</a>
<div class="mb-3 lg-6">
<label for="search" class="form-label">Поиск игрока:</label>
<input type="text" id="search" class="form-control" @bind="searchTerm" @oninput="FilterPlayers" placeholder="Введите имя игрока..." />
</div>
<table class="table">
<thead>
<tr>
<th scope="col">#</th>
<th scope="col">Имя</th>
<th scope="col">Команда</th>
<th scope="col">Действия</th>
</tr>
</thead>
<tbody>
@if (filteredList.Count > 0)
{
foreach (var item in filteredList)
{
<tr>
<th scope="row">@item.Id</th>
<td>@item.Name</td>
<td>@item.Team.Name</td>
<td>
<button type="button" class="btn btn-primary" @onclick="() => EditPlayer(item.Id, item.TeamId)">Редактировать</button>
</td>
</tr>
}
}
else
{
<tr>
<td colspan="4" class="text-center">Нет записей.</td>
</tr>
}
</tbody>
</table>
@code {
List<Models.Player> fullList = new List<Models.Player>();
List<Models.Player> filteredList = new List<Models.Player>();
string searchTerm = "";
protected override async Task OnInitializedAsync()
{
fullList = await service.GetAllPlayers();
filteredList = fullList;
}
private void FilterPlayers()
{
if (string.IsNullOrEmpty(searchTerm))
{
filteredList = fullList;
} else
{
filteredList = fullList.Where(p => p.Name.Contains(searchTerm, StringComparison.OrdinalIgnoreCase))
.ToList();
}
StateHasChanged();
}
private void EditPlayer(int id, int teamId)
{
navigationManager.NavigateTo($"/player/{id}/{teamId}"); <-- Здесь вызываетсяDispose
}
}
Это страница, на которую я перехожу:
@page "/player/{id:int?}/{teamId:int?}"
@attribute [Authorize]
@using Biwenger.Models.ViewModels
@using Biwenger.Services
@inject ILogger<Player> Logger
@inject PlayersService service
@inject TeamsService teamsService
@inject SeasonsService seasonsService;
@inject NavigationManager navigationManager
@inject IJSRuntime JS
<PageTitle>Команда</PageTitle>
<h3>@(model?.PlayerId == 0 ? "Новый игрок" : "Редактировать игрока")</h3>
<EditForm EditContext="editContext" OnValidSubmit="Submit" FormName="form">
<DataAnnotationsValidator />
<div class="col-mb-3 col-lg-6 col-md-6">
<label for="name" class="form-label">Имя</label>
<input id="name" class="form-control" @bind="model!.Name" @onblur="CheckIfNameExists" @ref="nameInput"></input>
</div>
<div class="col-mb-3 col-lg-6">
<ValidationMessage For="() => model!.Name"></ValidationMessage>
</div>
<div class="col-mb-3 col-lg-6 col-md-6">
<label for="team" class="form-label">Команда</label>
<InputSelect id="team" class="form-control" @bind-Value="model!.TeamId">
<option value="">Выберите команду</option>
@foreach (Models.Team team in listTeams)
{
<option value="@(team.Id)">@team.Name</option>
}
</InputSelect>
</div>
<div class="col-mb-3 col-lg-6">
<ValidationMessage For="() => model!.TeamId"></ValidationMessage>
</div>
<div class="col-mb-3 col-lg-6 col-md-6">
<label for="team" class="form-label">Позиция</label>
@if (positionsItems != null)
{
<BootstrapSelect
TItem="Models.DropdownItem<String>"
Data="@positionsItems"
@bind-Value="model!.Position"
TextField="@((item) => item.Value)"
ValueField="@((item) => item.Key.ToString())"
TType="Biwenger.Enums.Positions">
</BootstrapSelect>
}
</div>
<div class="col-mb-3 col-lg-6">
<ValidationMessage For="() => model!.TeamId"></ValidationMessage>
</div>
<div class="col-mb-3 col-lg-6 col-md-6">
<label for="cost" class="form-label">Стоимость @currentSeason!.Name</label>
<input type="number" id="cost" @bind="currentPlayerCost!.Cost" min="1" class="form-control text-end" @ref="costInput" @onfocus="selectAllText" @onblur="addMillions" />
</div>
<div class="form-check">
<input class="form-check-input" type="checkbox" value="@model!.Black" id="checkBlack" @bind="model!.Black" />
<label class="form-check-label" for="checkBlack"> Черный</label>
</div>
<div class="form-check">
<input class="form-check-input" type="checkbox" value="@model!.Active" id="checkActive" @bind="model!.Active" />
<label class="form-check-label" for="checkActive"> Активный</label>
</div>
<div class="col-mb-3 col-lg-6">
<button type="button" class="btn btn-secondary ms-2" @onclick="GoBack">Назад</button>
<button type="submit" class="btn @buttonClass" disabled="@(!editContext?.Validate() ?? false || isLoading)">
@if (isLoading)
{
<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span>
<span class="visually-hidden">Отправка...</span>
}
else if (showSuccess)
{
<span>Сохранено...</span>
}
else if (showError)
{
<span>Ошибка...</span>
}
else
{
<span>Отправить</span>
}
</button>
</div>
</EditForm>
@code {
private EditContext? editContext;
private ElementReference nameInput;
private ElementReference costInput;
private Biwenger.Models.Season? currentSeason;
private Biwenger.Models.PlayerSeasonCost? currentPlayerCost;
[Parameter]
public int? id { get; set; }
[Parameter]
public int? teamId { get; set; }
[SupplyParameterFromForm]
private PlayerWithCostViewModel? model { get; set; }
private List<Models.Team> listTeams = new List<Models.Team>();
private ValidationMessageStore? messageStore;
private bool isLoading = false;
private bool showSuccess = false;
private bool showError = false;
private string buttonClassSuccess = "btn-success";
private string buttonClassError = "btn-danger";
private string buttonClassIdle = "btn-primary";
private string buttonClass = "btn-primary";
IList<Models.DropdownItem<String>> positionsItems;
protected override async Task OnInitializedAsync()
{
positionsItems = new List<Models.DropdownItem<String>>
{
new Models.DropdownItem<String> { Key = 0, Value = "Позиция" },
new Models.DropdownItem<String> { Key = 1, Value = "Вратарь" },
new Models.DropdownItem<String> { Key = 2, Value = "Защитник" },
new Models.DropdownItem<String> { Key = 3, Value = "Полузащитник" },
new Models.DropdownItem<String> { Key = 4, Value = "Нападающий" },
};
editContext = new EditContext(model ??= new PlayerWithCostViewModel());
currentSeason = seasonsService.GetCurrentSeason();
currentPlayerCost = new Models.PlayerSeasonCost();
listTeams = await teamsService.GetAllTeams();
if (id.HasValue && id.Value > 0)
{
model = await service.GetPlayerWithCostById(id.Value, teamId!.Value);
if (model != null)
{
editContext = new EditContext(model);
}
}
messageStore = new ValidationMessageStore(editContext);
}
private async void CheckIfNameExists()
{
if (!string.IsNullOrEmpty(model!.Name))
{
bool exists = await service!.NameExists(model!.Name, id, model!.TeamId);
messageStore?.Clear(() => model.Name);
if (exists)
{
messageStore?.Clear();
messageStore?.Add(() => model.Name, "Имя уже существует");
}
editContext?.NotifyValidationStateChanged();
}
}
private void GoBack()
{
navigationManager.NavigateTo("/players");
}
private async Task Submit()
{
isLoading = true;
Logger.LogInformation("Вызван submit");
bool success = false;
if (editContext!.Validate())
{
if (id.HasValue && id.Value > 0)
{
success = await service.UpdatePlayer(model!, currentSeason!.Id);
}
else
{
if (model!.Costs!.Count == 0)
{
model!.Costs.Add(new Models.PlayerSeasonCost()
{
Cost = currentPlayerCost!.Cost,
SeasonId = currentSeason!.Id,
TeamId = model!.TeamId
});
}
success = await service!.AddPlayer(model!);
}
isLoading = false;
if (success)
{
showSuccess = true;
buttonClass = buttonClassSuccess;
StateHasChanged();
await Task.Delay(3000);
showSuccess = false;
buttonClass = buttonClassIdle;
StateHasChanged();
if (!id.HasValue)
{
model = new PlayerWithCostViewModel();
editContext = new EditContext(model);
messageStore = new ValidationMessageStore(editContext);
messageStore.Clear();
}
model!.Name = "";
model.TeamId = 0;
currentPlayerCost = new Models.PlayerSeasonCost();
model!.Black = false;
model!.Active = true;
StateHasChanged();
await JS.InvokeVoidAsync("focusElement", nameInput);
}
else
{
showSuccess = true;
buttonClass = buttonClassError;
await Task.Delay(1000);
showSuccess = false;
buttonClass = buttonClassIdle;
}
}
}
private async Task selectAllText()
{
await JS.InvokeVoidAsync("selectElementText", costInput);
}
private void addMillions()
{
if (currentPlayerCost!.Cost < 100)
{
currentPlayerCost.Cost = currentPlayerCost.Cost * 1000000;
}
}
}
И служба игроков:
using Biwenger.Data;
using Biwenger.Models;
using Biwenger.Models.ViewModels;
using Microsoft.EntityFrameworkCore;
namespace Biwenger.Services
{
public class PlayersService
{
private readonly ApplicationDbContext _dbContext;
public PlayersService(ApplicationDbContext dbContext)
{
_dbContext = dbContext;
}
public async Task<List<Player>> GetAllPlayers()
{
return await _dbContext.Players.Include(p => p.Team).AsNoTracking().ToListAsync();
}
public async Task<List<Player>> GetAllActivePlayers()
{
return await _dbContext.Players.Where(p => p.Active == true).AsNoTracking().ToListAsync();
}
public async Task<bool> AddPlayer(PlayerWithCostViewModel player)
{
var strategy = _dbContext.Database.CreateExecutionStrategy();
bool result = false;
await strategy.ExecuteAsync(async () =>
{
var transaction = await _dbContext.Database.BeginTransactionAsync();
try
{
var newPlayer = new Player
{
Active = player.Active,
Black = player.Black,
TeamId = player.TeamId,
Name = player.Name,
Position = player.Position,
};
await _dbContext.Players.AddAsync(newPlayer);
var playerSeasonCost = new PlayerSeasonCost
{
Cost = player.Costs!.First().Cost,
TeamId = player.TeamId,
PlayerId = newPlayer.Id,
SeasonId = player.Costs!.First().SeasonId,
};
await _dbContext.PlayersSeasonsCost.AddAsync(playerSeasonCost);
await _dbContext.SaveChangesAsync();
await transaction.CommitAsync();
result = true;
}
catch (Exception ex)
{
await transaction.RollbackAsync();
result = false;
}
});
return result;
}
public async Task<bool> UpdatePlayer(PlayerWithCostViewModel player, int seasonId)
{
Player? currentPlayer = await _dbContext.Players.FindAsync(player.PlayerId);
if (currentPlayer == null)
{
return false;
}
var transaction = await _dbContext.Database.BeginTransactionAsync();
try
{
currentPlayer.Name = player.Name;
currentPlayer.Black = player.Black;
currentPlayer.Position = player.Position;
currentPlayer.Active = player.Active;
currentPlayer.TeamId = player.TeamId;
_dbContext.Players.Update(currentPlayer);
await _dbContext.SaveChangesAsync();
await transaction.CommitAsync();
return true;
}
catch (Exception ex)
{
await transaction.RollbackAsync();
return false;
}
}
public async Task<Player?> GetPlayerByid(int id)
{
return await _dbContext.Players.FindAsync(id);
}
public async Task<PlayerWithCostViewModel?> GetPlayerWithCostById(int playerId, int teamId)
{
try
{
Player? player = await _dbContext.Players
.Include(p => p.PlayersSeasonsCost)
.Where(p => p.Id == playerId && p.TeamId == teamId)
.FirstOrDefaultAsync(); <-- _dbContext уже удален
if (player == null)
{
return null;
}
List<PlayerSeasonCost>? pscList = await _dbContext.PlayersSeasonsCost.Where(psc => psc.PlayerId == playerId).AsNoTracking().ToListAsync();
PlayerWithCostViewModel pwcvm = new PlayerWithCostViewModel
{
PlayerId = playerId,
TeamId = teamId,
Active = player.Active,
Black = player.Black,
Position = player.Position,
Name = player.Name,
Costs = pscList ??= new List<PlayerSeasonCost>()
};
return pwcvm;
}
catch (Exception ex)
{
Console.WriteLine(ex.Message);
return null;
}
}
public async Task<bool> NameExists(string name, int? id, int teamId)
{
if (id.HasValue && id.Value > 0)
{
return await _dbContext.Players.AnyAsync(t => t.Name == name && t.TeamId == teamId && t.Id != id);
}
return await _dbContext.Players.AnyAsync(t => t.Name == name && t.TeamId == teamId);
}
public async Task<List<PlayerWithCurrentCostVM>> GetPlayersWithCurrentCost()
{
List<PlayerWithCurrentCostVM> pwcc = new List<PlayerWithCurrentCostVM>();
pwcc = await _dbContext.Players.Include(p => p.PlayersSeasonsCost)
.ThenInclude(psc => psc.Season)
.Where(p => p.Active == true)
.Select(p => new PlayerWithCurrentCostVM
{
Player = p,
Name = p.Name,
Cost = p.PlayersSeasonsCost == null ? 0 : p.PlayersSeasonsCost.Where(psc => psc.Season.Active == true).First().Cost
})
.AsNoTracking()
.ToListAsync();
return pwcc;
}
}
}
Ответ или решение
Причины ошибки "Cannot access a disposed object" в Blazor Web-приложении при работе с DbContext
При разработке веб-приложений на платформе Blazor, вы можете столкнуться с проблемой, когда вызов методов, использующих DbContext
, завершает свою работу с ошибкой "Cannot access a disposed object". Данная ошибка часто возникает из-за того, что контекст базы данных (DbContext
) был освобожден (disposed) до того, как попытались получить к нему доступ. Давайте проанализируем вашу ситуацию.
Описание проблемы
В вашем случае вы столкнулись с проблемой при редактировании игрока через NavLink, что приводит к ошибке в стиле "Cannot access a disposed object", ссылающейся на _dbContext
. Обнаружено, что метод DisposeAsync()
активируется в момент навигации на страницу редактирования игрока. Это происходит только на странице игрока, в то время как все остальные страницы обрабатываются без затруднений.
Причины возникновения ошибки
-
Область видимости DbContext: Если ваш
DbContext
создается с неправильной областью видимости, он может быть уничтожен (disposed) до завершения обработки запросов. Убедитесь, что вы используете правильный жизненный цикл для вашегоDbContext
. Рекомендуется использоватьScoped
, так какDbContext
обычно должен существовать в пределах одного HTTP-запроса. -
Асинхронные вызовы и управление жизненным циклом: Если вы используете
async
/await
для асинхронных операций сDbContext
, важно удостовериться, что вы не пытаетесь использоватьDbContext
после его освобождения. Например, если вEditPlayer
вы вызываетеNavigateTo
, а затем пытаетесь получить данные изDbContext
на новом компоненте, это может произойти после его утилизации. -
Передача данных между страницами: При передаче параметров между страницами с использованием
NavigateTo
, нужно обращать внимание на то, как данные загружаются и используются. Если в вашемPlayerService
при получении игрока вы делаете запрос с уже оконченной (disposed) сессиейDbContext
, произойдет эта ошибка. -
Синхронизация состояния: Убедитесь, что операции, которые требуют доступа к объектам, содержащимся в контексте базы данных, не вызываются до завершения навигации или после ее завершения.
Рекомендации по решению
-
Проверьте жизненный цикл DbContext: Убедитесь, что
DbContext
зарегистрирован какScoped
вStartup.cs
:services.AddDbContext<ApplicationDbContext>(options => options.UseSqlServer(Configuration.GetConnectionString("DefaultConnection")));
-
Отложенная асинхронная загрузка: Вместо того чтобы выполнять асинхронные вызовы непосредственно при навигации, перенесите логику получения асинхронных данных в
OnInitializedAsync
метода на странице редактирования:private async Task EditPlayer(int id, int teamId) { navigationManager.NavigateTo($"/player/{id}/{teamId}"); // Загрузка данных может быть выполнена в отдельном методе на странице редактирования }
-
Убедитесь в правильном использовании
async
: По возможности избегайте вызова асинхронных методов вasync void
. Старайтесь использоватьasync Task
для более лучшего контроля ошибок и выполнения. Например, методSubmit
также должен быть типаasync Task
. -
Логирование ошибок: Добавьте логирование ошибок для лучшего понимания того, когда и где возникает проблема. Используйте
ILogger
для записи информации, когдаDbContext
и запросы к базе данных выполняются.
Заключение
Ваша ситуация требует аккуратного управления DbContext
в соответствии с жизненным циклом и порядком выполнения асинхронных операций. Убедитесь, что не происходит доступ к уже освободившемуся контексту базы данных, и придерживайтесь рекомендаций по его жизненному циклу и порядку загрузки. Это поможет избежать ошибок и обеспечит более стабильную работу вашего Blazor приложения.