C# LINQ-TO-SQL: datacontext.ChangeConflicts not showing all conflicts - c#

I am running into problem when working with linq-to-sql and trying to resolve conflicts. The problem is that sometimes conflict is not detected. Please look on the below code sample:
// Setup the object to re-produce the problem
//
// MyObject has properties: id, my_string, my_int and version (timestamp) to enable
// conflicts detection
var context = new MyDataContext();
var obj = new MyObject();
obj.id = "1";
obj.my_string = "value";
obj.my_int = 0;
context.MyTable.InsertOnSubmit(obj);
context.SubmitChanges();
context.dispose();
// Get 2 data contexts
var context1 = new MyDataContext();
var context2 = new MyDataContext();
// Get 2 instances of obj - 1 from each context
var obj1= context1.MyTable.SingleOrDefault(o => o.id == "1");
var obj2= context2.MyTable.SingleOrDefault(o => o.id == "1");
// Change the values of obj1 and update it to the database
obj1.my_string= "value1";
obj1.my_int= 0;
context1.SubmitChanges();
context1.Dispose();
// Update the changes in obj2
obj2.my_string= "value2";
obj2.my_int= 1;
// Now the database contains:
// id: "1"
// my_string: "value1"
// my_int: 0
// obj2 contains:
// id: "1"
// my_string: "value2"
// my_int: 1
try
{
context2.SubmitChanges();
}
catch(ChangeConflictException ex)
{
LogInfo("Conflicting members:");
context2.ChangeConflicts[0].MemberConflicts.ToList().ForEach(
mcc=>LogInfo("Property '{0}': Database value: {1}, Current Value:{2}", mcc.Member.Name, mcc.DatabaseValue, mcc.CurrentValue)
);
}
context2.Dispose();
In the catch I expects to see 3 member conflicts: my_string, my_int and version but I see only 2 member conflicts: my_string and version. The my_int is not detected as conflict.
If I would have setup the my_int when the object was created to value different from the value that I have assigned to obj2, the conflict is being detected.
I found some commonality: when the value of a property (any property) of the original object is equal to the value of obj1, the conflict for this property is not detected.
I would like to get any idea how to overcome this problem so conflicts will successfully detected

I found the root cause for the problem. The reason that I didn't get conflict for "my_int" property is because it is not conflicting with the database value that context2 "knows". I'll explain this:
I thought that conflict is defined as when the values of the object that being saved are not equal to the values in the database. IT'S NOT!!!!
Conflict is defined as when the current value in the database is not equal to the original value that the context familiar. The original value is the value that was in the database when the select query executed.
Examining the example in my question, when context2 selected the data from the database to create obj2, the proeprties values were: my_string: "value", my_int:0. Those are the original values.
When I tried to save the data, LINQ-TO-SQL compared the original values to the database values. The database values at this time (after obj1 was saved): my_string: "value1", my_int:0
As a result, I got conflict in my_string (original: "value", database:"value1") but not in my_int(original: 0, database: 0).
Discovering this helped me to understand why there is no conflict, but still it didn't helped me to solve the problem because the wrong value was not saved to the database because I analyzed only the MemberChangeConflicts that exists in the ObjectChangeConflict and if the property that I am checking is not exists there, the check logic is skipped.
The solution was to analyze also the modified members enumerable that give access to all the properties that were modified and for each one I can get the original value and the new value.
To get the modified members, there is a need to execute method called GetModifiedMembers. This method is in the table of the object data type and can be executed as follows:
var mmc = context.MyTable.GetModifiedMembers(myobject);
Combining the conflicts and modified members gave me the full picutre of what happened to the object that caused the conflict and handle it properly.
Thanks for Damien_The_Unbeliever for giving the hint in his comment.

Related

How to deal with multiple newly Added Entities that depend on each otter with a Foreign key constraint - EF6

