Rhys Stephens Rhys Stephens - 3 months ago 28
C# Question

ASP.NET MVC Entity Framework 6 Navigation Property not saving on SaveChanges() Repository Pattern on Edit

I am having a strange problem where a navigation property is not persisiting on the Update of a page, but is on a Create.
I am using a Generic Repository pattern with Entity Framework 6, ASP.NET MVC 5.

Here's the skeleton code:

My context:

public class ApplicationDbContext : IdentityDbContext<ApplicationUser>
{
public ApplicationDbContext() : base("DefaultConnection") { }
}


My repository implementation:

public class EntityFrameworkRepository : IRepository, IDisposable
{
private ApplicationDbContext _context;
private readonly ConcurrentDictionary<Type, object> _dbSets =
new ConcurrentDictionary<Type, object>();

public EntityFrameworkRepository(ApplicationDbContext context)
{
_context = context;
}

public void Save<T>(T entity) where T : BaseModel
{
GetDbSet<T>().Add(entity);
_context.SaveChanges();
}

public void Update<T> (T entity) where T: BaseModel
{
_context.Entry(entity).State = EntityState.Modified;
_context.SaveChanges();
}
}


Here the basis of Get of the Edit page uses AutoMapper to map between the domain model and the view model.

public ActionResult Edit(int menuId, int? id)
{
MenuItem mi = _repository.Get<MenuItem>((int)id);
Mapper.CreateMap<MenuItem, MenuItemViewModel>();
MenuItemViewModel mivm = Mapper.Map<MenuItem, MenuItemViewModel>(mi);
///....///
}


This is the method that is not working. As you can see, the full object UseSiblingClaim is being retrieved by id and set to the MenuItem itemToSave. I check the object just before _repository.Update() is called, and the property correctly.

[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult Edit(int menuId, [Bind(Include = "Id,Name,ControllerName,ActionName,Order,CustomUrl,Tooltip,IconClass,CanUseParentsClaim,DoesNotNeedClaim,UseSiblingClaim_Id")] MenuItemViewModel mivm)
{
if (ModelState.IsValid)
{
mivm.Menu = _repository.Get<Menu>(menuId);

if (mivm.UseSiblingClaim_Id != null)
{
mivm.UseSiblingClaim = _repository.Get<MenuItem>((int)mivm.UseSiblingClaim_Id);
}

MenuItem itemToSave = MapViewModelToModel(mivm, _repository);
_repository.Update<MenuItem>(itemToSave);
return RedirectToAction("Details", "Menu", new { Id = menuId });
}
return View(mivm);
}


Here's the MenuItem class:

public class MenuItem : BaseModel
{
public MenuItem()
{
Children = new HashSet<MenuItem>();
}

[Key, DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public int Id { get; set; }
public virtual MenuItem ParentMenuItem { get; set; }
public virtual Menu Menu { get; set; }
public string Name { get; set; }
///.....///
public virtual MenuItem UseSiblingClaim { get; set; }
}


The mapping method is here: UseSiblingClaim is being set.

private static MenuItem MapViewModelToModel(MenuItemViewModel mivm, IRepository _repository)
{


MenuItem itemToSave = new MenuItem()
{
UseSiblingClaim = mivm.UseSiblingClaim,
Id = mivm.Id,
ActionName = mivm.ActionName,
CanUseParentsClaim = mivm.CanUseParentsClaim,
ControllerName = mivm.ControllerName,
CustomURL = mivm.CustomURL,
DoesNotNeedClaim = mivm.DoesNotNeedClaim,
IconClass = mivm.IconClass,
Menu = mivm.Menu,
Name = mivm.Name,
Order = mivm.Order,
ParentMenuItem = mivm.ParentMenuItem,
Tooltip = mivm.Tooltip,
};
return itemToSave;
}


Here's the view model:

public class MenuItemViewModel : BaseModel
{
public int Id { get; set; }
public int ParentMenuItem_Id { get; set; }
public MenuItem ParentMenuItem { get; set; }
public int Menu_Id { get; set; }
public Menu Menu { get; set; }
public IEnumerable<SelectListItem> SiblingItems { get; set; }
[DisplayName("Can Use Sibling's Claim")]
public int? UseSiblingClaim_Id { get; set; }
public MenuItem UseSiblingClaim { get; set; }
}


The View part (this is the same for Create() and Update():

<div class="form-group">
@Html.LabelFor(model => model.UseSiblingClaim_Id, new { @class = "control-label col-md-2" })
<div class="col-md-10">
@Html.DropDownListFor(x => x.UseSiblingClaim_Id, Model.SiblingItems, new { @class = "select2", @multiple = "multiple", @style = "width:100%" })
@Html.ValidationMessageFor(model => model.UseSiblingClaim_Id)
</div>
</div>


Model.SiblingItems is created and passed into the view.

Why won't the UseSiblingClaim navigation property be persisted? It works on Create of the Menu Item, but not on Update? Is it something to do with me uaing a generic repositroy pattern? Please help

Answer

I found a solution to this. I'm still not sure why it wasn't updating, but the way I got it working was to expose the ForeignKey on the MenuItem object explicitly as such:

public class MenuItem : BaseModel
{    
    [Key, DatabaseGenerated(DatabaseGeneratedOption.Identity)]
    public int Id { get; set; }        
    // I didn't have this exposed before //
    public int? UseSiblingClaim_Id { get; set; }
    public virtual MenuItem UseSiblingClaim { get; set; }
}

Then change my mapping from:

protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
    modelBuilder.Entity<MenuItem>()
                .HasOptional(x=>x.UseSiblingClaim)                
                .WithOptionalDependent()
                .Map(m => m.MapKey("UseSiblingClaim_Id"));
    base.OnModelCreating(modelBuilder);
}

to:

protected override void OnModelCreating(DbModelBuilder modelBuilder)
{  
    modelBuilder.Entity<MenuItem>()
            .HasOptional(x => x.UseSiblingClaim)
            .WithMany()
            .HasForeignKey(x => x.UseSiblingClaim_Id);
    base.OnModelCreating(modelBuilder);
}

Then in my MenuItemController I only had to set the foreign key property (UseSiblingClaim_Id), and not the navigation property, which also saved a db trip.

The answer was buried in here: Cannot get relationship to update for navigation properties in entity framework