Позвольте мне нарисовать вам эту картину. Однажды вы работали над проектом. У вас только что реализованы и работают ценные объекты - вы чувствуете себя прекрасно. Типа, действительно здорово. Вы смотрите в окно, а свиньи летают! Итак, вы просто тестируете небольшой фрагмент кода и попадаете здесь в следующую строку:

context.SaveChanges();

И БУМ! он сломался, свиньи падают с неба, и ваш компьютер выдает вам исключение, говоря что-то из следующего:

Сущность «‹entity›» совместно использует таблицу «‹entity›» со столбцом ›# ‹other_column›, но нет сущности этого типа с таким же значением ключа, которая была бы помечена как« добавлено ».

КАКИЕ!? Приходи еще? В моем последнем посте Использование объектов-значений с Entity Framework Core я цитировал информацию об ограничениях собственных типов в Entity Framework Core (EF Core). Запомните эту часть:

Нет поддержки необязательных (то есть допускающих значение NULL) принадлежащих типов, которые сопоставляются с владельцем в той же таблице (то есть с использованием разделения таблицы). Это связано с тем, что сопоставление выполняется для каждого свойства, у нас нет отдельного дозатора для нулевого комплексного значения a в целом.

А также помните, как я упоминал, что наличие базового класса для наших объектов значений поможет пометить эти объекты внутри нашей сущности? Да, это пригодится в этой статье.

Итак, вот что происходит. У вас была сущность, у которой был один или несколько объектов значений, которые были полностью и полностью нулевыми. Нет ссылки на адрес. Нада. Хорошо? Entity Framework (EF) не любит, когда у вас включено разделение таблицы и вы пытаетесь сохранить объект значения, имеющий значение NULL (если вы не помните, что такое разделение таблицы, прочтите мою последнюю статью здесь). Итак, какие есть решения?

  • Отключите разделение таблицы, вызвав .ToTable (‹string tableName›) в конфигурации объекта.
  • Не используйте объекты ценности, не страдайте и в конечном итоге не будьте несчастными.
  • Продолжайте читать эту статью.

Первый вариант, безусловно, самый простой и быстрый способ исправить, но если вы похожи на меня, то вы упрямый и, вероятно, мазохистский. Но, честно говоря, все должно работать, как ожидалось, когда вы вызываете SaveChanges () с объектом с нулевым значением. Второй вариант на самом деле не вариант, по крайней мере, я надеюсь, вы серьезно его не рассматриваете. Верно?…

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

Все мы знаем, что такое отражение и что оно может делать, по крайней мере, я надеюсь, что вы знаете, потому что мы будем использовать его, чтобы исправить нашу небольшую проблему. Вот предпосылка: мы собираемся переопределить метод SaveChanges в нашем DbContext и просканировать все сущности, которые содержат свойства типа класса ValueObject, а затем проверить, имеет ли это свойство значение null. Если это свойство имеет значение null, создайте новый экземпляр типа производного класса и назначьте его вышеупомянутому свойству ПЕРЕД вызовом base.SaveChanges (). Звучит довольно просто и понятно, а? Большой! давай перейдем к делу.

Он большой и серый.

Прежде всего, поговорим о слоне в комнате. Кажется ли то, о чем я только что говорил, разумным и уместным? В конце концов, размышления идут медленно! Позвольте мне возразить: «Да, это разумно и уместно, и никакое отражение не замедлит при правильном выполнении этого над вашими сущностями». Вот причина, по которой вы можете сделать это исправление практически без снижения производительности при сохранении. Система отслеживания EF Core кэширует информацию о свойствах всех отслеживаемых объектов в ее контексте. Если задуматься, это имеет смысл: EF должен знать, как создавать запросы на основе конфигураций для данного типа.

Пора на работу.

Так что давай продолжим. Нам нужно переопределить наш метод SaveChanges () в нашем классе «DbContext». Если вы используете и SaveChanges, и SaveChangesAsync, вам нужно будет переопределить оба и убедиться, что оба метода выполняют код, как показано ниже:

Я знаю, что только что уронил тебе огромный кусок кода, так что давай распакуем его. Во-первых, ради производительности традиционные циклы for намного превосходят своего аналога цикла foreach, и массивы обрабатываются быстрее, чем другие перечисляемые типы - мы это знаем.

  1. Нам нужно погрузиться в трекер изменений, перебрать наши отслеживаемые объекты и извлечь то, что называется ReferenceEntries. Вы можете распознать там другие типы записей, а некоторые из них являются подклассами других, например, "Навигация" или "Свойства"; это просто зависит от того, насколько детальным вы хотите быть. Нам нужны ссылки, которые, насколько я знаю, читая экран, означают типы, которые были настроены как принадлежащие типу, то есть настроены с помощью метода OwnsOne () Fluent API. Это приближает нас к тому, что мы ищем, но еще не совсем там. Помните, мы заботимся о собственных типах, которые мы создали с помощью нашего класса ValueObject из прошлой статьи.
  2. Затем мы переберем все ссылочные типы и найдем те, которые имеют значение NULL и являются подклассом нашего класса «ValueObject». Как мы делаем последнее? довольно просто, EF предоставляет свойство метаданных, содержащее всю кэшированную информацию о свойствах, которая нам нужна для достижения нашей цели выяснить, является ли это текущее свойство подклассом «ValueObject». Для чистоты я реализовал частный вспомогательный метод для решения этой проблемы под названием IsValueObject ().
  3. Затем, если мы действительно обнаруживаем, что текущая запись является подклассом «ValueObject» и ее текущее значение равно нулю. Затем нам нужно создать новый экземпляр этого производного типа и назначить его свойству текущего значения текущей записи. Это может быть сложно в зависимости от того, как вы можете с этим справиться, но для меня я знаю, что у меня будет либо общедоступный, либо частный конструктор без параметров в моих производных классах, поэтому я выбрал подход GetConstructor (), предоставляя необходимые флаги и тип аргументы, чтобы получить то, что мне нужно.

Как только я могу иметь или не иметь ConstructorInfo, который мне нужно использовать для вызова, я, наконец, проверяю, является ли моя информация ctor null или нет, если нет, то я вызываю invoke () без аргументов и назначаю экземпляр текущей записи CurrentValue свойство.

Заключение

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