Entity Framework Not Saving Child Link - entity-framework

I am inserting or updating a parent entity with associated children entities. Entity framework inserts or updates the entities BUT it never creates or updates the links between them even though they exist in C#.
public void MergeParent(Parent parent)
{
// Set Parent
var parentInDB = context.Parents.SingleOrDefault(p => p.ParentID == parent.ParentID);
if (parentInDB != null)
{
parent.ID = parentInDB.ID;
context.Entry(parentInDB).CurrentValues.SetValues(parent);
}
else
{
context.parents.Add(parent);
}
// Set child
foreach(var child in parent.children)
{
var childInDB = context.children.SingleOrDefault(a => a.DiscogsID == child.DiscogsID && child.DiscogsID != null);
if (childInDB != null)
{
child.ID = childInDB.ID;
context.Entry(childInDB).CurrentValues.SetValues(child);
}
else
{
context.children.Add(child);
}
}
// Finally Save
context.SaveChanges();
}
If I set a breakpoint, I can see the parent has the children associated, but while the database contains the elements it does not ever update the link table.

// Set child
foreach(var child in parent.children)
{
var childInDB = context.children.SingleOrDefault(a => a.DiscogsID == child.DiscogsID && child.DiscogsID != null);
if (childInDB != null)
{
child.ID = childInDB.ID;
context.Entry(childInDB).CurrentValues.SetValues(child);
}
else
{
context.children.Add(child);
}
}
The problem is in the section above, for the children, you're adding to and retrieving from the database instead of from the parentInDb's collection of children. I don't think SetValues updates relationships.
I modified some of your code and added some comments:
// Add an include to bring in the children
var parentInDB = context.Parents.Include(p => p.children)
.SingleOrDefault(p => p.ParentID == parent.ParentID);
if (parentInDB != null)
{
// This line shouldn't be necessary...
// parent.ID = parentInDB.ID;
context.Entry(parentInDB).CurrentValues.SetValues(parent);
// Set child
foreach(var child in parent.children)
{
// Should search in parentInDB, not context
// If DiscogsID is a primary key, it can't be null
var childInDB = parentInDB.children.SingleOrDefault(a => a.DiscogsID == child.DiscogsID);
if (childInDB != null)
{
// This line shouldn't be necessary...
// child.ID = childInDB.ID;
context.Entry(childInDB).CurrentValues.SetValues(child);
}
else
{
// Should add to parentInDB, not context
parentInDB.children.Add(child);
}
}
// Would children in parentInDB.children not found in parent.children need to be removed?
}
else
{
// This will automatically add untracked children
context.parents.Add(parent);
}
// Finally Save
context.SaveChanges();
I don't know what happens if you add a new parent with previously attached entities in children.

Related

Only Update Entity (Parent or children) if Property Values Have Changed with EF

