Проблемы работы с Entity Framework на Blazor Server

Моя цель - предложение широкого ассортимента товаров и услуг на постоянно высоком качестве обслуживания по самым выгодным ценам.

Прежде чем перейти к статье, хочу вам представить, экономическую онлайн игру Brave Knights, в которой вы можете играть и зарабатывать. Регистируйтесь, играйте и зарабатывайте!

Blazor Server - технология для простого написания Web-систем на платформе .Net. Для удобной работы с базами данных была создана библиотека Entity Framework, которая позволяет работать программисту напрямую с моделями, не задумываясь об SQL-запросах. Но всё ли так хорошо, если соединить Blazor и EF?

Проблема №1 - два потока не могут одновременно работать с одним DbContext

A second operation started on this context before a previous operation completed. Any instance members are not guaranteed to be thread-safe.

Что это значит?

Для того чтобы понять из-за чего возникает такая ошибка, нужно знать одну вещь - DbContext не является Thread-save, то есть нельзя одновременно использовать один и тот же контекст в двух разных потоках (Из официальной документации Microsoft). В Blazor-е такая ошибка возникает зачастую из-за того, что несколько компонентов, которые во время инициализации обращаются к одному DbContext, создаются одновременно.

Пример

Представим, что Вам нужно написать страницу, на которой пользователь должен указать своё учебное заведение (ему нужно выбрать страну, город и само учебное заведение), а также образование, которое он уже получил (направление и специальность). Вся информация, которую выбирает пользователь хранится в БД. Для того, чтобы структура Вашей страницы была более понятной, Вы разделяете её на два компонента - один занимается местоположением, другой - образованием. Для этого Вы в каждом компоненте с помощью стандартной системы Blazor-овского Dependency Injection получаете DbContext, затем переопределяете метод OnInitialized (или OnInitializedAsync), где с помощью полученного контекста достаёте нужную Вам информацию из базы данных. Как итог, после запуска приложения и открытия этой страницы, дочерние компоненты создаются и работают с БД одновременно и Вы получаете вышеупомянутую ошибку.

Как решить?

Есть несколько вариантов решений:

  1. Получить всю нужную информацию из базы данных в родительском компоненте и передать её дочерним в качестве параметра. Кроме того, нужно убедиться, что у пользователя не будет возможности одновременно запустить методы, которые будут работать с базой данных - для этого можно использовать bool-переменную IsLoading, привязав её ко всем дочерним компонентам.
    Минус данного способа - усложнение структуры страницы и невозможность выполнять операции с базой данных асинхронно

  2. Создавать на каждый метод или компонент, который может выполняться асинхронно, новый DbContext. Такой функционал можно реализовать через DbContextFactory или IServiceScopeFactory.
    Минус данного способа - повышенное потребление ресурсов

Итог

Первый вариант решения подходит только для тех случаев, когда у Вас нет необходимости выполнять какие-то операции в БД асинхронно. Поэтому для всех остальных ситуациях нужно использовать второе решение.

Проблема №2 - не удается отследить экземпляр объекта, так как объект с таким Id уже существует

The instance of entity type cannot be tracked because another instance of this type with the same key is already being tracked

Что это значит?

Это значит, что у вас большие проблемы. Дело в том, что для того чтобы DbContext мог отследить изменения моделей, у него есть список TrackedEntities (Из официальной документации Microsoft). Изначально этот список пустой. Но после он будет наполняться моделями, которые взаимодействовали с этим контекстом. В случае если в этом списке уже будет нужная Вам модель и Вы создадите её копию, то после взаимодействия этой копии с DbContext, этот контекст не сможет её добавить к себе в список TrackedEntities чем и вызовет исключение.

Пример

