An object structure that does not update the property of a complex model set

This question was probably asked in many forms before, but still I think that there is no clear solution to the scenario.

I have the following entity classes.

public class Project { public int ProjectId { get; set; } [Required(ErrorMessage="please enter name")] public string Name { get; set; } public string Url { get; set; } public DateTime CreatedOn { get; set; } public DateTime UpdatedOn { get; set; } public bool isFeatured { get; set; } public bool isDisabled { get; set; } public int GroupId { get; set; } public virtual Group Group { get; set; } [Required(ErrorMessage="Please select atleast one tag")] public virtual ICollection<Tag> Tags { get; set; } } public class Tag { public int TagId { get; set; } public string Name { get; set; } public DateTime CreatedOn { get; set; } public DateTime UpdatedOn { get; set; } public virtual ICollection<Project> Projects { get; set; } } public class Group { public int GroupId { get; set; } public string Name { get; set; } public DateTime CreatedOn { get; set; } public DateTime UpdatedOn { get; set; } public virtual ICollection<Project> Projects { get; set; } } 

I have a viewmodel for a project object and a custom mediation for this model.

 public class NewProjectModelBinder : DefaultModelBinder { public override object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext) { ProjectNewViewModel model = (ProjectNewViewModel)bindingContext.Model ?? (ProjectNewViewModel)DependencyResolver.Current.GetService(typeof(ProjectNewViewModel)); bool hasPrefix = bindingContext.ValueProvider.ContainsPrefix(bindingContext.ModelName); string searchPrefix = (hasPrefix) ? bindingContext.ModelName + ".":""; //since viewmodel contains custom types like project make sure project is not null and to pass key arround for value providers //use Project.Name even if your makrup dont have Project prefix model.Project = model.Project ?? new Project(); //populate the fields of the model if (GetValue(bindingContext, searchPrefix, "Project.ProjectId") != null) { model.Project.ProjectId = int.Parse(GetValue(bindingContext, searchPrefix, "Project.ProjectId")); } // model.Project.Name = GetValue(bindingContext, searchPrefix, "Project.Name"); model.Project.Url = GetValue(bindingContext, searchPrefix, "Project.Url"); model.Project.CreatedOn = DateTime.Now; model.Project.UpdatedOn = DateTime.Now; model.Project.isDisabled = GetCheckedValue(bindingContext, searchPrefix, "Project.isDisabled"); model.Project.isFeatured = GetCheckedValue(bindingContext, searchPrefix, "Project.isFeatured"); model.Project.GroupId = int.Parse(GetValue(bindingContext, searchPrefix, "Project.GroupId")); model.Project.Tags = new List<Tag>(); foreach (var tagid in GetValue(bindingContext, searchPrefix, "Project.Tags").Split(',')) { var tag = new Tag { TagId = int.Parse(tagid)}; model.Project.Tags.Add(tag); } var total = model.Project.Tags.Count; return model; } private string GetValue(ModelBindingContext context, string prefix, string key) { ValueProviderResult vpr = context.ValueProvider.GetValue(prefix + key); return vpr == null ? null : vpr.AttemptedValue; } private bool GetCheckedValue(ModelBindingContext context, string prefix, string key) { bool result = false; ValueProviderResult vpr = context.ValueProvider.GetValue(prefix + key); if (vpr != null) { result = (bool)vpr.ConvertTo(typeof(bool)); } return result; } } //My project controller edit action defined as under: [HttpPost] [ActionName("Edit")] public ActionResult EditProject( ProjectNewViewModel ProjectVM) { if (ModelState.IsValid) { projectRepository.InsertOrUpdate(ProjectVM.Project); projectRepository.Save(); return RedirectToAction("Index"); } else { ViewBag.PossibleGroups = groupRepository.All; return View(); } } //Group Repository public void InsertOrUpdate(Project project) { if (project.ProjectId == default(int)) { // New entity foreach (var tag in project.Tags) { context.Entry(tag).State = EntityState.Unchanged; } context.Projects.Add(project); } else { context.Entry(project).State = EntityState.Modified; } } 

Now that I have a project inside the editing view, and I select new tags for the project and present a form editing action parameter, use the model binding and set all the properties of the project object, including tags. But when the project object is passed to the insertorupdate grouprepository method, all the changes we made go to the database, except for the tag collection property, now I'm really upset about this thing.

Please provide me with a solution that would not introduce changes to the structure that have been developed so far.

+4
source share
2 answers

Something like this for your else case in InsertOrUpdate (the if case is good in my opinion) might work:

 //... else { // Reload project with all tags from DB var projectInDb = context.Projects.Include(p => p.Tags) .Single(p => p.ProjectId == project.ProjectId); // Update scalar properties of the project context.Entry(projectInDb).CurrentValues.SetValues(project); // Check if tags have been removed, if yes: remove from loaded project tags foreach(var tagInDb in projectInDb.Tags.ToList()) { // Check if project.Tags collection contains a tag with TagId // equal to tagInDb.TagId. "Any" just asks: Is there an element // which meets the condition, yes or no? It like "Exists". if (!project.Tags.Any(t => t.TagId == tagInDb.TagId)) projectInDb.Tags.Remove(tagInDb); } // Check if tags have been added, if yes: add to loaded project tags foreach(var tag in project.Tags) { // Check if projectInDb.Tags collection contains a tag with TagId // equal to tag.TagId. See comment above. if (!projectInDb.Tags.Any(t => t.TagId == tag.TagId)) { // We MUST attach because tag already exists in the DB // but it was not assigned to the project yet. Attach tells // EF: "I know that it exists, don't insert a new one!!!" context.Tags.Attach(tag); // Now, we just add a new relationship between projectInDb and tag, // not a new tag itself projectInDb.Tags.Add(tag); } } } // context.SaveChanges() somewhere later 

SaveChanges will actually save the previously reloaded project using the tag list due to the detection of EF changes. The project passed to this method is not even context bound and is simply used to update the reloaded project and its tag list.

Edit

context.Tags.Attach(tag); added to the code, otherwise SaveChanges will create new tags in the database.

+3
source

I created an assistant in DBContext [CodeFirst]

  /// <summary> /// Reattaches the relationships so that they can be committed in a <see cref="DbContext.SaveChanges()"/> /// Determines equality using <see cref="OPSDEV.Utils.EF.KeyEqualityComparer"/> /// </summary> /// <typeparam name="T">The Model or Entity to Attach</typeparam> /// <param name="db">The DbContext to use to do the reattaching</param> /// <param name="new">The new list of values to attach</param> /// <param name="old">The old or previous values that existed in the database</param> /// <returns>The new list to be committed</returns> public static ICollection<T> AttachToContext<T>(this DbContext db, ICollection<T> @new, ICollection<T> old) where T : class { if (@new == null) return null; var result = new List<T>(); var comparer = new KeyEqualityComparer<T>(); var added = @new.Where(c => !old.Contains(c, comparer)).ToList(); var existing = old.Where(c => @new.Contains(c, comparer)).ToList(); foreach (var entity in added) { db.Entry(entity).State = EntityState.Unchanged; result.Add(entity); } foreach (var entity in existing) { db.Entry(entity).State = EntityState.Unchanged; result.Add(entity); } return result; } 

It uses KeyEqualityComparer

  /// <summary> /// Uses the Key attribute to determine equality. /// Both keys but have have equal values for the comparer to return true. /// Throws "No Key property found" ArgumentException if no key attribute can be found. /// </summary> /// <typeparam name="T">The Model or Entity type to be compared</typeparam> public class KeyEqualityComparer<T> : EqualityComparer<T> { private PropertyInfo Property { get; set; } public KeyEqualityComparer() { Property = typeof(T).GetProperties(BindingFlags.Public | BindingFlags.Instance) .FirstOrDefault(p => p.GetCustomAttributes(typeof(KeyAttribute), false).Any()); if (Property == null) throw new ArgumentException("No Key property found"); } public override bool Equals(T x, T y) { return GetValue(x).Equals(GetValue(y)); } public override int GetHashCode(T obj) { return GetValue(obj).GetHashCode(); } public object GetValue(object obj) { var value = Property.GetValue(obj, null); return value ?? default(T); } } 
+2
source

All Articles