EF entity is not saving child property of same entity type on update

asp.net-mvc asp.net-mvc-4 c# entity-framework entity-framework-6

Question

I am using ASP.NET MVC 5 and Entity Framework 6. I have a page that allows the user to enter Process information. One aspect of this information is to choose from a drop down the Starting Process. This class roughly looks like:

**

public class SupportProcess
  {
    [Key]
    public int ProcessId { get; set; }
    [DisplayName("Starting process?")]
    public virtual SupportProcess StartProcess { get; set; }
    public string Name { get; set; }
    [DisplayName("When is this run?")]
    public virtual ProcessSchedule ProcessSchedule { get; set; }
    [DisplayName("")]
    public string Description { get; set; }
    [DisplayName("Expected Result")]
    public string ExpectedResult { get; set; }
  }

**

I am using a view model that has properties for the SupportProcess, the selected start process, and a list of processes to populate the drop down on the view.

  public class SupportProcessViewModel
  {
    public SupportProcess SupportProcess { get; set; }
    public int SelectedStartProcess { get; set; }
    public List<SupportProcess> Processes { get; set; }

    public SupportProcessViewModel()
    {
      this.SupportProcess = new SupportProcess();
    }
  }

My Edit post action looks like:

   [HttpPost]
    [ValidateAntiForgeryToken]
    public ActionResult Edit(SupportProcessViewModel vm)
    {
        if (ModelState.IsValid)
        {              

      if (vm.SelectedStartProcess > 0)
      {
        vm.SupportProcess.StartProcess = db.SupportProcesses.Find(vm.SelectedStartProcess);
      }
        db.Entry(vm.SupportProcess).State = EntityState.Modified;
        db.SaveChanges();
        return RedirectToAction("Index");
    }
    return View(vm);
}

The issue is that, while vm.SelectedStartProcess is not null and has a proper value, it never gets saved to the database. The database shows this field as StartProcess_ProcessId. Also of note is that a process may have 0 or 1 Starting processes.

I'm wondering if the fact that EF has made this property in the database table a foreign key, which would, in essence, point to the same table, is somehow causing the issue. I'm all ears for suggestions.

enter image description here

I'll also add that the Create Post action works as expected. This must be something to do with conveying to EF that the StartProcess property also needs to be updated on the entity..That's a guess though...

    [HttpPost]
    [ValidateAntiForgeryToken]
    public ActionResult Create( SupportProcessViewModel vm)
    {
        if (ModelState.IsValid)
        {
            if(vm.SelectedStartProcess > 0) {
              vm.SupportProcess.StartProcess = db.SupportProcesses.Find(vm.SelectedStartProcess);
            }
            db.SupportProcesses.Add(vm.SupportProcess);
            db.SaveChanges();
            return RedirectToAction("Index");
        }

        return View(vm);
    }
1
7
4/3/2017 5:35:56 PM

Accepted Answer

The problem with reference navigation property without explicit primitive FK property is that the shadow FK state is maintained by the DbContext from which the entity has been retrieved. Which information is lost in disconnected scenario like yours.

When you invoke

db.Entry(entity).State = EntityState.Modified;

with disconnected entity, EF will attach the entity to the context and mark all primitive properties as modified. The reference navigation properties are considered unchanged, thus not updating them when you call SaveChanges.

To let EF update the FK, it's critical to know both original and new reference property value. In your case, the original value of StartProcess property of the passed SupportProcess entity.

Since in disconnected scenarios (and especially with lazy loaded properties like yours) you can't rely on the passed object property value, I would suggest the following safe sequence of operations:

(1) Set the navigation property to null to avoid attaching the value to the context.
(2) Attach the entity to the context and mark it as modified.
(3) Explicitly load the navigation property. This will cause additional database trip, but will ensure the update is working.
(4) Set the new value of the navigation property. The context change tracker will be able to determine if FK update is needed or not when you call SaveChanges.

Applying it to your case:

[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult Edit(SupportProcessViewModel vm)
{        
    if (ModelState.IsValid)
    {
        // (1)
        vm.SupportProcess.StartProcess = null;
        // (2)  
        db.Entry(vm.SupportProcess).State = EntityState.Modified;
        // (3)
        db.Entry(vm.SupportProcess).Reference(e => e.StartProcess).Load();
        // (4)
        vm.SupportProcess.StartProcess =
            vm.SelectedStartProcess > 0 ?
            db.SupportProcesses.Find(vm.SelectedStartProcess) :
            null;

        db.SaveChanges();
        return RedirectToAction("Index");
    }
    return View(vm);
}
9
4/3/2017 6:03:09 PM

Popular Answer

Make sure your model is configured properly:

public class SupportProcess
{
    [Key]
    public int ProcessId { get; set; }
    [DisplayName("Starting process?")]
    public int StartProcessId { get; set; }
    [ForeignKey("StartProcessId")]
    public virtual SupportProcess StartProcess { get; set; }
    public string Name { get; set; }
    [DisplayName("When is this run?")]
    public virtual ProcessSchedule ProcessSchedule { get; set; }
    [DisplayName("")]
    public string Description { get; set; }
    [DisplayName("Expected Result")]
    public string ExpectedResult { get; set; }
}

And your edit method:

[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult Edit(SupportProcessViewModel vm)
{
    if (!ModelState.IsValid)
    {
        return View(vm);   
    {           

    // Get the item
    var sp = db.SupportProcesses.FirstOrDefault(p => p.Id == vm.SupportProcess.ProcessId);

    // Set the new values
    if (vm.SelectedStartProcess > 0)
    {
        sp.StartProcessId = vm.SelectedStartProcess;
    }
    sp.Name = vm.SupportProcess.Name;
    sp.Description = vm.SupportProcess.Description;
    // all the rest values        

    // Save changes
    db.SupportProcesses.Update(sp);
    db.SaveChanges();

    return RedirectToAction("Index");
}    


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