Entity Framework modifying collection property with detect changes off - c#

For performance reasons I have AutoDetectChangesEnabled = false on the DbContext.
Updating simple properties and reference properties all works fine but I am having trouble with collection properties that are many-to-many and don't have a joining class.
This is abbreviated code trying to add to the collection:
var item = context.Set<Item>().FirstOrDefault();
var category = context.Set<Category>().FirstDefault();
context.Entry(item).Collection(i => i.Categories).CurrentValue.Add(category);
But it does nothing, after SaveChanges the database is same as it was. Is this the correct way to be doing this?

Call:
context.ChangeTracker.DetectChanges();
Or:
context.Entry(item).State = EntityState.Modified;

I always thought that EF executed DetectChanges as part of SaveChanges no matter what. But inspecting the source code reveals that even then DetectChanges isn't executed when AutoDetectChangesEnabled is false.
I think in your case, the best you can do is override SaveChanges so it will always detect changes before saving:
public override int SaveChanges()
{
var detectChanges = this.Configuration.AutoDetectChangesEnabled;
try
{
this.Configuration.AutoDetectChangesEnabled = true;
return base.SaveChanges();
}
finally
{
this.Configuration.AutoDetectChangesEnabled = detectChanges;
}
}
An alternative would be to call ChangeTracker.DetectChanges(); in the override, but by setting AutoDetectChangesEnabled = true, EF itself will choose the moment when to call DetectChanges during SaveChanges, which seems preferable to me.

Related

ASP.NET C#: Entity updating is being blocked