I had a bit of difficulties with getting the right title for this problem so I hope my explanation below will make it a bit clearer.
I am using EntityFramework 6 and I am doing multiple inserts within a function.
There are 3 different tables which get updated / inserted: table EntityMethod, EntityRoom and EntityRoomMethod. The table EntityRoomMethod has a foreign key relationship with the table EntityMethod.
In some cases, a EntityMethod row is missing and this is newly created by adding the object with entity framework:
if (mn == null)
{
mn = new Method
{
ElementId = floorProgram.ElementId,
ActionId = m.ActionId,
ElementCount = m.ElementCount,
ColorId = m.ColorId,
IsBase = m.IsBase,
IsHccp = m.IsHccp,
TimeNorm = m.TimeNorm,
Frequency5Id = m.Frequency5Id,
MaterialId = m.MaterialId,
ProductId = m.ProductId,
MethodTypeId = m.MethodTypeId,
};
}
In another part of code the Method foreign key (MethodId) of the EntityRoomMethod table is being set:
roomMethodObject.RightId = mn.Id;
RightId is in this case the relationship with the EntityMethod table.
On a later point the other 2 table objects (EntityRoom and EntityRoomMethod) are also added (DBSet.Add) using EF.
The problem however is, that when the EntityMethod is newly added, it gets the Id value of 0, because SaveChanges() is not yet executed. The foreign key reference in the EntityRoomMethod is therefor also being set to 0.
When the function returns to the caller, the SaveChanges() is being executed and all 3 objects (representing the 3 tables) are being saved.
This however will generate a FK error (because Id 0 does not exist obviously).
I tried to fix this by calling SaveChanges() after Adding the new Method (so directly in the function). This however will cause some other problems.
In the end I have gotten multiple errors but I assume it all has to do with the same thing, the errors were the following:
Unable to determine the principal end of the 'Solution.Data.RoomMethod_Method' relationship. Multiple added entities may have the same primary key.
The property 'RightId' is part of the object's key information and cannot be modified.
Conflicting changes to the role x of the relationship y have been detected
So now the actual question:
Is there an (easy) way to call SaveChanges() after all 3 entities have been added with EF but also handling the FK errors? Does this mean I have to generate the Id's myself? Or was the first approach better (Calling SaveChanges directly after adding the EntityMethod object).
For now I have some not-nice-looking solution with doing a direct INSERT statement after adding a new EntityMethod (using Dapper). This kind-of works but I assume there is a better way wherein I can just use EF6.
P.S. calling SaveChanges() after adding the EntityMethod was basically the same by doing it with Dapper, however this generated some other errors while using Dapper it didn't generate those errors.
You can wrap several SaveChanges within a transaction, and EF (or probably SQL) will generate the required ids at each point, allowing you to reference them later on.
using(var transaction = _context.Database.BeginTransaction())
{
var p1 = new Something { Name = "Fred" };
_context.SaveChanges();
var a2 = new Dependency { SomethingId = p1.Id }; <-- p1.Id now has an Id value
_context.SaveChanges();
var b3 = new OtherDependency { DependencyId = a2.Id }; <-- a2.Id now has an Id value
_context.SaveChanges();
transaction.Commit(); <-- All 3 changes are fully committed to the db at this point.
}

EFcore update context after _dbContext.Database.ExecuteSqlCommand

I am working with SQL Geography types and since EFCore doesn't support type geography, I need to execute a stored proc to update my entity.
This works well, but when I call the row that has just been updated, Its returning the old value.
I can see the correct value in the DB.
Here is some of my code:
var FlId = new SqlParameter("#flId", SqlDbType.Int) { Value = testModel.Flid ?? SqlInt32.Null };
var OID = new SqlParameter("#oID", SqlDbType.Int) { Value = testModel.OID };
var flName = new SqlParameter("#flName", SqlDbType.VarChar) { Value = testModel.FLName };
var result = _dbContext.Database.ExecuteSqlCommand("testSchema.spUpsertOFL #flId, #oID, #flName,
parameters: new[] { FlId , OID , flName });
The above code works and performs the upsert, but when I then run a regular query using EFCore, I get back a stale record.
var TestQuery = _dbContext.Fl.FirstOrDefault(x => x.FLCode == testModel.Flcode && x.OID == testModel.OID);
I tried reloading the context with:
_dbContext.Entry(fl).Reload();
But I got an error saying :
"There was an internal error creating the Fl. The instance of entity type 'Fl' cannot be tracked because another instance with the same key value for {'FlID'} is already being tracked."
If I use postman and hit my get endpoint - the correct result returns.
Try detaching the entry and then requerying
dbContext.Entry(entry).State = EntityState.Detached;
entry = context.Users.FirstOrDefault(e => e.Id == id);
While researching this problem I saw many people say the best course of action is to dispose and create a new dbcontext https://github.com/dotnet/efcore/issues/16861
The next most common solution I saw was to call dbContext.Entry(entry).Reload(); This did not work for me because I was changing the type of an entry via a discriminator. Detaching the old entry means when you request it again it pulls from the database instead of the local cache

