具有虚拟导航属性的深度复制/复制对象



我在C#/Blazor 中工作

我有一个对象,比如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.UpdatedByFkNavigation.FullName更新Project的人的全名。用户根本无法修改导航字段,它只是显示的。

我的问题是关于复制导航项目。为了简单起见,现在在表单的OnInitialized中,我将原始project对象传递给表单,并使用如下构造函数创建一个新的objProject

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

这似乎起到了作用,并创建了一个单独的Project对象,该对象不是引用,我可以将其用作DTO,但我不确定以这种方式分配virtual属性是否合适。

这种方法是否遵循了创建具有虚拟导航字段的对象的非引用副本的最佳实践,或者我应该采取不同的方法?

这取决于关系。引用在EF中很重要,因此您需要考虑是希望新克隆引用相同的UserData,还是使用相同数据的新的不同UserData。通常,在多对一关系中,您希望使用相同的引用,或更新引用以匹配。如果原件由";约翰·史密斯;ID#201,克隆将被修改为";约翰·史密斯;ID#201或改变为当前用户";Jane Doe";ID#405将是相同的";Jane Doe";引用为用户修改的任何其他记录。您可能不希望EF创建一个新的";John Doe";这将以ID#545结束,因为EF被赋予了对具有"0"拷贝的UserData的全新引用;John Doe";。

因此,在您的情况下,我假设您希望引用相同的现有用户实例,因此您的方法是正确的。在使用序列化/反序列化之类的快捷方式制作克隆时,需要小心。在这种情况下,序列化Project和任何加载的UpdatedBy引用将创建具有相同字段甚至PK值的UserData的新实例。然而,当你用它的新UserData引用保存这个新项目时,你要么会得到一个重复的PK异常,一个";具有相同关键字的对象已经被跟踪";或者发现自己有了一个新的"例外";John Doe";ID为#545的记录,如果该实体被设置为期望其PK的Identity列。

关于使用导航属性与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 => x.Id == userId)
.SelectMany(x => x.UpdatedProjects)
.ToList();

我只想使用:

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

通常,您应该通过聚合根来组织您的域及其内部的关系:本质上是应用程序中具有顶级重要性的实体。双向引用也有类似的问题,即在从一方修改这些关系时,有两个真相来源在给定的时间点不一定匹配。这在很大程度上取决于所有关系是否都充满了渴望。

如果两个实体都是聚合根,并且关系足够重要,那么这可以提供双向引用和它应该得到的额外关注。一个很好的例子可能是多对多的关系,比如CourseClass(即数学A级)和Students之间的关系,其中CourseClass有很多学生,而Students有很多CourseClass,从CourseClass的角度列出它的Students是有意义的,从Students的角度列出他们的CourseClass也是有意义的。

最新更新