Cannot get relationship to update for navigation properties in entity framework

13,052

Solution 1

Setting the state to Modified only marks scalar properties as modified, not navigation properties. You have several options:

  • A hack (you won't like it)

    //...
    else
    {
        var manager = project.Manager;
        project.Manager = null;
        context.Entry(project).State = EntityState.Modified;
        // the line before did attach the object to the context
        // with project.Manager == null
        project.Manager = manager;
        // this "fakes" a change of the relationship, EF will detect this
        // and update the relatonship
    }
    
  • Reload the project from the database including (eager loading) the current manager. Then set the properties. Change tracking will detect a change of the manager again and write an UPDATE.

  • Expose a foreign key property for the Manager navigation property in your model:

    public class Project
    {
        public int ProjectID { get; set; }
        [Required]
        public string Name { get; set; }
    
        public int ManagerID { get; set; }
        public virtual User Manager { get; set; }
    }
    

    Now ManagerID is a scalar property and setting the state to Modified will include this property. Moreover you don't need to load the Manager user from the database, you can just assign the ID you get from your view:

    Project project = new Project();
    project.ProjectID = projectViewModel.ProjectID;
    project.Name = projectViewModel.Name;
    project.ManagerID = projectViewModel.ManagerID;
    repository.InsertOrUpdateProject(project);
    repository.Save();
    

Solution 2

There are several options here, I will list 3 of them:

Option 1: Using GraphDiff

*This needs the Configuration.AutoDetectChangesEnabled of your context set to true.

Just install GraphDiff with NuGet

Install-Package RefactorThis.GraphDiff

Then

using (var context = new Context())
{
    var customer = new Customer()
    {
        Id = 12503,
        Name = "Jhon Doe",
        City = new City() { Id = 8, Name = "abc" }
    };

    context.UpdateGraph(customer, map => map.AssociatedEntity(p => p.City));
    context.Configuration.AutoDetectChangesEnabled = true;

    context.SaveChanges();
}

For more details about GraphDiff look here.

Option 2: Find and Edit

Searching your entity with EF to track it to the context. Then edit the properties.

*This needs the Configuration.AutoDetectChangesEnabled of your context set to true.

var customer = new Customer()
{
    Id = 12503,
    Name = "Jhon Doe",
    City = new City() { Id = 8, Name = "abc" }
};

using (var context = new Contexto())
{
    var customerFromDatabase = context.Customers
                                      .Include(x => x.City)
                                      .FirstOrDefault(x => x.Id == customer.Id);

    var cityFromDataBase = context.Cities.FirstOrDefault(x => x.Id == customer.City.Id);

    customerFromDatabase.Name = customer.Name;
    customerFromDatabase.City = cityFromDataBase;                

    context.Configuration.AutoDetectChangesEnabled = true;
    context.SaveChanges();
}

Option 3: Using a scalar property

In a matter of performance this is the best way, but it mess your class with database concerns. Because you will need to create a scalar (primitive type) property to map the Id.

*In this way there is no need to set the Configuration.AutoDetectChangesEnabled to true. And also you won't need to do a query to the database to retrieve the entities (as the first two options would - yes GraphDiff does it behind the scenes!).

var customer = new Customer()
{
    Id = 12503,
    Name = "Jhon Doe",
    City_Id = 8,
    City = null
};

using (var contexto = new Contexto())
{
    contexto.Entry(customer).State = EntityState.Modified;
    contexto.SaveChanges();
}
Share:
13,052
glockman
Author by

glockman

Updated on June 20, 2022

Comments

  • glockman
    glockman almost 2 years

    I am currently using EF4.3 and Code First. Creation of my objects works (via my views - just using the auto-generated Create), but when I attempt to edit an object, it does not save any changes that, utlimately, tie back to my navigation properties. I have been reading on relationships, but I don't understand how to tell my context that the relationship has changed.

    Here is some example code of my implementation.

    @* Snippet from my view where I link into my ViewModel. *@
    <div class="row">
        <div class="editor-label">
            @Html.LabelFor(model => model.ManagerID)
        </div>
        <div class="editor-field">
            @Html.DropDownListFor(model => model.ManagerID, ViewBag.Manager as SelectList, String.Empty)
            @Html.ValidationMessageFor(model => model.ManagerID)
        </div>
    </div>
    

    Here is my Controller implementation (POST of my Edit):

        [HttpPost]
        public ActionResult Edit(ProjectViewModel projectViewModel)
        {
            if (ModelState.IsValid)
            {
                Project project = new Project();
                project.ProjectID = projectViewModel.ProjectID;
                project.Name = projectViewModel.Name;
                project.ProjectManager = repository.GetUser(projectViewModel.ManagerID);
                repository.InsertOrUpdateProject(project);
                repository.Save();
                return RedirectToAction("Index");
            }
            ViewBag.Manager = new SelectList(repository.GetUsers(), "UserID", "FullName", projectViewModel.ManagerID);
            return View(projectViewModel);
        }
    

    Within my Project object:

    public class Project
    {
        public int ProjectID { get; set; }
        [Required]
        public string Name { get; set; }
    
        // Navigation Properties
        public virtual User Manager { get; set; }
    }
    

    Here is the corresponding method from the repository (where my context resides):

    public void InsertOrUpdateProject(Project project)
        {
            if (program.ProjectID == default(int))
            {
                context.Projects.Add(project);
            }
            else
            {
                context.Entry(project).State = EntityState.Modified;
            }
        }
    

    Just to be clear, this does work to update my properties, but it does not update my navigation properties (in this case, Manager). Appreciate any help.

  • glockman
    glockman about 12 years
    So I don't explicitly define my FK names. I want to be able to set my Manager value to a new Manager (which appears to work) but the changes aren't actually persisting to the database when I call SaveChanges(). I'm not sure how to make that happen.
  • glockman
    glockman about 12 years
    Excellent. Thank you. I originally had done the foreign key route, but had some issues with it. Turns out, it works, but I had to defined the [ForeignKey("Name")] attribute with the navigation property for everything to map properly. Accepting your answer. Thanks!
  • Slauma
    Slauma about 12 years
    @user1240560: With the names in my answer it should actually work without ForeignKey attribute (FK detection by name convention). But if your names are slighly different and don't follow the convention rules, then yes, you need the annotation.
  • Hector Sanchez
    Hector Sanchez almost 10 years
    @Slauma I love you, the other approaches should work with Code First Model, but with a DataBase First the only one that worked for me was your hack, I was messing with it for days, and now I have to say that I love you so much
  • peter
    peter almost 9 years
    Thanks from the future
  • sairfan
    sairfan almost 6 years
    thanks really helpful, i believe if Configuration.AutoDetectChangesEnabled = true; does not work then check other options