How do I replace an entity in the middle of the graph?

I have a disconnected entity framework graph that is sent to the client and returned with updates. One of the updates may replace an entity in the middle of the graph. When I try to replace it, I get an InvalidOperationException with the message:
The operation failed: The relationship could not be changed because one or more of the foreign-key properties is non-nullable. When a change is made to a relationship, the related foreign-key property is set to a null value. If the foreign-key does not support null values, a new relationship must be defined, the foreign-key property must be assigned another non-null value, or the unrelated object must be deleted.
For simplicity, the model would look something like this:
The code that's causing the exception would look like this:
// Create a tree of objects
Root root = new Root() { Id = 1 };
Branch origBranch = new Branch() { Id = 3 };
Twig onlyTwig = new Twig() { Id = 5 };
Leaf onlyLeaf = new Leaf() { Id = 7 };
onlyTwig.Leaves.Add(onlyLeaf);
origBranch.Twigs.Add(onlyTwig);
root.Branches.Add(origBranch);
// Store the structure in the database using the container
using (Pot container1 = new Pot())
{
container1.Roots.Add(root);
container1.SaveChanges();
}
// Create a new Branch to replace the original
Branch newBranch = new Branch() { Id = 11 };
// Add the Twig from the original object
newBranch.Twigs.Add(onlyTwig);
using (Pot container2 = new Pot())
{
container2.Roots.Attach(root);
// Replace the branch
root.Branches.Remove(origBranch);
root.Branches.Add(newBranch);
container2.SaveChanges(); // THROWS EXCEPTION !!
}
I thought by removing the old entity, then adding a new one that I would satisfy the "new relationship must be defined..." criteria, but it's failing. In our case, making the column nullable in the database leads to other issues, the first of which is risking database integrity. So how is this normally handled?
I resolved part of the issue by essentially tricking entity framework into believing it was making an update in place, rather than replacing the item with a new item. The key to the solution is setting the same primary key value, then setting the entity state to Modified The replacement code is posted below.
// Create a new Branch to replace the original
// Make sure to use the id from the original saved object
Branch newBranch = new Branch() { Id = origBranch.Id };
// Add the Twig from the original object
newBranch.Twigs.Add(onlyTwig);
// Replace the original branch with the new branch
root.Branches.Remove(origBranch);
root.Branches.Add(newBranch);
using (Pot container2 = new Pot())
{
// Attach the graph to the container. The default state is unchanged.
container2.Roots.Attach(root);
// Trick entity framework into thinking the new branch with the
// same primary key is the original item with modifications
container2.Entry(newBranch).State = EntityState.Modified;
container2.SaveChanges();
}
However, there are occasions to replace the entity with an entirely new entity, so I still need an answer for how to remove then add.

Cannot add an entity with a key that is already in use update operation

