DbContext уничтожается после навигации

Вопрос или проблема

Я разрабатываю веб-приложение на 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() активируется в момент навигации на страницу редактирования игрока. Это происходит только на странице игрока, в то время как все остальные страницы обрабатываются без затруднений.

Причины возникновения ошибки

  1. Область видимости DbContext: Если ваш DbContext создается с неправильной областью видимости, он может быть уничтожен (disposed) до завершения обработки запросов. Убедитесь, что вы используете правильный жизненный цикл для вашего DbContext. Рекомендуется использовать Scoped, так как DbContext обычно должен существовать в пределах одного HTTP-запроса.

  2. Асинхронные вызовы и управление жизненным циклом: Если вы используете async/await для асинхронных операций с DbContext, важно удостовериться, что вы не пытаетесь использовать DbContext после его освобождения. Например, если в EditPlayer вы вызываете NavigateTo, а затем пытаетесь получить данные из DbContext на новом компоненте, это может произойти после его утилизации.

  3. Передача данных между страницами: При передаче параметров между страницами с использованием NavigateTo, нужно обращать внимание на то, как данные загружаются и используются. Если в вашем PlayerService при получении игрока вы делаете запрос с уже оконченной (disposed) сессией DbContext, произойдет эта ошибка.

  4. Синхронизация состояния: Убедитесь, что операции, которые требуют доступа к объектам, содержащимся в контексте базы данных, не вызываются до завершения навигации или после ее завершения.

Рекомендации по решению

  1. Проверьте жизненный цикл DbContext: Убедитесь, что DbContext зарегистрирован как Scoped в Startup.cs:

    services.AddDbContext<ApplicationDbContext>(options =>
        options.UseSqlServer(Configuration.GetConnectionString("DefaultConnection")));
  2. Отложенная асинхронная загрузка: Вместо того чтобы выполнять асинхронные вызовы непосредственно при навигации, перенесите логику получения асинхронных данных в OnInitializedAsync метода на странице редактирования:

    private async Task EditPlayer(int id, int teamId)
    {
        navigationManager.NavigateTo($"/player/{id}/{teamId}");
    
        // Загрузка данных может быть выполнена в отдельном методе на странице редактирования
    }
  3. Убедитесь в правильном использовании async: По возможности избегайте вызова асинхронных методов в async void. Старайтесь использовать async Task для более лучшего контроля ошибок и выполнения. Например, метод Submit также должен быть типа async Task.

  4. Логирование ошибок: Добавьте логирование ошибок для лучшего понимания того, когда и где возникает проблема. Используйте ILogger для записи информации, когда DbContext и запросы к базе данных выполняются.

Заключение

Ваша ситуация требует аккуратного управления DbContext в соответствии с жизненным циклом и порядком выполнения асинхронных операций. Убедитесь, что не происходит доступ к уже освободившемуся контексту базы данных, и придерживайтесь рекомендаций по его жизненному циклу и порядку загрузки. Это поможет избежать ошибок и обеспечит более стабильную работу вашего Blazor приложения.

Оцените материал
Добавить комментарий

Капча загружается...