While writing an update method for my Entity Framework repository, I include the following code:
public bool UpdateProduct(int id, Models.Product product)
{
Product ctxProduct = GetProductIncludingProductLists(id); //Pulls directly from context
if (ctxProduct != null && product != null)
{
/*Update ctxProduct fields using product*/
_ctx.Entry(ctxProduct).State = EntityState.Modified; //_ctx is my DbContext
return true;
}
return false;
}
But the line of code where I set the entity's status to modified throws the following error:
A first chance exception of type 'System.InvalidOperationException' occurred in EntityFramework.dll
Attaching an entity of type '..Product' failed because another entity of the same type already has the same primary key value. This can happen when using the 'Attach' method or setting the state of an entity to 'Unchanged' or 'Modified' if any entities in the graph have conflicting key values. This may be because some entities are new and have not yet received database-generated key values. In this case use the 'Add' method or the 'Added' entity state to track the graph and then set the state of non-new entities to 'Unchanged' or 'Modified' as appropriate.
I am confused by what this error message is trying to tell me. Since I have pulled this entry out of the context, the only thing its Primary key should be conflicting with is itself. In addition, I know this entry has been assigned an id since I accessed it using its id.
Lastly, the reason I am setting this entry's state to modified is because calling _ctx.saveChanges() is returning 0, indicating to me that the context isn't aware I've changed anything (when I have).
Can anyone explain why this error is being thrown and what I need to do to make the context aware of my changes?
EDIT
GetProductIncludingProductLists(id):
public Product GetProductIncludingProductLists(int id)
{
try
{
return _ctx.Products.Include("ProductLists")
.ToList()
.Select(p => new Product()
{
ProductId = p.ProductId,
CUSIP = p.CUSIP,
SEDOL = p.SEDOL,
BUID = p.BUID,
Key = p.Key,
Track = p.Track,
ProductLists = ((p.ProductLists.Select(l => new ProductList()
{
ProductListId = l.ProductListId,
Name = l.Name,
Products = null
})
.ToList() as List<ProductList>) ?? null)
})
.First(item => item.ProductId == id);
}
catch (Exception e)
{
System.Diagnostics.Debug.WriteLine(e.Message);
return null;
}
}
The reason for the crazy select statements is because product and product list are related N:N and not including them would cause an error for 'circular serialization'
looking at this code, I'm afraid there is some lack of knowledge on how EF works.
First, to load an entity this code is overcomplicated and does horrible things:
1: this loads all the products in the DB into memory
return _ctx.Products.Include("ProductLists")
.ToList()
2: this copies the properties of already existing objects in new objects
.Select(p => new Product() { // create new object and copy properties !!
3: this queries individually each of the related lists of each and every product in the DB (and creates a new list, when there is one already available)
p.ProductLists.Select( // query the list of each product !!
4: and all of this just to get a product with the given id!
.First(item => item.ProductId == id);
You can simply do this:
return _ctx.Products
.Include(p => p.ProductLists) // it's much safer a lambda than a magic string
.First(item => item.ProductId == id);
which will load only the required product, with its corresponding product lists. And it will be attached to the context.
Second. If your product is already attached to the context, i.e. you've loaded it using the context, for example as I've just shown, and provided that _ctx has not been disposed, the product is already tracked by the context, and you don't need to care about setting its state. Whenever you make any change to it, the context will automatically change its state so that, when you call SaveChanges the changes will be automatically posted to the DB.
As you can see your code is overcomplicated. Try to make some tutorials to understand how EF works. You'll spare a lot of time. You can use the EF section of MSDN. It has clear documentation and examples.
You are returning a new product that is not being change tracked by Entity Framework:
.Select(p => new Product()
the first ToList() bring all products in the context (are you sure you want that ?).
then you create new one with the select
trying to attach the new one conflict with the loaded one.
Related
This relates to my previous question from a few days ago: EF Core duplicate keys: The instance of entity type '' cannot be tracked because another instance with the key value '' is already being tracked
I am using context.ChangeTracker.TrackGraph to give me more control when inserting data. The insert process works great as show by my own answer on the question above. This problem results when I update items and attempt to save them.
My primary model is as follows (type names have been changed for simplicity):
public class Model
{
public List<Data1> List1 { get; set; }
...
}
Data1 looks like this:
public class Data1
{
public string Name{ get; set; }
...
}
To update to the database, I use this:
using var context = Factory.CreateDbContext();
context.ChangeTracker.TrackGraph(model, node =>
{
if (!(node.Entry.Entity.GetType().IsSubclassOf(typeof(EFChangeIgnore)) && node.Entry.IsKeySet)) //this is a workaround but it solves the pk problems I have been having
{
node.Entry.State = EntityState.Modified;
}
else if (!node.Entry.IsKeySet)
{
node.Entry.State = EntityState.Added;
}
});
return await context.SaveChangesAsync();
This works great as it prevents my existing entities (inheriting fromEFChangeIgnore) from being inserted and otherwise handles updates and inserts of other entities well. However, if I have more than one Data item in List1, I get the error:
The instance of entity type 'Data1' cannot be tracked because another instance with the key value '{ID: 0}' is already being tracked. When attaching existing entities, ensure that only one entity instance with a given key value is attached.
I understand this error but I am confused as to why it is appearing in this scenario. Yes, both entities have an ID of zero, but only because they have yet to be inserted to the database. What am I doing wrong here?
It turns out my bug was due to a misplaced parenthesis. Go figure.
In my SaveChanges I had accidently negated the entire conditional at the top, such that entities that should have been Modified became Added, and vice versa. This led to multiple entities with an ID of 0 that I intended to add being marked as Modified. EF then complained that two existing entities had the same ID.
That is, this line:
if (!(node.Entry.Entity.GetType().IsSubclassOf(typeof(EFChangeIgnore)) && node.Entry.IsKeySet))
now becomes (conditionals swapped for clarity):
if (node.Entry.IsKeySet && (!node.Entry.Entity.GetType().IsSubclassOf(typeof(EFChangeIgnore))))
And my entire SaveChanges method now is:
using var context = Factory.CreateDbContext();
context.ChangeTracker.TrackGraph(model, node => //all entities inheriting from EFChangeIgnore will not be touched by the change tracker. Other entities with keys will be modified, and items without keys will be added
{
if (node.Entry.IsKeySet && (!node.Entry.Entity.GetType().IsSubclassOf(typeof(EFChangeIgnore)))) //this is a workaround but it solves key duplication problems
{
node.Entry.State = EntityState.Modified;
}
else if (!node.Entry.IsKeySet)
{
node.Entry.State = EntityState.Added;
}
return await context.SaveChangesAsync();
OK I can delete a single item in EF6 like this:
public void DeleteUserGroup(MY_GROUPS ug)
{
using (var context = new MYConn())
{
var entry = context.Entry(ug);
if (entry.State == EntityState.Detached)
{
context.MY_GROUPS.Attach(ug);
}
context.MY_GROUPS.Remove(ug);
context.SaveChanges();
}
}
If this method changed from passing a single instance of MY_GROUPS to a List<MY_GROUPS> how would I handle the delete?
Would there be a more efficient way then just doing a foreach and setting the state one at a time?
UPDATE:
I am already using a similar method as above utilizing the RemoveRange method.
However I am getting an error:
The object cannot be deleted because it was not found in the
ObjectStateManager.
I'm looking for the best way to attach a list of objects to the context so that I can delete them.
To be able to remove records, you need to make sure your ObjectContext is tracking them. Right now you have detached objects, and your context has no knowledge of them so it's impossible to delete them. One way to remove them is to do like you say, Attach all your objects to the context, then delete them. The other way is to fetch the records from the database so you can remove them:
//Find all groups in database with an Id that is in your group collection 'ug'
var groups = context.My_Groups.Where(g => ug.Any(u => u.Id == g.Id));
context.My_Groups.RemoveRange(groups);
context.SaveChanges();
However, note that even while using RemoveRange, a delete command will be send to the database per item you want to remove. The only difference between RemoveRange and Remove is that the first will only call DetectChanges once, which can really improve performance.
Iterate over your collection and set Deleted state for each
groups.ForEach(group => ctx.Entry(group).State = EntityState.Deleted);
ctx.SaveChanges();
You can use RemoveRange:
context.MY_GROUPS.RemoveRange(context.MY_GROUPS.Where(x => x.columnName== "Foo"));
You can also use ForEach like this:
context.MY_GROUPS.Where(x => x.columnName == "Foo").ToList().ForEach(context.DeleteObject);
context.SaveChanges();
You could also use ObjectContext.ExecuteStoreCommand Method as an another approach for this purpose.
I found this it worked for me. I did it in a loop before calling save changes. I wanted it to create just the delete sql command and it did.
http://www.entityframeworktutorial.net/entityframework6/delete-disconnected-entity-in-entity-framework.aspx
// disconnected entity to be deleted
var student = new Student(){ StudentId = 1 };
using (var context = new SchoolDBEntities())
{
context.Entry(student).State = System.Data.Entity.EntityState.Deleted;
context.SaveChanges();
}
EntityFramework 6+ has made this a bit easier with .RemoveRange().
public void DeleteUserGroup(List<MY_GROUPS> ug)
{
using (var context = new MYConn())
{
context.MY_GROUPS.RemoveRange(ug);
context.SaveChanges();
}
}
By using the solution provided by Alexander Deck where brands is an IEnumerable:
context.Brands.RemoveRange(context.Brands.Where(cb => brands.Any(b => b.Id == cb.Id)));
I got the following error:
Unable to create a constant value of type 'SomeCompany.SomeApp.DB.Entities.Brand'. Only primitive types or enumeration types are supported in this context.
And it was solved by converting the Brands DBSet to IEnumerable with the AsEnumerable() extension method:
context.Brands.RemoveRange(context.Brands.AsEnumerable().Where(cb => brands.Any(b => b.Id == cb.Id)));
That did the trick for me.
I am reading the following tutorial about entity framework 6 Link. And inside the section named ”Adding an Edit Page for Instructors”, the author wrote the following code inside the Post edit action method:-
[HttpPost, ActionName("Edit")]
[ValidateAntiForgeryToken]
public ActionResult EditPost(int? id)
{
if (id == null)
{
return new HttpStatusCodeResult(HttpStatusCode.BadRequest);
}
var instructorToUpdate = db.Instructors
.Include(i => i.OfficeAssignment)
.Where(i => i.ID == id)
.Single();
if (TryUpdateModel(instructorToUpdate, "",
new string[] { "LastName", "FirstMidName", "HireDate", "OfficeAssignment" }))
{
try
{
if (String.IsNullOrWhiteSpace(instructorToUpdate.OfficeAssignment.Location))
{
instructorToUpdate.OfficeAssignment = null;
}
db.Entry(instructorToUpdate).State = EntityState.Modified;
db.SaveChanges();
return RedirectToAction("Index");
}
catch (RetryLimitExceededException /* dex */)
{
//Log the error (uncomment dex variable name and add a line here to write a log.
ModelState.AddModelError("", "Unable to save changes. Try again, and if the problem persists, see your system administrator.");
}
}
return View(instructorToUpdate);
}
This code will cover these three conditions:-
If the user clears the office assignment and it originally had a value, you must remove and delete the OfficeAssignment entity.
If the user enters an office assignment value and it originally was empty, you must create a new OfficeAssignment entity.
If the user changes the value of an office assignment, you must change the value in an existing OfficeAssignment entity.
So does this means that
db.Entry(instructorToUpdate).State = EntityState.Modified;
will cause an insert statement to be performed for the OfficeAssignment record incase the Instructor did not have a prevouse OfficeAssignment object ? and what is the rule that govern this ?
here is the complete model diagram:-
DbContext.Entry method is used to do an explicit loading, that means that It gives you access to all the information that the DbContext has about an entity. This goes beyond the values that are stored in the properties of the actual entity and includes things such as the state of the entity and the original values for each property when it was retrieved from the database.
When you call the TryUpdateModel method, it will update the properties (that you pass their names as a parameter) with values from the model binder. One of these properties is OfficeAssignment, wich is updated too. If in your view you don't enter a Location, then you don't have reason to create a new OfficeAssigment (that's way you need to do instructorToUpdate.OfficeAssignment = null; because even when you don't enter a new Location, you will have a instance of OfficeAssignment). If you add a new Location, you are going to create a new OfficeAssignment, and if you modified the Location, then you are going to modified its value.
When you do this:
db.Entry(instructorToUpdate).State = EntityState.Modified;
You are going to set a flag on the entity indicating it has been changed. When the SaveChanges method is called, the Modified flag causes the Entity Framework to create SQL statements to update the database row. All columns of the database row will be updated, including those that the user didn't change, and concurrency conflicts are ignored. To understand better what happend, you can look the Instructor instance like a tree. Code First recognizes that you have a navegation property, so it need to be updated or insterted(depending on the case). If the OfficeAssignment have an Id different of the default(int) (I'm pressuming that is an interger), then it will be updated, and in other case, it will be inserted.
There are basically two ways an entity can be persisted through EF.
A. Add it directly to the Dbset with the additional relationships you want it to have.
Entity e = new Entity();
e.ForeignEntityId = 123;
context.Entities.Add(e);
context.SaveChanges();
B. Attach it to an existing entity and if that entity is/was untracked, mark that entity as `Modified.
Entity e = new Entity();
ForeignEntity fe = context.Find(...);
//Only needed if 'fe' was untracked
//context.Entry(fe).State = EntityState.Modified;
fe.Entity = e;
context.SaveChanges();
The way presented in your question is the second way. It's all about getting the "new" object to be present in the object graph that represents all tracked EF entities from your DB.
Yes,can load the DbContext.Entry method and can be used to do an explicit loading as mentioned above
I will suggest rather do Delete and Insert until you do not have real-time needs of modification.
{
//Remove existing data
modelname existingobj = dbobj.tablename.Find(id);
dbobj.tablename.Remove(existingobj);
dbobj.SaveChanges();
//Add data
dbobj.Entry(existingobj).State = EntityState.Added;
dbobj.SaveChanges();
}
What's the recommended way of dealing with the following scenario in Entity Framework?
I have some session related data stored in a class called SessionData (which uses the Singleton pattern). One of the properties in this class is called Basket. The Basket class has a collection of BasketItems.
So when the basket is initialised, it gets added to the database and stored in the session:
var basket = new Basket();
using(var db = new DataContext())
{
db.Baskets.Add(basket);
db.SaveChanges();
}
SessionData.Current.Basket = basket;
Then later when a basket item is added to the basket:
using(var db = new DataContext())
{
var basketItem = new BasketItem() { initialisation here }
SessionData.Current.Basket.BasketItems.Add(basketItem);
db.SaveChanges();
}
This doesn't work because SessionData.Current.Basket isn't attached to the current DbContext. I've tried using:
db.Baskets.Attach(SessionData.Current.Basket)
This works the first time it's called, but fails with the following error on following calls:
An object with the same key already exists in the ObjectStateManager. The ObjectStateManager cannot track multiple objects with the same key.
What's the recommended way of doing this update to an entity (and it's hierarchy) that is stored outside the usage of a current DbContext using block?
In the new db context, retrieve the basket from the database, and then create the new basket items.
The problem turned out to be because when I was initialising the properties of the new BasketItem, one of its properties (called Product) was tracked by another instance of DbContext. I hadn't spotted it, as I was thinking about the Basket class, not its sub-properties. I solved this by changing the query where I got the product instance from this:
var product = (from x in db.Products
where x.ID == basketRequest.ProductID
select x).FirstOrDefault();
to:
var product = (from x in db.Products.AsNoTracking()
where x.ID == basketRequest.ProductID
select x).FirstOrDefault();
Which gets the entity instance without it being tracked by the DbContext.
The link in the question that Daniel Auger mentioned in his comment also helped me understand the 'Insert or update pattern', which is what my question was really about. I disagree that the two questions are duplicates, as they're not really the same. Very helpful link though!
I am trying to implement an AuditLog using EF 4.1, by overriding the SaveChanges() method as discussed in the following places:
http://jmdority.wordpress.com/2011/07/20/using-entity-framework-4-1-dbcontext-change-tracking-for-audit-logging/
Entity Framework 4.1 DbContext Override SaveChanges to Audit Property Change
I am having problems with the "modified" entries though. Whenever I attempt to get at the OriginalValue of the property in question, it always has the same value as it does in the CurrentValue field.
I first use this code, and it successfully identifies the Entries that are modified:
public int SaveChanges(string userID)
{
// Have tried both with and without the following line, and received same results:
// ChangeTracker.DetectChanges();
foreach (
var ent in this.ChangeTracker
.Entries()
.Where( p => p.State == System.Data.EntityState.Added ||
p.State == System.Data.EntityState.Deleted ||
p.State == System.Data.EntityState.Modified ))
{
// For each change record, get the audit record entries and add them
foreach (AuditLog log in GetAuditRecordsForChange(ent, userID))
{
this.AuditLog.Add(log);
}
}
return base.SaveChanges();
}
The problem is in this (abbreviated code):
private List<AuditLog> GetAuditRecordsForChange(DbEntityEntry dbEntry, string userID)
{
if (dbEntry.State == System.Data.EntityState.Modified)
{
foreach (string propertyName in dbEntry.OriginalValues.PropertyNames)
{
if (!object.Equals(dbEntry.OriginalValues.GetValue<object>(propertyName),
dbEntry.CurrentValues.GetValue<object>(propertyName)))
{
// It never makes it into this if block, even when
// the property has been updated.
}
// If I updated the property "Name" which was originally "OldName" to the value "NewName" and then break here and inspect the values by calling:
// ?dbEntry.OriginalValues.GetValue<object>("Name").ToString()
// the result will be "NewName" and not "OldName" as expected
}
}
}
The strange thing is that the call to dbEntry.Property(propertyName).IsModified(); will
return true in this case. It is just that the OriginalValue doesn't have the expected value inside. Would anyone be willing to help point me in the right direction? I cannot seem to get this to work correctly.
When EF retrieves an entity from the database it takes a snapshot of the original values for all properties of that entity. Later, as changes are made to the values of these properties the original values will remain the same while the current values change.
However, for this to happen EF needs to be tracking the entity throughout the process. In a web or other n-tier application, typically the values are sent to the client and the context used to query the entity is disposed. This means that the entity is now no longer being tracked by EF. This is fine and good practice.
Once the application posts back the entity is reconstructed using values from the client and then re-attached to the context and set into a Modified state. However, by default the only values that come back from the client are the current values. The original values are lost. Usually this doesn't matter unless you are doing optimistic concurrency or want to be very careful about only updating values that have really changed. In these cases the original values should also be sent to the client (usually as hidden fields in a web app) and then re-applied as the original values as a part of the attach process. This was not happening in the example above and this is why the original values were not showing as expected.
If you change
dbEntry.OriginalValues.GetValue<object>(propertyName);
to
dbEntry.GetDatabaseValues().GetValue<object>(propertyName);
then that works.
I got this error when i override SaveChanges in context As follows
public override int SaveChanges()
{
var changeInfo = ChangeTracker.Entries()
.Select(t => new {
Original = t.OriginalValues.PropertyNames.ToDictionary(pn => pn, pn => t.OriginalValues[pn]),
Current = t.CurrentValues.PropertyNames.ToDictionary(pn => pn, pn => t.CurrentValues[pn]),
}).ToList();
return base.SaveChanges();
}
and when I cleared it fixed!
ChangeTracker.Entries().ToList() in SaveChanges is wrong...
The problem is not in the code you show here. The issue is that how you track entities.
If you just create an entity object and calls Update on it EF framework just overwrite the existing value in db ( provided you supplied correct ID ). That is done for efficiency. So if you do:
var company = new Company{Id = mySuppliedId, Name = newname};
Context.Companies.Update(company);
Context.SaveChanges();
EF will go directly to DB in one shot and update all properties on the entity, without bringing anything back first. So it has no way of knowing the original values.
If you change the code in your logic to something like:
var company = Context.Companies.Where(c=>c.Id == mySuppliedId).FirstOrDefault();
company.Name = newName;
Context.SaveChanges()
Then your ChangeTracker code you showed above all of sudden starts working, as EF brought the data from DB first. It is however less efficient as you make and extra query.
I need the old/original value in post method. Finally this worked for me.
//Get Orignal value before save changes
Item entityBeforeChange = db.Items.Single(x => x.Id == item.Id);
db.Entry(entityBeforeChange).State = EntityState.Detached; // breaks up the connection to the Context
var locId = entityBeforeChange.LocationId;//Orignal value
//Saving the current Value
if (ModelState.IsValid)
{
db.Entry(item).State = EntityState.Modified;
await db.SaveChangesAsync();
return RedirectToAction("Index");
}
You can get data that you haven't committed yet.
var Current = _dbContext.Entry(entity).GetDatabaseValues().ToObject();