Прежде чем перейти к статье, хочу вам представить, экономическую онлайн игру 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), где с помощью полученного контекста достаёте нужную Вам информацию из базы данных. Как итог, после запуска приложения и открытия этой страницы, дочерние компоненты создаются и работают с БД одновременно и Вы получаете вышеупомянутую ошибку.
Как решить?
Есть несколько вариантов решений:
Получить всю нужную информацию из базы данных в родительском компоненте и передать её дочерним в качестве параметра. Кроме того, нужно убедиться, что у пользователя не будет возможности одновременно запустить методы, которые будут работать с базой данных - для этого можно использовать bool-переменную IsLoading, привязав её ко всем дочерним компонентам.
Минус данного способа - усложнение структуры страницы и невозможность выполнять операции с базой данных асинхронноСоздавать на каждый метод или компонент, который может выполняться асинхронно, новый 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, контекст попытается отследить все вложенные модели. Допустим, сначала он отследит выбранное направление с привязанными к нему специализациями. Потом он попытается сделать тоже самое с выбранной специализацией, но обнаружит, что такую модель он уже отслеживает, а что делать с его копией - не понятно. Именно поэтому у Вас вылетит вышеупомянутая ошибка.
Как решить?
Использовать подход с DTO моделями. Таким образом Вы привязываетесь не к объектам из БД, а к их копиям. После изменений копий Вы заново достаёте из БД нужные Вам модели и переносите в них данные из копий.
Минус данного способа - более сложная структура проекта, повышенное потребление ресурсовНе допускать копий. Если к контексту привязать модель, которую Вы получили из другого контекста, он будет уже использовать не новый объект, а тот что Вы к нему привязали.
Минус данного способа - Вы не всегда сможете отследить повторение (особенно если у Вас сложная структура моделей и логика взаимодействия с ними), а это значит, что ошибка может возникнутьна продев самый не подходящий момент.Не использовать различные контексты (первый вариант первой проблемы).
Итог
В случае, если у Вас не большой проект, а проблему нужно решить срочно - можете спокойно использовать 2-ой вариант. Но если Вы знаете, что ваш проект уже большой или будет большим, то лучше использовать первый вариант. Тем более, если правильно модифицировать 3-х слойную архитектуру проекта под это решение, Вы получите готовую основу для API проекта.
Так что же использовать?
На самом деле, всё зависит от сложности проекта. Но всё же, если Вы хотите избавиться от всех подобных проблем, используйте IServiceScopeFactory
и DTO модели.