As I build a project with Entity Framework 6 (using EF for the first time), I noticed that when I only Update the relationships of an Entity, EF updates the main Entity too.
I can tell this is happening because I'm using System Versioned tables on Sql Server 2017.
This is a made up scenario, but most of the concept is here.
public async Task<ActionResult> Edit([Bind(Include="Id,Name,LocationTimes")] LocationViewModel locationVM) {
if (ModelState.IsValid) {
var location = await _db.Locations.FirstOrDefaultAsync(l => l.Id == locationsViewModel.Id && l.UserId == UserId);
if (location == null) {
return HttpNotFound();
}
location.Name = locationsViewModel.Name;
// ... other properties
foreach (var day in locationsViewModel.LocationTimes.Days) {
var time = new Time {
Day = day.Day,
OpenTime = day.OpenTime,
CloseTime = day.CloseTime,
};
// Find current Time or keep newly created
time = await time.FindByTimeAsync(time, _db) ?? time;
// Find LocationTime with same day
var locationTime = location.LocationTimes.FirstOrDefault(lt => lt.Time.Day == day.Day);
// If all times are the same, skip (continue)
if (locationTime != null && locationTime.Time.OpenTime == time.OpenTime && locationTime.Time.CloseTime == time.CloseTime)
continue;
if (locationTime != null && (locationTime.Time.OpenTime != time.OpenTime || locationTime.Time.CloseTime != time.CloseTime)) {
// Remove, At least one of the Times do not match
locationTime.Time = time;
_db.Entry(locationTime).State = EntityState.Modified;
} else {
location.LocationTimes.Add(new LocationTime {
Location = location,
Time = time,
});
}
}
_db.Entry(location).State = EntityState.Modified;
await _db.SaveChangesAsync();
return RedirectToAction("Index");
}
}
I assume, that by marking the entire Entity as Modified, EF will call the update statement.
How can I avoid an UPDATE to the parent Entity, if no properties have changed on the parent, but still Add/Update the child relationships?
I assume I have to check that each property has not changed and therefore I should not be setting location state to Modified, but how would I handle the newly added Times?
Update #1
So I tried what I mentioned and it works, but is this the correct way to do this?
public async Task<ActionResult> Edit([Bind(Include="Id,Name,LocationTimes")] LocationViewModel locationVM) {
if (ModelState.IsValid) {
var location = await _db.Locations.FirstOrDefaultAsync(l => l.Id == locationsViewModel.Id && l.UserId == UserId);
if (location == null) {
return HttpNotFound();
}
/*******************
This is new part
*******************/
if (
location.Name != locationsViewModel.Name
// || ... test other properties
) {
location.Name = locationsViewModel.Name;
// ... other properties
_db.Entry(location).State = EntityState.Modified;
} else {
_db.Entry(location).State = EntityState.Unchanged;
}
/*******************/
foreach (var day in locationsViewModel.LocationTimes.Days) {
var time = new Time {
Day = day.Day,
OpenTime = day.OpenTime,
CloseTime = day.CloseTime,
};
// Find current Time or keep newly created
time = await time.FindByTimeAsync(time, _db) ?? time;
// Find LocationTime with same day
var locationTime = location.LocationTimes.FirstOrDefault(lt => lt.Time.Day == day.Day);
// If all times are the same, skip (continue)
if (locationTime != null && locationTime.Time.OpenTime == time.OpenTime && locationTime.Time.CloseTime == time.CloseTime)
continue;
if (locationTime != null && (locationTime.Time.OpenTime != time.OpenTime || locationTime.Time.CloseTime != time.CloseTime)) {
// Remove, At least one of the Times do not match
locationTime.Time = time;
_db.Entry(locationTime).State = EntityState.Modified;
} else {
location.LocationTimes.Add(new LocationTime {
Location = location,
Time = time,
});
}
}
/* removed, added above */
//_db.Entry(location).State = EntityState.Modified;
await _db.SaveChangesAsync();
return RedirectToAction("Index");
}
}
So after trial and error, I guess I misunderstood how EF handles the EntityState. I though if a child was Modified, you had to set the parent as Modified as well.
Gladly, that's not the case and the code below works as desired.
public async Task<ActionResult> Edit([Bind(Include="Id,Name,LocationTimes")] LocationViewModel locationVM) {
if (ModelState.IsValid) {
var location = await _db.Locations.FirstOrDefaultAsync(l => l.Id == locationsViewModel.Id && l.UserId == UserId);
if (location == null) {
return HttpNotFound();
}
/*******************
This is new part
check if at least one property was changed
*******************/
if (
location.Name != locationsViewModel.Name
|| location.Ref != locationsViewModel.Ref
// || ... test other properties
) {
location.Name = locationsViewModel.Name;
location.Ref = locationsViewModel.Ref;
// ... other properties
// Tell EF that the Entity has been modified (probably not needed, but just in case)
_db.Entry(location).State = EntityState.Modified;
} else {
// Tell EF that the Entity has *NOT* been modified
_db.Entry(location).State = EntityState.Unchanged;
}
/*******************/
foreach (var day in locationsViewModel.LocationTimes.Days) {
var time = new Time {
Day = day.Day,
OpenTime = day.OpenTime,
CloseTime = day.CloseTime,
};
// Find current Time or keep newly created
time = await time.FindByTimeAsync(time, _db) ?? time;
// Find LocationTime with same day
var locationTime = location.LocationTimes.FirstOrDefault(lt => lt.Time.Day == day.Day);
// If all times are the same, skip (continue)
if (locationTime != null && locationTime.Time.OpenTime == time.OpenTime && locationTime.Time.CloseTime == time.CloseTime)
continue;
if (locationTime != null && (locationTime.Time.OpenTime != time.OpenTime || locationTime.Time.CloseTime != time.CloseTime)) {
// Remove, At least one of the Times do not match
locationTime.Time = time;
_db.Entry(locationTime).State = EntityState.Modified;
} else {
location.LocationTimes.Add(new LocationTime {
Location = location,
Time = time,
});
}
}
/* removed, added above */
//_db.Entry(location).State = EntityState.Modified;
await _db.SaveChangesAsync();
return RedirectToAction("Index");
}
}