Воспользуемся предыдущим примером. Допустим, Вы всё таки решили выбрать второй способ решения, а именно использовать IServiceScopeFactory и для каждого метода создавать новый DbContext. После того как пользователь укажет все выбранные им данные, Вам нужно сохранить их в базу данных (назовем модель, где будет хранится эта информация как UserInformation). В компоненте, где пользователь указывает своё образование, Вы сделали такой механизм - сначала пользователь выбирает направление, а после выбора, из БД вытягиваются специальности для выбранного направления. Но Вы сделали ошибку, из-за которой при инициализации компонента вытягивается список направлений, в каждом из которых уже есть список специализаций. Таким образом получается вот какой казус - специальности, которые привязаны к направлению, и специальности, которые доставались отдельно - в БД это хоть и одинаковые модели, но в памяти будут хранится как разные объекты (они были вытянуты из БД с помощью разных DbContext). Затем, если Вы попробуете сохранить выбранное направление и выбранную специализацию в модели UserInformation, там у Вас будет храниться привязанное направление со списком специализаций и выбранная специализация (Важно: модель выбранной специализации в UserInformation и та же модель в списке специализаций выбранного направления это два разных объекта). То есть, когда Вы будете сохранять в БД UserInformation через Insert/Update в новом DbContext, контекст попытается отследить все вложенные модели. Допустим, сначала он отследит выбранное направление с привязанными к нему специализациями. Потом он попытается сделать тоже самое с выбранной специализацией, но обнаружит, что такую модель он уже отслеживает, а что делать с его копией - не понятно. Именно поэтому у Вас вылетит вышеупомянутая ошибка.

Как решить?

  1. Использовать подход с DTO моделями. Таким образом Вы привязываетесь не к объектам из БД, а к их копиям. После изменений копий Вы заново достаёте из БД нужные Вам модели и переносите в них данные из копий.

    Минус данного способа - более сложная структура проекта, повышенное потребление ресурсов

  2. Не допускать копий. Если к контексту привязать модель, которую Вы получили из другого контекста, он будет уже использовать не новый объект, а тот что Вы к нему привязали.

    Минус данного способа - Вы не всегда сможете отследить повторение (особенно если у Вас сложная структура моделей и логика взаимодействия с ними), а это значит, что ошибка может возникнуть на проде в самый не подходящий момент.

  3. Не использовать различные контексты (первый вариант первой проблемы).

Итог

В случае, если у Вас не большой проект, а проблему нужно решить срочно - можете спокойно использовать 2-ой вариант. Но если Вы знаете, что ваш проект уже большой или будет большим, то лучше использовать первый вариант. Тем более, если правильно модифицировать 3-х слойную архитектуру проекта под это решение, Вы получите готовую основу для API проекта.

Так что же использовать?

На самом деле, всё зависит от сложности проекта. Но всё же, если Вы хотите избавиться от всех подобных проблем, используйте IServiceScopeFactory и DTO модели.

Источник: https://habr.com/ru/post/658865/


Интересные статьи

Интересные статьи

По специфике работы нам часто приходится иметь дело с самыми разными конфигурациями серверной инфраструктуры клиентов. Сегодня хотел бы поделиться моим опытом поиска первопричины сбоев в работе ре...
Доброго времени суток, хабражители! Меня зовут Димитрий Зуйков, и я из TrueConf. Нас тут уже много, мы рассказываем о продуктах компании, и что важнее – о подходах к их развитию. Мы отлично понимаем п...
Прим. перев.: месяц назад компания Kinvolk выпустила свой интерфейс для управления Kubernetes-кластерами. Новый Open Source-проект, пополнив уже немалочисленные ряды подобных решений, соч...
Angular — это быстрый фреймворк. Он даёт разработчикам обширные возможности по улучшению производительности за счёт тонких настроек. Правда, программистам практически никогда не требуется делать ...
Аппараты миссии «Чанъэ-4» (спускаемый модуль и ровер) успешно выполнили научные исследования во время своего пятого лунного дня на обратной стороне Луны, прислав на Землю уже более 6,6 GB нау...