Глубокое копирование/Дублирование объекта со свойствами виртуальной навигации

#c# #entity-framework #blazor #blazor-server-side

#c# #структура организации #блейзор #blazor-серверная часть

Вопрос:

Я работаю в C#/Blazor

У меня есть объект, скажем a Project , который я извлекаю из базы данных, которая поставляется с внешними ключами и связанными с ними навигационными свойствами. Я извлекаю объект, а затем использую его в отключенном состоянии.

Как только объект извлечен, он загружается в форму для отображения/редактирования/обновления по мере необходимости. Я хочу создать отдельный клон Project для использования в форме в качестве DTO, чтобы любые изменения можно было отбросить без проблем со ссылкой на извлеченный оригинал Project .

Например, это упрощенный Project класс:

 public partial class Project  {  [Key]  public int Id { get; set; }  [Required]  [StringLength(150)]  public string ProjectName { get; set; }  [Column("UpdatedBy_Fk")]  public int UpdatedByFk { get; set; }   [ForeignKey(nameof(UpdatedByFk))]  [InverseProperty(nameof(UserData.ProjectUpdatedByFkNavigations))]  public virtual UserData UpdatedByFkNavigation { get; set; }  }  

В форме я отобразил полное имя последнего человека, который обновил Project с помощью @project.UpdatedByFkNavigation.FullName . Пользователь вообще не может изменять поле навигации, оно только отображается.