"the operation cannot be completed because the dbcontext has been disposed entity framework" error is no var userinfo line

Error happens on the var userinfo line... I'm new to ASP.NET MVC and I'm trying to create a sample project to learn ASP.NET MVC, but got stuck here. I searched other posts with similar errors, but the issue was with not saving .
public ActionResult Login(LoginViewModel c)
{
using (db = new DBEntities())
if (!ModelState.IsValid)
{
return View(c);
}
// error happens on this line of code
var userinfo = db.e_usr.Where(m => m.usr_ename == c.Email && m.usr_pswd == c.Password).FirstOrDefault();
if (userinfo != null)
{
Session["LoginID"] = userinfo.usr_ename;
Session["LoginUser"] = userinfo.usr_pswd;
return Redirect("Home/Index");
}
return null;
}
You're missing a code block (by specifying { .... }) after your using statement. Therefore, the db is valid for only the next statement - the check whether your ModelState is valid or not. After that, the db variable is gone.
Change your code to:
public ActionResult Login(LoginViewModel c)
{
using (db = new DBEntities())
{ // <<<==== add this leading curly brace!
if (!ModelState.IsValid)
{
return View(c);
}
// Simplify your LINQ here - define the condition directly
// in the ".FirstOrDefault()" method
var userinfo = db.e_usr.FirstOrDefault(m => m.usr_ename == c.Email && m.usr_pswd == c.Password);
if (userinfo != null)
{
Session["LoginID"] = userinfo.usr_ename;
Session["LoginUser"] = userinfo.usr_pswd;
return Redirect("Home/Index");
}
return null;
} /// <<<==== add these closing curly braces
}

I am not able to add new child entity while updating

I am not able to add child entity while updating through EF.
public virtual void TestUpdate(object item)
{
var props = item.GetType().GetRuntimeProperties().Where(x => (x.PropertyType.FullName.StartsWith("MIRRA"))||(x.PropertyType.FullName.StartsWith("System.Collections.Generic.ICollection"))).Select(m => m).ToList();
foreach (var prop in props)
{
object value = prop.GetValue(item);
if (value is IEnumerable)
{
foreach (var listitem in value as IEnumerable)
{
TestUpdate(listitem);
}
}else if (value != null)
{
root = value;
TestUpdate(value);
}
}
int id = (int)item.GetType().GetProperties().FirstOrDefault().GetValue(item);
if (id == 0)
{
context.Entry(item).State = EntityState.Added;
//add new child entity to context
}
else
{
context.Entry(item).State = EntityState.Modified;
}
}
at last I am saving this context but context not adding child entities

MVC 4 EF Update failed Object Already Exists In The ObjectStateManager