Experiencing an issue about updating mysql DB through EF. It's not the first time I'm dealing with it, so I had some ideas about why isn't my data getting changed. I tried changing an element in goods array; tried editing an object, recieved through LINQ-request (seen some examples of this method); made some attempts on marking element found in the database before editing (like EntityState and Attach()). Nothing of these made any difference, so I tried removing <asp:UpdatePanel> from Site.Master to see what happens (responsive for postback blocking to prevent page shaking on update), but nothing changed (while btnRedeemEdit.IsPostBack having its default value).
Code below is the function I use for updates.
protected void btnRedeemEdit_Click(object sender, EventArgs e)
{
if (!string.IsNullOrEmpty(Request.QueryString["id"]))
{
var db = new GoodContext();
var goods = db.Goods.ToList();
Good theGood = goods.FirstOrDefault(x => x.Id == int.Parse(Request.QueryString["id"]));
//db.Goods.Attach(theGood);//No effect
//db.Entry(theGood).State = System.Data.Entity.EntityState.Modified; //No effect
if (theGood != default)
{
theGood.AmountSold = GetInput().AmountSold;
theGood.APF = GetInput().APF;
theGood.Barcode = GetInput().Barcode;
theGood.Description = GetInput().Description;
theGood.ImagesUrl = GetInput().ImagesUrl;//"https://i.pinimg.com/564x/2d/b7/d8/2db7d8c53b818ce838ad8bf6a4768c71.jpg";
theGood.Name = GetInput().Name;
theGood.OrderPrice = GetInput().OrderPrice;
theGood.Profit = GetInput().Profit;
theGood.RecievedOn = GetInput().RecievedOn;//DateTime.Parse(GetInput().RecievedOn).Date.ToString();
theGood.TotalAmount = GetInput().TotalAmount;
theGood.WeightKg = GetInput().WeightKg;
//SetGoodValues(goods[editIndex],GetInput());//Non-working
db.SaveChanges();
Response.Redirect("/AdminGoods");
}
else Response.Write($"<script>alert('Good on ID does not exist');</script>");
}
else Response.Write($"<script>alert('Unable to change: element selected does not exist');</script>");
}
Notice, that no alerts appear during execution, so object in database can be found.
Are there any more things, that can be responsible for blocking database updates?
A few things to update & check:
Firstly, DbContexts should always be disposed, so in your case wrap the DbContext inside a using statement:
using (var db = new GoodContext())
{
// ...
}
Next, there is no need to load all goods from the DbContext, just use Linq to retrieve the one you want to update:
using (var db = new GoodContext())
{
Good theGood = db.Goods.SingleOrDefault(x => x.Id == int.Parse(Request.QueryString["id"]));
if (theGood is null)
{
Response.Write($"<script>alert('Good on ID does not exist');</script>");
return;
}
}
The plausible suspect is what does "GetInput()" actually do, and have you confirmed that it actually has the changes you want? If GetInput is a method that returns an object containing your changes then it only needs to be called once rather than each time you set a property:
(Inside the using() {} scope...)
var input = GetInput();
theGood.AmountSold = input.AmountSold;
theGood.APF = input.APF;
theGood.Barcode = input.Barcode;
theGood.Description = input.Description;
// ...
db.SaveChanges();
If input has updated values but after calling SaveChanges you aren't seeing updated values in the database then there are two things to check.
1) Check that the database connection string at runtime matches the database that you are checking against. The easiest way to do that is to get the connection string from the DbContext instance's Database.
EF 6:
using (var db = new GoodContext())
{
var connectionString = db.Database.Connection.ConnectionString; // Breakpoint here and inspect.
EF Core: (5/6)
using (var db = new GoodContext())
{
var connectionString = db.Database.GetConnectionString();
Often at runtime the DbContext will be initialized with a connection string from a web.config / .exe.config file that you don't expect so you're checking one database expecting changes while the application is using a different database / server. (More common than you'd expect:)
2) Check that you aren't disabling tracking proxies. By default EF will enable change tracking which is how it knows if/when data has changed for SaveChanges to generate SQL statements. Sometimes developers will encounter performance issues and start looking for ways to speed up EF including disabling change tracking on the DbContext. (A fine option for read-only systems, but a pain for read-write)
EF6 & EF Core: (DbContext initialization)
Configuration.AutoDetectChangesEnabled = false; // If you have this set to false consider removing it.
If you must disable change tracking then you have to explicitly set the EntityState of the entity to Modified before calling SaveChanges():
db.Entry(theGood).State = EntityState.Modified;
db.SaveChanges();
Using change tracking is preferable to using EntityState because with change tracking EF will only generate an UPDATE statement if any values have changed, and only for the values that changed. With EntityState.Modified EF will always generate an UPDATE statement for all non-key fields regardless if any of them had actually changed or not.

ChangeTracker.StateChanged of DbContext gets triggered when calling .Entry()

I implemented similar solution on how we can modify created and updated date upon saving data through EF Core as what is suggested here Populate Created and LastModified automagically in EF Core.
void OnEntityStateChanged(object sender, EntityStateChangedEventArgs e)
{
if (e.NewState == EntityState.Modified && e.Entry.Entity is IHasCreationLastModified entity)
entity.LastModified = DateTime.Now;
}
At first I thought this will be triggered only when SaveChanges() is called. But apparently it is also called on Entry()
// Get entity
var student = _dbContext.Students.Find(studentId);
// Modify student object
student.Name = "New student name";
// Called Entry(), trigger ChangeTracker.StateChanged
var entry = _dbContext.Entry(student);
// Doesn't trigger ChangeTracker.StateChanged
_dbContext.SaveChanges();
I found that ChangeTracker.StateChanged is triggered when _dbContext.Entry(student) is called. Then it doesn't get triggered again when _dbContext.SaveChanges() is called. And it also passes the condition above if (e.NewState == EntityState.Modified && e.Entry.Entity is IHasCreationLastModified entity).
My assumption why it is not triggered again when SaveChanges() is called, because there is no new update to the entity after Entity() is called.
This results in the LastModified property being assigned when .Entry(student) is called, instead of when .SaveChanges() is called.
Is there a way to only update LastModified property once when SaveChanges is called on the scenario above?
I suggest that you could override you SaveChanges method in your dbContext. You could refer to below code that I usually use.
public class ForumContext : DbContext
{
public ForumContext(DbContextOptions<ForumContext> options) : base(options)
{
}
//other settings
public override int SaveChanges(bool acceptAllChangesOnSuccess)
{
foreach (var entry in ChangeTracker.Entries())
{
switch (entry.State)
{
case EntityState.Added:
((BaseEntity)entry.Entity).AddedDate = DateTime.Now;
((BaseEntity)entry.Entity).LastModified = DateTime.Now;
break;
case EntityState.Modified:
((BaseEntity)entry.Entity).LastModified = DateTime.Now;
break;
case EntityState.Deleted:
entry.State = EntityState.Modified;
entry.CurrentValues["IsDeleted"] = true;
break;
}
}
return base.SaveChanges(acceptAllChangesOnSuccess);
}
I thought you might like to know why you were getting the the events you saw in your question.
When you execute the line student.Name = "New student name";, then, by default, nothing happens because EF Core hasn't called the ChangeTracker.DetectChanges method yet so it doesn't know anything has changed.
But the call to var entry = _dbContext.Entry(student); then runs a version of the ChangeTracker.DetectChanges - see the code below taken from the EF Core code.
public virtual EntityEntry<TEntity> Entry<TEntity>([NotNull] TEntity entity) where TEntity : class
{
Check.NotNull<TEntity>(entity, nameof (entity));
this.CheckDisposed();
EntityEntry<TEntity> entityEntry = this.EntryWithoutDetectChanges<TEntity>(entity);
//My comment - this runs a version of the DetectChanges method.
this.TryDetectChanges((EntityEntry) entityEntry);
return entityEntry;
}
EF Core's Entry method does this because you might ask for the State of the entity and therefore it has to call DetectChanges to make sure its up to date.
Now, it turns out that if you do the following
student.Name = "New student name";
_dbContext.SaveChanges();
Then (in EF Core 5 preview, but I think it is the same in EF Core 3.1) you get two events.
OldState.EntityState == Unchanged, newState.EntityState == Modified - that is triggered by the call to DetectChanges.
OldState.EntityState == Modified, newState.EntityState == Unchanged - that is triggered by SaveChanges when it set the state to say the database matches the entity class.
If you do the following
student.Name = "New student name";
var entry = _dbContext.Entry(student);
_dbContext.SaveChanges();
Then you would get the same events. The DetectChanges would be called twice (once by Entry and once by SaveChanges), but there is no change in the State on the second call the DetectChanges
You can see this in my unit tests in the repo I am writing to support my book Entity Framework Core in Action. I'm writing the section on these events and found your question and though I would answer it.
I hope it helps you understand what is going on, but I should say that the other answers suggesting overriding SaveChanges is a better solution than using these events.

Entity Framework SaveChanges() not updating the database

var paymentAttempt = _auctionContext.PaymentAttempts.Where(o => o.Id == paymentAttemptId).SingleOrDefault();
if (paymentAttempt != null)
{
paymentAttempt.PaymentAttemptStatusId = (int)PaymentAttemptStatus.Defunct;
paymentAttempt.PaymentAttemptStatus = _auctionContext.PaymentAttemptStatuses.Where(pas => pas.Id == paymentAttempt.PaymentAttemptStatusId).First();
var relevantWinningBidsTotalPrices = _auctionContext.GetWinningBidsTotalPricesForPaymentAttempt(paymentAttemptId).ToArray();
foreach (var winningBid in relevantWinningBidsTotalPrices)
{
winningBid.Locked = false;
_auctionContext.UpdateObject(winningBid);
}
_auctionContext.SaveChanges();
}
In the above code after
_auctionContext.SaveChanges();
is called winningBid is updated as expected but paymentAttempt isn't. Why is this? It is really frustrating. There is no error either. I would expect a failure to occur if there was a problem like EF wasn't tracking the object or something like that, but no such error is happening.
That's because you need to pass the paymentAttempt object to your context, to let it know that it is an object that needs to be updated.
For example, assuming that _auctionContext is an instance of DbContext:
// any changes related to the paymentAttempt object
_auctionContext.Entry(paymentAttempt).State = EntityState.Modified;
foreach (var winningBid in relevantWinningBidsTotalPrices)
{
winningBid.Locked = false;
_auctionContext.UpdateObject(winningBid);
}
_auctionContext.SaveChanges();
Another option is the Attach method:
_auctionContext.Attach(paymentAttempt);
_auctionContext.ObjectStateManager.ChangeObjectState(paymentAttempt, System.Data.EntityState.Modified);
If you don't have Entry try adding:
using System.Data.Entity.Infrastructure;
using System.Data.Entity;
then you may simply use:
_auctionContext.Entry(paymentAttempt).State = EntityState.Modified;
_auctionContext.SaveChanges();
I fell on this question but for a different problem. I discovered that if you call SaveChanges() on an object that hasn't been modified, EF will not update anything. This makes sense, but I needed the DB to be updated so that other users would see that a SaveChanges() had been executed, regardless of whether any fields had changed. To force an update without changing any fields:
Dim entry As DbEntityEntry = entities.Entry(myentity)
entry.State = Entity.EntityState.Modified
I know this is late but there's another explanation worth mentioning. Even though your field name contains ID and may be set to autoincrement, be sure to verify that you declared it in your table the primary key.

Why doesn't entity framework concretize my entity's one to many relationship?

I am using a code-first approach with Entity Framework, and a repository pattern to get entities back from my database. In my data model, each OverallEvent has many EventInConcept children. I want my GetEvents method to return an IList of OverallEvents, and I want the children of the aforementioned relationship to be concretized such that they can be accessed outside my DbContext (which AssessmentSystemContext is). This is the code I currently have:
public IList<OverallEvent> GetEvents() {
using (var context = new AssessmentSystemContext()) {
return context.OverallEvents
.Select(evnt => new {
OverallEvent = evnt,
// evnt.EventsInConcept is a public virtual ICollection<EventInConcept>
ConcreteEventsInConcept = evnt.EventsInConcept
})
.AsEnumerable()
.Select(evntData => {
evntData.OverallEvent.EventsInConcept = evntData.ConcreteEventsInConcept.ToList();
// foreach (var eic in evntData.OverallEvent.EventsInConcept) {
// eic.Name = eic.Name;
// }
return evntData.OverallEvent;
})
.ToList();
}
}
It gives me back a list of OverallEvent entities, which is fine, but the trouble is that if I try to access the child relationship EventsInConcept, I get an error. For example:
EventRepository repoEvent = new EventRepository();
var gotEvents = repoEvent.GetEvents();
var firstEventInConcept = gotEvents[0].EventsInConcept.FirstOrDefault();
... gives me the error "The ObjectContext instance has been disposed and can no longer be used for operations that require a connection."
I understood from the answer to an earlier question that if I projected EventsInConcept into a wrapper object, then explicitly set it in a later .Select call (ie. evntData.OverallEvent.EventsInConcept = evntData.ConcreteEventsInConcept.ToList();), it would concretize this one:many relationship and I would be able to access EventsInConcept outside of the DbContext, but it isn't working here. Note that if I uncomment the foreach loop, it starts working, so to get it to work I have to explicitly set a property on every single entry of EventsInConcept. I don't really want to have to do this (I'm picking an arbitrary property, .Name, which feels wrong anyway). Is there a better way?
Disable lazy loading for this query. It is of no use in that situation and when you dispose the context after the entities have been retrieved:
public IList<OverallEvent> GetEvents() {
using (var context = new AssessmentSystemContext()) {
context.Configuration.LazyLoadingEnabled = false;
return ...
}
}
It might be possible that EF doesn't recognize that the collection has been loaded when you use a projection (instead of eager or explicit loading) and triggers lazy loading as soon as you access the collection.

Adding and deleting many-to-many using DbContext API

I am using Entity Framework and DbContext API do build my application but I am having trouble working with objects with many-to-many relations. A simplified save-method could look like this
public void MyObj_Save(MyObj myobj)
{
DbContext.Entry(myobj).State = EntityState.Added;
DbContext.SaveChanges();
}
This code works fine, but if MyObj contains a many-to-many relation this is not saved. I know from using the old POCO API, that I needed to attach the related objects to the context but I cannot find a way to do this correctly with the DbContext API - a simplified example below
public void MyObj_Save(MyObj myobj, List<OtherObj> otherObjList)
{
foreach (OtherObj otherObj in otherObjList)
{
DbContext.OtherObj.Attach(otherObj);
myobj.OtherObj.Add(otherObj);
}
DbContext.Entry(myobj).State = EntityState.Added;
DbContext.SaveChanges();
}
I get no error, but the relations are not saved. What to do?
I quote your (important!) comment:
The objects I send to the method are attached and EntityState is
Unchanged. The configuration of my DbContext is, that I have disabled
AutoDetectChangesEnabled...
So, your code would look like this:
DbContext.Configuration.AutoDetectChangesEnabled = false;
DbContext.Entry(myobj).State = EntityState.Unchanged;
foreach (OtherObj otherObj in otherObjList)
DbContext.Entry(otherObj).State = EntityState.Unchanged;
// entering MyObj_Save method here
foreach (OtherObj otherObj in otherObjList)
{
//DbContext.OtherObj.Attach(otherObj); // does not have an effect
myobj.OtherObj.Add(otherObj);
}
DbContext.Entry(myobj).State = EntityState.Added;
DbContext.SaveChanges();
And this indeed doesn't work because EF doesn't notice that you have changed the relationship between myobj and the list of OtherObj in the line myobj.OtherObj.Add(otherObj); because you have disabled automatic change detection. So, no entries will be written into the join table. Only myobj itself will be saved.
You cannot set any state on an entity to put the state manager into a state that the relationship is saved because it is not an entity state which is important here but a relationship state. These are separate entries in the object state manager which are created and maintained by change detection.
I see three solution:
Set DbContext.Configuration.AutoDetectChangesEnabled = true;
Call DetectChanges manually:
//...
DbContext.Entry(myobj).State = EntityState.Added;
DbContext.ChangeTracker.DetectChanges();
DbContext.SaveChanges();
Detach the new myobj from the context before you set it into Added state (this feels very hacky to me):
// entering MyObj_Save method here
DbContext.Entry(myobj).State = EntityState.Detached;
foreach (OtherObj otherObj in otherObjList)
//...
Maybe it is possible - by getting to the ObjectContext through the IObjectContextAdapter - to modify the relationship entries in the object state manager manually but I don't know how.
In my opinion, this procedure to manipulate entity (and relationship) states manually is not the way you are supposed to work with EF. AutoDetectChangesEnabled has been introduced to make working with EF easier and safer and the only recommended situation to disable it is a high performance requirement (for example for bulk inserts). If you disable automatic change detection without need you are running into problems like this which are difficult to detect and it requires advanced knowledge of EF's inner workings to fix those bugs.
public void MyObj_Save(MyObj myobj, List<OtherObj> otherObjList)
{
DbContext.Entry(myobj).State = EntityState.Added;
foreach (OtherObj otherObj in otherObjList)
{
(((System.Data.Entity.Infrastructure.IObjectContextAdapter)DbContext)
.ObjectContext)
.ObjectStateManager
.ChangeRelationshipState(myobj, otherObj,
q => q.OtherObjs, EntityState.Added);
}
DbContext.SaveChanges();
}
Again, it is a simplified and not a real life example!

Categories