I am creating small application in which i have used LINQ To SQL to perform all operation to database.
Now here i am giving the small part of my database structure please take a look.
So update language detail i am getting the object of login using the datacontext something like this.
XVDataContext Context = new XVDataContext ();
var myQuery = from objLogIn in Context.GetTable<LogIn>() where objLogIn.Emp_Id == nEmpId select objLogIn;
In nEmpId i will always have some value.
So it is not creating any problem in fact i am getting the required record from DB and storing it in objUser object using the following code.
LogIn objUser = myQuery.First<LogIn>();
Now to update LanguageDetail i am executing following code but it throws Exception when i execute SubmitChanges line.
Here is the code that i am executing to update.
LanguageDetail obj = new LanguageDetail();
foreach (string sLanguages in TextBoxLanguagesKnown.Text.Split('\n'))
{
obj.Emp_Id = objUser.Emp_Id;
obj.Language = sLanguages.Trim();
}
objUser.LanguageDetails[0] = obj;
Context.SubmitChanges();
I already read following links.
cannot add an entity with a key that is already in use
LINQ To SQL exception with Attach(): Cannot add an entity with a key that is alredy in use
Cannot add an entity with a key that is already in use (LINQ)
By reading the above links i found that i am doing some mistake in ID fields but still i am unable to resolve.
Please tell me the clear understanding of raising this issue and how can i resolve this.
EDIT:
I simply want to update LanguageDetail table.
When i try to add new object using following code it still throws exception.
objUser.LanguageDetail.Add(obj);
You might want to add / remove languages for specific user by using following code.
var languages = TextBoxLanguagesKnown.Text.Split('\n');
// Removes deleted languages (first find all language details that are missing from the UI).
var deletedLanguages = objUser.LanguageDetails.Where(ld => !languages
.Any(l => ld.Language == l.Trim())).ToArray();
foreach(var deletedLanguage in deletedLanguages)
{
objUser.LanguageDetails.Remove(deletedLanguage);
Context.LanguageDetails.DeleteOnSubmit(deletedLanguage);
}
// Adds new languages (then adds new language details that are not found in the database).
var newLanguages = languages.Where(l => !objUser.LanguageDetails
.Any(ld => ld.Language == l.Trim())).ToArray();
foreach (string newLanguage in newLanguages)
{
var languageDetail = new LanguageDetail
{
Emp_Id = objUser.Emp_Id,
Language = newLanguage.Trim()
};
objUser.LanguageDetails.Add(languageDetail);
}
Context.SubmitChanges();
From my understanding you want to update the LanguageDetail entity in your database. In order to do so you have to do one of the following:
Retrieve the original LanguageDetail object based on its id, and update that object instead of creating a new one and assigning it the id of an existing object.
Attach the newly created object to your context instead of just giving a reference to it to your LanguageDetails collection.
The exception you are seeing happens because the way linq to sql behaves is that it threats the obj as a new object that you want to insert and because of that it tries to insert it into the language details table.
Modifying your code like that should work:
Context.LanguageDetails.Attach(obj);
objUser.Employee_LanguageDetails[0] = obj;

New object, but error: an object with the same key already exists in the objectstatemanager. the objectstatemanager cannot track multiple (...)

Student and Teacher are i relationship many-to-many.
When I have Student existed in database, and want to add new Teacher, I try code below.
But in line:
addedTeachers.ForEach(a => dbStudent.Teacher.Add(a)); I get error
"an object with the same key already exists in the objectstatemanager. the objectstatemanager cannot track multiple objects with the same key".
What's wrong?
void Update(Student s)
{
using(var context = new MyEntities(connectionString))
{
context.ContextOptions.LazyLoadingEnabled = false;
var dbStudent = context.Student.Include("Teacher").Where(a => a.Id == s.Id).SingleOrDefault();
var dbTeachers = dbStudent.Teacher.ToList();
var newTeachers = s.Teacher.ToList();
var addedTeachers = newTeachers.Except(dbTeachers).ToList();
var deletedTeachers = dbTeachers.Except(newTeachers).ToList();
addedTeachers.ForEach(a => dbStudent.Teacher.Add(a));
deletedTeachers.ForEach(a => dbStudent.Teacher.Remove(a));
context.SaveChanges();
}
}
EDIT
What's strange else:
just before this line with exception dbStudent.Teacher.Count is 0. But after exception is 1. Of course addedTeachers.Count is also 1, but debugger doesn't reach the next line.
The problem is s and dbStudent has the same primary key. Your Teacher instances in s.Teacher collection may refer to the s instance. Then when you call addedTeachers.ForEach(a => dbStudent.Teacher.Add(a));, EF will identity all objects linked to teacher instance and try to add them too.
Try
addedTeachers.ForEach(a => { a.Students.Remove(s);
a.Students.Add(dbStudent);
dbStudent.Teacher.Add(a);});
I think your problem is your Except statement. It is comparing your collection items using the Default comparer which is comparing to see if the items reference the same object in memory when you actually intend it to compare the values of your Teacher objects.
The solution is to provide your own IEqualityComparer and specify exactly how the Teacher objects should be compared to each other. MSDN provides a good example of how to do this.

Categories

Resources