具有虚拟导航属性的深度 Copy/Duplicating 对象

Deep Copy/Duplicating Object with Virtual Navigation Properties

我正在使用 C#/Blazor

我有一个对象,比如 Project,我从数据库中获取该对象,该数据库带有外键及其关联的导航属性。我正在获取对象,然后在断开连接的状态下使用它。

获取对象后,根据需要将其输入到 displaying/editing/updating 的表单中。我想创建一个单独的 Project 克隆以在表单中用作 DTO,以便可以丢弃任何更改而不会对原始提取的 Project.

产生引用问题

例如,这是一个简化的 Project class:

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 中很重要,因此您需要考虑是希望新克隆引用 same UserData 还是具有相同数据的新的不同 UserData。通常在多对一关系中,您希望使用相同的引用,或更新引用以匹配。如果原件被“John Smith”ID #201 修改,克隆将被“John Smith”ID #201 修改,或更改为当前用户“Jane Doe”ID #405,这将是相同的“Jane Doe”引用作为用户修改的任何其他记录。您可能不希望 EF 创建一个最终 ID #545 的新“John Doe”,因为 EF 获得了对具有“John Doe”副本的 UserData 的全新引用。

因此,在您的情况下,我假设您希望引用相同的现有用户实例,因此您的方法是正确的。在使用 Serialization/Deserialization 之类的快捷方式进行克隆时,您需要注意的地方。在这种情况下,序列化项目和任何加载的 UpdatedBy 引用将创建一个具有相同字段甚至 PK 值的 UserData 的新实例。但是,当您使用新的 UserData 引用保存这个新项目时,您要么以重复的 PK 异常结束,即“已跟踪具有相同键的对象”异常,要么发现自己有一个新的“John Doe " 如果该实体设置为期望其 PK 的标识列,则记录 ID 为 #545。

关于使用导航属性与 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();

一般来说,您应该通过聚合根来组织您的域及其中的关系:本质上是在应用程序中具有顶级重要性的实体。双向引用有类似的问题,即当从一侧修改这些关系时,有两个事实来源在给定时间点不一定匹配。这在很大程度上取决于是否所有关系都是急切加载的。

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