I am working in C#/Blazor
I have an object, say a Project
, that I fetch from a database that comes in with Foreign Keys and their associated navigation properties. I am fetching the object then using it in a disconnected state.
Once the object is fetched, it gets fed into a form for displaying/editing/updating as necessary. I want to create a separate clone of the Project
to use in the form as a DTO so that any changes can be discarded without reference issues to the original fetched Project
.
For example, this is a simplified 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; }
}
In the form, I display the full name of the last person who updated the Project
by using @project.UpdatedByFkNavigation.FullName
. The user cannot modify the navigation field at all, it's display only.
My question is regarding copying the navigation items. For simplicity now, within the form's OnInitialized
, I pass the form the original project
object and create a new objProject
using a constructor like this:
Project objProject = new() { Id = project.Id,
ProjectName = project.ProjectName,
UpdatedByFk = project.UpdatedByFk,
UpdatedByFkNavigation = project.UpdatedByFkNavigation,
This appears to be working and to create a separate Project
object that isn't references and that I can use as my DTO, however I'm not sure if it is appropriate to assign virtual
properties this way.
Is this approach following best practices for creating a non-referenced copy of an object with virtual navigation fields, or is there a different way I should approach this?
CodePudding user response:
It depends on the relationship. References are important in EF, so you need to consider whether you want the new clone to reference the same UserData or a new and distinct UserData with the same data. Typically in a Many-to-one relationship you want to use the same reference, or update the reference to match. If the original was modified by "John Smith" ID #201, a clone would be modified by "John Smith" ID #201, or changed to the current user "Jane Doe" ID #405 which would be the same "Jane Doe" reference as any other record that user modified. You likely would not want EF to create a new "John Doe" which would end up with an ID #545 because EF was given a brand new reference to a UserData that has a copy of "John Doe".
So in your case, I would assume that you would want to refer to the same, existing user instance, so your approach is correct. Where you would need to be careful is when using a shortcut like Serialization/Deserialization to make clones. In that case serializing the Project and any loaded UpdatedBy reference would create a new instance of a UserData with the same fields and even PK value. However, when you go to save this new Project with its new UserData reference, you're either going to end up with a duplicate PK exception, an "Object with same key already tracked" exception, or find yourself with a new "John Doe" record with an ID of #545 if that entity is set up to expect an Identity column for it's PK.
Regarding the typical advice on the use of navigation properties vs. FK fields: My advice is to use one or the other, not both. The reason for this is that when you use both you have two sources of truth for the relationship and depending on the state of the entity, when you change one, the other does not necessarily reflect the change automatically. For instance some code my look at the relationship by going: project.UpdatedByFk
, while other code might use project.UpdatedByFkNavigation.Id
. Your naming convention is a bit odd when it comes to the navigation property. For your example I would have expected:
public virtual UserData UpdatedBy { get; set; }
In general I would use the navigation property solely and rely on a shadow property in EF for the FK. This would look like:
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; }
}
Here we define the navigation property and by nominating the FK column name, EF will create a field behind the scenes for that FK which isn't directly accessible. Our code exposes one source of truth for the relationship.
In certain cases where speed is important and I have little to no need for the related data, I will declare the FK property and no navigation property.
In reference to this:
[InverseProperty(nameof(UserData.ProjectUpdatedByFkNavigations))]
I would also recommend avoiding bi-directional references unless they are absolutely necessary for the same reason. If I want all projects last modified by a given user, I don't really stand to gain anything by:
var projects = context.Users
.Where(x => x.Id == userId)
.SelectMany(x => x.UpdatedProjects)
.ToList();
I would just use:
var projects = context.Projects
.Where(x => x.UpdatedBy.Id == userId)
.ToList();
In general you should look to organize your domain and the relationships within it by aggregate roots: Essentially entities that are of top-level importance within the application. Bidirectional references have similar issues of having two sources of truth that don't necessarily match at a given point of time when modifying those relationships from one side. It depends largely on whether all relationships are eager loaded or not.
Where both entities are aggregate roots and the relationship is important enough, then this can afford a bi-directional reference and the extra attention it deserves. A good example of that might be many-to-many relationships like the relationship between a CourseClass (I.e. Math Class A) and Students where a CourseClass has many students, while a Student has many CourseClasses and it makes sense from a CourseClass perspective to list it's Students, and from a Student perspective to list their CourseClasses.