Мой вопрос касается копирования элементов навигации. Для простоты теперь, внутри формы OnInitialized , я передаю форме исходный project объект и создаю новый objProject , используя конструктор, подобный этому:

 Project objProject = new() { Id = project.Id,   ProjectName = project.ProjectName,  UpdatedByFk = project.UpdatedByFk,  UpdatedByFkNavigation = project.UpdatedByFkNavigation,   

Это, по-видимому, работает и для создания отдельного Project объекта, который не является ссылками и который я могу использовать в качестве своего DTO, однако я не уверен, уместно ли назначать virtual свойства таким образом.

Соответствует ли этот подход рекомендациям по созданию копии объекта без ссылок с виртуальными полями навигации или я должен подойти к этому по-другому?

Ответ №1:

Это зависит от отношений. Ссылки важны в EF, поэтому вам необходимо решить, хотите ли вы, чтобы новый клон ссылался на те же данные пользователя или на новые и отличные данные пользователя с теми же данными. Обычно в отношениях «Многие к одному» требуется использовать одну и ту же ссылку или обновить ссылку для соответствия. Если оригинал был изменен идентификатором «Джон Смит» № 201, клон будет изменен идентификатором «Джон Смит» № 201 или изменен на идентификатор текущего пользователя «Джейн Доу» № 405, который будет такой же ссылкой «Джейн Доу», как и любая другая запись, измененная пользователем. Вы, вероятно, не захотите, чтобы EF создавал нового «Неизвестного Джона», который в конечном итоге получил бы идентификатор #545, потому что EF получил совершенно новую ссылку на данные пользователя, в которых есть копия «Неизвестного Джона».

Поэтому в вашем случае я бы предположил, что вы захотите сослаться на один и тот же существующий экземпляр пользователя, поэтому ваш подход верен. Где вам нужно быть осторожным, так это при использовании ярлыка, такого как Сериализация/десериализация, для создания клонов. В этом случае сериализация проекта и любая загруженная ссылка UpdatedBy создадут новый экземпляр данных пользователя с теми же полями и даже значением PK. Однако, когда вы перейдете к сохранению этого нового проекта с его новой ссылкой на данные пользователя, вы либо получите повторяющееся исключение PK, исключение «Объект с тем же ключом, который уже отслежен», либо получите новую запись «Неизвестный» с идентификатором #545, если эта сущность настроена на ожидание столбца идентификатора для своего PK.

Относительно типичных рекомендаций по использованию навигационных свойств по сравнению Поля FK: Мой совет-использовать одно или другое, а не оба. Причина этого в том, что при использовании обоих у вас есть два источника истины для отношений, и в зависимости от состояния сущности, когда вы меняете один, другой не обязательно отражает изменение автоматически. Например , в каком-то коде я смотрю на отношения, перейдя: project.UpdatedByFk , в то время как другой код может использоваться project.UpdatedByFkNavigation.Id . Ваше соглашение об именовании немного странное, когда дело доходит до свойства навигации. Для вашего примера я бы ожидал:

 public virtual UserData UpdatedBy { get; set; }  

В общем, я бы использовал исключительно свойство навигации и полагался на свойство тени в EF для FK. Это выглядело бы так:

 public partial class Project {  [Key]  public int Id { get; set; }  [Required]  [StringLength(150)]  public string ProjectName { get; set; }   [ForeignKey("UpdatedBy_Fk")] // EF Core.. For EF6 this needs to be done via configuration using .Map(MapKey()).  public virtual UserData UpdatedBy { get; set; } }  

Здесь мы определяем свойство навигации, и, назначив имя столбца FK, EF создаст поле за кулисами для этого FK, которое недоступно напрямую. Наш код раскрывает один источник истины для отношений.

В некоторых случаях, когда важна скорость и мне практически не нужны соответствующие данные, я объявлю свойство FK и свойство навигации отсутствует.

В связи с этим:

 [InverseProperty(nameof(UserData.ProjectUpdatedByFkNavigations))]  

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

 var projects = context.Users  .Where(x =gt; x.Id == userId)  .SelectMany(x =gt; x.UpdatedProjects)  .ToList();  

Я бы просто использовал:

 var projects = context.Projects  .Where(x =gt; x.UpdatedBy.Id == userId)  .ToList();  

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

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

Комментарии:

1. Вау, Стив, это удивительно полезно. Спасибо. Я действительно хочу сослаться на тот же экземпляр UserData для этого обновления, и я пытаюсь обновить только Project не все связанные объекты навигации. Объекты навигации используются более или менее для отображения, но я понимаю, что вы говорите о возможном создании будущих ошибок при сохранении. Я собираюсь перечитать этот ответ несколько раз и убедиться, что я правильно его применяю. Здесь есть о чем подумать.

2. Для ясности того, как были созданы свойства и поля навигации, мы используем подход, основанный на базе базы данных, с помощью основных инструментов EF для обратного проектирования классов и DbContext. Я создал таблицу Бд Project , которая включает CreatedByFk ссылки на ограничения и ограничения UserData . Реверсивный инженер выплевывает обратное свойство, и все это автоматически основано на ограничении. Мне нужно переоценить это и убедиться, что это не делает что-то неподобающее.

3. Ах, это объяснило бы название. Возможно, есть способы настроить этот инструмент для ожидания и обработки соглашения об именовании. Т. Е. он может ожидать чего-то вроде «CreatedById» и лучше справился бы с этим именем, но его можно настроить для обработки «*Fk». Честно говоря, я никогда не был поклонником использования дизайнеров/инструментов для отображения EF. У самого EF есть некоторые странности с соглашениями, когда дело доходит до ожиданий имен FK. В худшем случае вы могли бы использовать его, чтобы помочь настроить начальные сопоставления, исправить странные, которые он предлагает, а затем взять на себя управление отображением вручную.

4. Кроме того, инструменты, как правило, хотят отображать отношения, не зная о том, какие из них на самом деле достаточно важны для полезных двунаправленных ссылок, поэтому они отображают все . Еще одна причина использовать их в качестве первого прохода, а затем удалить все, что не является необходимым. Ссылки ничего не нарушат, но они могут добавить «шум» при работе с доменом со свойствами, которые никогда не используются, или путаницу, когда существует несколько способов учета получения данных, которые могут привести к неожиданной ленивой загрузке.

5. Определенно, существуют параметры конфигурации для соглашений об именах и других вещей, которые вы можете настроить, но я не слишком углублялся в них. Я довольно новичок в EF, поэтому я просто предполагаю, что инструмент знает больше, чем я, и оставляю его работать по стандартным настройкам по умолчанию, пока я не смогу более быстро с ним справиться. Вероятно, это не лучшая долгосрочная стратегия 🙂 Я еще раз взгляну на двунаправленные ссылки.