I have this relationship for my tables. Activity, Workstation, Platform, Part are the lookup tables.
I have ActivitWorkstation that contains (ActivityId, WorkstationId) foreign keys to Activity and Workstation tables.
I also have PlatformPart that contains (PlatformId, PartId) foreign keys to Platform and Part tables.
Lastly I have PartStaging table that has (ActivityWorkstationId, PlatformPartId) foreign keys to ActivityWorkstation and PlatformPart tables.
Below is my PartStaging Edit method:
[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult Edit(PartStagingVM partstagingvm)
{
if (ModelState.IsValid)
{
PartStaging partstaging = new PartStaging();
var activityWorkstation = db.ActivityWorkstations.FirstOrDefault(aw => aw.ActivityId == partstagingvm.ActivityId && aw.WorkstationId == partstagingvm.WorkstationId);
if (activityWorkstation == null)
{
activityWorkstation = new ActivityWorkstation
{
ActivityId = partstagingvm.ActivityId,
WorkstationId = partstagingvm.WorkstationId
};
db.Entry(activityWorkstation).State = EntityState.Added;
}
var platformPart = db.PlatformParts.FirstOrDefault(pp => pp.PlatformId == partstagingvm.PlatformId && pp.PartId == partstagingvm.PartId);
if (platformPart == null)
{
platformPart = new PlatformPart
{
PlatformId = partstagingvm.PlatformId,
PartId = partstagingvm.PartId
};
db.Entry(platformPart).State = EntityState.Added;
}
var partStaging = db.PartStagings.FirstOrDefault(ps => ps.ActivityWorkstationId == activityWorkstation.Id && ps.PlatformPartId == platformPart.Id);
if (partStaging != null && partStaging.Id != partstagingvm.Id)
{
TempData["Message"] = "The record already exists.";
}
else
{
partstaging.Id = partstagingvm.Id;
partstaging.ActivityWorkstationId = activityWorkstation.Id;
partstaging.PlatformPartId = platformPart.Id;
partstaging.Description = partstagingvm.Description;
db.Entry(partstaging).State = EntityState.Modified;
db.SaveChanges();
TempData["Message"] = "The record has been modified.";
}
return RedirectToAction("Edit");
}
return View(partstagingvm);
}
I can edit ActivityWorkstation. I can edit PlatformPart. It prevents me from updating when the record already exists. But I have a problem when editing the Description. It's supposed to be the simplest update since all I have to do is to assign:
partstaging.Description = partstagingvm.Description
But when I submit, I get this error:
An object with the same key already exists in the ObjectStateManager. The ObjectStateManager cannot track multiple objects with the same key.
Description: An unhandled exception occurred during the execution of the current web request. Please review the stack trace for more information about the error and where it originated in the code.
Exception Details: System.InvalidOperationException: An object with the same key already exists in the ObjectStateManager. The ObjectStateManager cannot track multiple objects with the same key.
Source Error:
Line 216: partstaging.Description = partstagingvm.Description;
Line 217:
Line 218: db.Entry(partstaging).State = EntityState.Modified;
Line 219: db.SaveChanges();
Line 220:
What am I missing?

Item's property inside a list changes value from EF source - is there a way to 'detach' it?

I have a list and this list is being populated thru loop. Each loop gets a collection directly from DB. The problem is that when it gets CaseNo(PK) that has the same CaseNo existing in the list and I changed the value of one of the property, the item that has the same CaseNo which is already in the list also changes.
ex. caseNo 1234, proc code = OTH
then I add another item which I get from DB again. but this time I programatically change the proc code, the item in the list above also changes to that value also.
I do not want that to happen because it is not the same Case in the record because a Case can have different type of pro code as its category.
I get that it is changing because it is detecting that it is the same item as the one in the list but I need to add the same case number in the list as a separate item because the proc code is different. Is there a way I can treat it as different entity as the one in the list?
Ex. of data
1.)CaseNO 123
Proc code OTH
2.) CaseNO 123
Proc code OTH
but I need it to be:
1.)CaseNO 123
Proc code DSP
1.)CaseNO 123
Proc code OTH
it goes wrong when this line is executed.
adrCase.ProcCode = "DSP";
then the item in the list (_adrMasterList) if there is any with the same case no, changes its proc code too to "DSP".
here is my code:
private void GenerateReport(string caseCoordinatorID) //LGF 08012011 ADR-59: add case coordinator - add parameter
{
var dispositions = FileMaintenanceBusiness.Instance.GetManyDispositionInfobyKeyword(_selectedProcCode, "ProcCode");
var adrDispos = FileMaintenanceBusiness.Instance.GetAllADRDispositionInfoList();
var calendarActivities = FileMaintenanceBusiness.Instance.GetManyCalendarActivityInfobyKeyword(_selectedProcCode, "ProcCode");
var adrCalActivities = FileMaintenanceBusiness.Instance.GetAllADRCalendars();
var otherActivities = FileMaintenanceBusiness.Instance.GetManyOtherActivities(_selectedProcCode, "ProcCode");
var adrOtherActivities = FileMaintenanceBusiness.Instance.GetAllADROtherActivities();
_adrMasterList.Clear();
if (_selectedProcCode == "ALL")
{
var dispositionALL = FileMaintenanceBusiness.Instance.GetAllDispositionInfoList();
var adrDisposALL = FileMaintenanceBusiness.Instance.GetAllADRDispositionInfoList();
var calendarActivitiesALL = FileMaintenanceBusiness.Instance.GetAllCalendarActivityInfoList();
var adrCalActivitiesALL = FileMaintenanceBusiness.Instance.GetAllADRCalendars();
var otherActivitiesALL = FileMaintenanceBusiness.Instance.GetAllOtherActivities();
var adrOtherActivitiesALL = FileMaintenanceBusiness.Instance.GetAllADROtherActivities();
if (dispositionALL != null && adrDisposALL != null)//dispos
{
foreach (var dispo in dispositionALL)
{
foreach (var adrDispo in adrDisposALL)
{
if (dispo.DispositionID == adrDispo.DispositionID)
{
var adrCase = FileMaintenanceBusiness.Instance.GetADRMasterInfobyKeyword(adrDispo.CaseNo, "CaseNo");
if (adrCase != null)
{
adrCase.ProcCode = "DSP";
adrCase.ProcDateString = dispo.ProcDate.HasValue ? dispo.ProcDate.Value.ToShortDateString() : string.Empty;
//LGF 08012011 ADR-59: add case coordinator - add CaseCoordinatorFilter
//if (!_adrMasterList.Contains(adrCase) && CaseCoordinatorFilter(caseCoordinatorID, adrCase.CaseNo))
// _adrMasterList.Add(adrCase);
if (CaseCoordinatorFilter(caseCoordinatorID, adrCase.CaseNo))
_adrMasterList.Add(adrCase);
}
}
}
}
}
if (calendarActivitiesALL != null && adrCalActivitiesALL != null)//cals
{
foreach (var cal in calendarActivitiesALL)
{
foreach (var adrCal in adrCalActivitiesALL)
{
if (cal.CalendarItemID == adrCal.CalendarItemID)
{
var adrCase = FileMaintenanceBusiness.Instance.GetADRMasterInfobyKeyword(adrCal.CaseNo, "CaseNo");
if (adrCase != null)
{
adrCase.ProcCode = "CAL";
adrCase.ProcDateString = cal.ProcDate.ToShortDateString();
//LGF 08012011 ADR-59: add case coordinator - add CaseCoordinatorFilter
//if (!_adrMasterList.Contains(adrCase) && CaseCoordinatorFilter(caseCoordinatorID, adrCase.CaseNo))
// _adrMasterList.Add(adrCase);
if (CaseCoordinatorFilter(caseCoordinatorID, adrCase.CaseNo))
_adrMasterList.Add(adrCase);
}
}
}
}
}
if (otherActivitiesALL != null && adrOtherActivitiesALL != null)//other activities
{
foreach (var otherActivity in otherActivitiesALL)
{
foreach (var adrotherActivity in adrOtherActivitiesALL)
{
if (otherActivity.ActivityID == adrotherActivity.ActivityID)
{
var adrCase = FileMaintenanceBusiness.Instance.GetADRMasterInfobyKeyword(adrotherActivity.CaseNo, "CaseNo");
if (adrCase != null)
{
adrCase.ProcCode = otherActivity.ProcCode;
adrCase.ProcDateString = otherActivity.ProcDate.ToShortDateString();
//LGF 08012011 ADR-59: add case coordinator - add CaseCoordinatorFilter
//if (!_adrMasterList.Contains(adrCase) && CaseCoordinatorFilter(caseCoordinatorID, adrCase.CaseNo))
if(CaseCoordinatorFilter(caseCoordinatorID, adrCase.CaseNo))
_adrMasterList.Add(adrCase);
}
}
}
}
}
}
else if (_selectedProcCode == "DSP")
{
if (dispositions != null && adrDispos != null)
{
foreach (var dispo in dispositions)
{
foreach (var adrDispo in adrDispos)
{
if (dispo.DispositionID == adrDispo.DispositionID)
{
var adrCase = FileMaintenanceBusiness.Instance.GetADRMasterInfobyKeyword(adrDispo.CaseNo, "CaseNo");
//LGF 08012011 ADR-59: add case coordinator - add CaseCoordinatorFilter
if (adrCase != null && CaseCoordinatorFilter(caseCoordinatorID, adrCase.CaseNo))
{
adrCase.ProcCode = _selectedProcCode;
//adrCase.ProcDateString = dispo.ProcDate.ToShortDateString();
_adrMasterList.Add(adrCase);
}
}
}
}
}
}
else if (_selectedProcCode == "CAL")
{
if (calendarActivities != null && adrCalActivities != null)
{
foreach (var cal in calendarActivities)
{
foreach (var adrCal in adrCalActivities)
{
if (cal.CalendarItemID == adrCal.CalendarItemID)
{
var adrCase = FileMaintenanceBusiness.Instance.GetADRMasterInfobyKeyword(adrCal.CaseNo, "CaseNo");
//LGF 08012011 ADR-59: add case coordinator - add CaseCoordinatorFilter
if (adrCase != null && CaseCoordinatorFilter(caseCoordinatorID, adrCase.CaseNo))
{
adrCase.ProcCode = _selectedProcCode;
adrCase.ProcDateString = cal.ProcDate.ToShortDateString();
_adrMasterList.Add(adrCase);
}
}
}
}
}
}
else
{
if (otherActivities != null && adrOtherActivities != null)
{
foreach (var otherActivity in otherActivities)
{
foreach (var adrotherActivity in adrOtherActivities)
{
if (otherActivity.ActivityID == adrotherActivity.ActivityID)
{
var adrCase = FileMaintenanceBusiness.Instance.GetADRMasterInfobyKeyword(adrotherActivity.CaseNo, "CaseNo");
//LGF 08012011 ADR-59: add case coordinator - add CaseCoordinatorFilter
if (adrCase != null && CaseCoordinatorFilter(caseCoordinatorID, adrCase.CaseNo))
{
adrCase.ProcCode = _selectedProcCode;
adrCase.ProcDateString = otherActivity.ProcDate.ToShortDateString();
_adrMasterList.Add(adrCase);
}
}
}
}
}
}
GeneratePrintReport();
}
That is happening because of internal identity map pattern implementation. You can never have attached two entity instances with the same entity key to the same context. You must simply call Detach once you load the entity.
context.Detach(entity);
Or you can try to load entities as non tracked (I'm not sure if it solves this problem):
context.YourEntitySet.MergeOption = MergeOption.NoTracking;
// Now run the query from YourEntitySet
Anyway it is odd that you want two instances of the same entity with different values. You should use separate non entity object for your "report" and make projections to that object. You would not have to deal with this problem at all.