Do navigation properties have to be explicitly set in EF Core?

So let’s say I have two models:

public class Simple
{
    public int SimpleId { get; set; }
    public int TestId { get; set; }
    public Test Test { get; set; }
}
public class Test
{
    public int TestId { get; set; }
    public string Name { get; set; }
}

And I want to add a new Simple like this:
    public void AddSimple()
    {
        var simple = new Simple
        {
            SimpleId = 11,
            TestId = 1 //Assume that this exists in the DB with name = "Testtest"
        };
        _repo.Add(simple);
    }

When I get this from the database like so:
    public async override Task<Simple> Get(int id)
    {
        var simple = _entries
                    .Include(n => n.Test)
                    .FirstOrDefault(n => n.SimpleId == id);
        return simple;
    }

Is EF Core supposed to automatically infer and get Simple.Test from Simple.TestId?
Because right now the way I have to create new Simple records is by setting Simple.Test explicitly like this:
    public void AddSimple()
    {
        var simple = new Simple
        {
            SimpleId = 11,
            TestId = 1 //Assume that this exists in the DB with name = "Testtest",
            Test = _testRepo.GetById(1)
        };
        _repo.Add(simple);
    }

Or else, the Simple.Test navigation property will be null. This doesn’t seem like the way it’s supposed to be done.

Answers:

Thank you for visiting the Q&A section on Magenaut. Please note that all the answers may not help you solve the issue immediately. So please treat them as advisements. If you found the post helpful (or not), leave a comment & I’ll get back to you as soon as possible.

Method 1

The simple answer is “No”, you don’t need to set navigation properties, but you arguably “should”. The behaviour you will see will behave more like “It Depends”. What you see will depend on whether the DbContext is tracking relative entities or not.

For a simple example with a Parent and Child entity where a Child has a Parent reference. An existing Parent ID #1 exists, and we aren’t adding a duplicate child.

using (var context = new AppDbContext())
{
   var child = new Child { Name = "Sven", ParentId = 1 };
   context.Children.Add(child);
   var parent = child.Parent; // Null
   context.SaveChanges();
   parent = child.Parent; // Still null.
   child = context.Children.Include(x => x.Parent).Single(x => x.Name == "Sven");
   parent = child.Parent; // Returns Parent /w ID 1.
}

The “depends” behavior is whether the DbContext isn’t already aware of the referenced entity. For example if we do this:
using (var context = new AppDbContext())
{
   var tossThis = context.Parents.Single(x => x.ParentId == 1);
   // Read for demonstration, the context is now tracking parent Id 1, we're not actually going to use it...

   var child = new Child { Name = "Sven", ParentId = 1 };
   context.Children.Add(child);
   var parent = child.Parent; // Returns Parent /w ID 1.
}

The reference for child.Parent will match tossThis even though we didn’t explicitly set it. EF will provide that reference when Adding child because it is tracking it. If the DbContext happens to be tracking an entity that you reference by ID when creating a new related entity, EF will automatically associate these related entities as soon as it associates the new entity.

This can lead to some inconsistent behaviour with your entity state if you have code logic called against a newly created or updated entity. When you set FKs but not navigation properties, the navigation properties might be available, or they might not. Exposing FKs also adds a second source of truth for dealing with associated references. For instance, what is the ID of a child’s parent assumed to be? child.ParentId or child.Parent.ParentId? Some code may use one or the other. This introduces opportunities for unexpected behaviour if code changes a Parent reference. For example moving a child’s parent:

var child = context.Children.Include(x => x.Parent).Single(x => x.Name == "Sven");
child.ParentId = 2;
var parent = child.Parent; // Still points to Parent ID #1.
context.SaveChanges();
parent = child.Parent; // Now it depends.

What child.Parent refers to after SaveChanges will depend on whether EF happens to be tracking the parent with the new ID. If it isn’t tracking parent ID #2, then child.Parent will now be null. If it was tracking parent ID #2, then child.Parent will be referencing that entity after SaveChanges.

vs.

var newParent = context.Parents.Single(x => x.ParentId == 2);
child.Parent = newParent;
var parentId = child.ParentId; // Still set to 1.
context.SaveChanges();
parentId = child.ParentId; // Updated to 2.

This behaviour is a bit more consistent. Setting the parent doesn’t automatically update the FK until SaveChanges is called.

Classic Parent/Child relationships don’t typically see “parents” change, but Many-to-One relationships such as Order to OrderStatus are cases where an Order could see it’s Status change. By setting FKs for OrderStatus where there is a navigation property available, your behaviour could change subtly depending if the context had happened to previously work with an order with the new status or not. (Whether that updated Status might be tracked already or not.)

Overall to avoid the risk of inconsistent behaviour and the bugs that that sometimes crop up when dealing with navigation properties vs. their FKs, my advice is to only use one or the other. For general use entities where having navigation properties available is beneficial, then use navigation properties along with shadow properties for the FKs. For situations where the navigation property isn’t required and we want raw performance, use FKs alone. (Bounded contexts can help manage separating entity definitions for general use /w navigation properties vs. cases where you want raw read/update performance)

The additional benefit of fetching related entities is that it can provide a more meaningful validation. For instance with an Order / OrderStatus update scenario:

var order = context.Orders.Single(x => x.OrderId == dto.OrderId);
// update various order details...
order.OrderStatusId = dto.OrderStatusId;
context.SaveChanges(); // Throws possible exception.

vs.
var orderStatus = context.OrderStatuses.Single(x => x.OrderStatusId == dto.OrderStatusId); // Throws exception if status ID is not valid.
var order = context.Orders.Single(x => x.OrderId == dto.OrderId);
// update various order details...
order.OrderStatus = orderStatus;
context.SaveChanges(); // Throws possible exception for other violations.

In the first example, any invalid/illegal data combination including FK violations will occur on SaveChanges which isn’t much of a hint what went wrong. Where-as in the second example, if the OrderStatus ID provided wasn’t valid, the exception details would be on the line that attempted to load that ID. Debugging issues when resolving the references is a lot easier to see exactly what/where the issue lies.

Fetching entities can “feel” expensive, but it really isn’t. EF will return references it is tracking from cache, or it will go to the DB if necessary. Fetching a row by ID is about as efficient as a DB operation can get. In cases where you might be dealing with a number of references (I.e. updating a set of data) you can consolidate the read operations ahead of time. (Get the relevant IDs from your ViewModels/DTOs, then pre-load those related rows in one read call, then set the references from that set.)

Method 2

From what you are saying, this looks like a Primary Key – Foreign Key relationship.
Add the annotation inside Simple class -> Test property to specify FK relationship as below:

    public class Simple
    {
        public int SimpleId { get; set; }
        public int TestId { get; set; }

        [ForeignKey("TestId")]
        public virtual Test Test { get; set; }
    }
    public class Test
    {
        public int TestId { get; set; }
        public string Name { get; set; }
    }

This explicitly tells EF that you have a relationship and helps filling in the navigational property.

You can also use fluent APIs to specify the above relationship.


All methods was sourced from stackoverflow.com or stackexchange.com, is licensed under cc by-sa 2.5, cc by-sa 3.0 and cc by-sa 4.0

0 0 votes
Article Rating
Subscribe
Notify of
guest

0 Comments
Inline Feedbacks
View all comments
0
Would love your thoughts, please comment.x
()
x