In entity framework, I can't get a relationship to update for navigation properties.

code-first entity-framework

Question

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.

1
6
3/7/2012 6:11:24 PM

Accepted Answer

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();
    
11
3/7/2012 6:45:13 PM

Popular Answer

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();
}


Related Questions





Related

Licensed under: CC-BY-SA with attribution
Not affiliated with Stack Overflow
Licensed under: CC-BY-SA with attribution
Not affiliated with Stack Overflow