Update Child Collection with EF 4.3 - Losing association in DB - c#

This is driving me nuts. I'm not sure what else to try. This is my latest attempt to update a list of objects within another object using EF 4.3.
The scenario is that a user has added a new Task to an Application that already has one task in its Tasks property. The Application is not attached to the DB context because it was retrieved in a prior logic/DB call. This is the class and property:
public class Application : EntityBase
{
public ObservableCollection<TaskBase> Tasks { // typical get/set code here }
}
This is my attempt to update the list. What happens is that the new Task gets added and the association correctly exists in the DB. However, the first task, that wasn't altered, has its association removed in the DB (its reference to the Application).
This is the Save() method that takes the Application that the user modified:
public void Save(Application newApp)
{
Application appFromContext;
appFromContext = this.Database.Applications
.Include(x => x.Tasks)
.Single(x => x.IdForEf == newApp.IdForEf);
AddTasksToApp(newApp, appFromContext);
this.Database.SaveChanges();
}
And this is the hooey that's apparently necessary to save using EF:
private void AddTasksToApp(Application appNotAssociatedWithContext, Application appFromContext)
{
List<TaskBase> originalTasks = appFromContext.Tasks.ToList();
appFromContext.Tasks.Clear();
foreach (TaskBase taskModified in appNotAssociatedWithContext.Tasks)
{
if (taskModified.IdForEf == 0)
{
appFromContext.Tasks.Add(taskModified);
}
else
{
TaskBase taskBase = originalTasks.Single(x => x.IdForEf == taskModified.IdForEf); // Get original task
this.Database.Entry(taskBase).CurrentValues.SetValues(taskModified); // Update with new
}
}
}
Can anyone see why the first task would be losing its association to the Application in the DB? That first task goes through the else block in the above code.
Next, I'll need to figure out how to delete one or more items, but first things first...

After continual trial and error, this appears to be working, including deleting Tasks. I thought I'd post this in case it helps someone else. I'm also hoping that someone tells me that I'm making this more complicated than it should be. This is tedious and error-prone code to write when saving every object that has a list property.
private void AddTasksToApp(Application appNotAssociatedWithContext, Application appFromContext)
{
foreach (TaskBase taskModified in appNotAssociatedWithContext.Tasks)
{
if (taskModified.IdForEf == 0)
{
appFromContext.Tasks.Add(taskModified);
}
else
{
TaskBase taskBase = appFromContext.Tasks.Single(x => x.IdForEf == taskModified.IdForEf); // Get original task
this.Database.Entry(taskBase).CurrentValues.SetValues(taskModified); // Update with new
}
}
// Delete tasks that no longer exist within the app.
List<TaskBase> tasksToDelete = new List<TaskBase>();
foreach (TaskBase originalTask in appFromContext.Tasks)
{
TaskBase task = appNotAssociatedWithContext.Tasks.Where(x => x.IdForEf == originalTask.IdForEf).FirstOrDefault();
if (task == null)
{
tasksToDelete.Add(originalTask);
}
}
foreach (TaskBase taskToDelete in tasksToDelete)
{
appFromContext.Tasks.Remove(taskToDelete);
this.Database.TaskBases.Remove(taskToDelete);
}
}

Related

ASP.NET MVC Updating Master-Detail Records in Single HTTP Post Request

I'm teaching myself C# and MVC but have a background in SQL. When updating an existing master-detail set of records in a single action (let's say for instance a customer order and order details), updating the master record is no problem. Regarding the detail records, I'm seeing examples that simply delete all existing details and then add them back in rather than add, delete or update only what's changed. That seems easy and effective but involves unnecessary changes to database records and might be an issue in complex relationships.
I've tried writing code that checks the existing values against posted values to determine the right EntityState (Added, Deleted, Modified, Unchanged) for each detail. Accomplishing this using LINQ Except and Intersect works but seems to cause an unexpected performance hit.
(Instead, I could load the original values in an "oldValue" hidden field in the original GET request to compare to the POST values except that would be unreliable in a multi-user environment and seems like a bad idea.)
I'll be happy to provide code examples, but my question is more about best practices. Is there a preferred method for updating existing master-detail sets of records?
EDIT: I've added the code below in response to questions. In this example, our application allows additional attributes to be attached to a product, kept in a separate table ProductAttributes. The view allows the user to edit both the product and the attributes on the same webpage and save at the same time. The code works fine but seems slow and lags at SaveChanges.
public ActionResult Edit(Product product)
{
if (ModelState.IsValid)
{
db.Entry(product).State = EntityState.Modified;
// Establish entity states for product attributes.
List<ProductAttribute> existingAttributes = new List<ProductAttribute>();
existingAttributes = db.ProductAttributes.AsNoTracking()
.Where(x => x.Sku == product.Sku).ToList();
// Review each attribute that DID NOT previously exist.
foreach (ProductAttribute pa in product.ProductAttributes
.Except(existingAttributes, new ProductAttributeComparer()))
{
if (pa.Value is null)
{
// Value didn't exist and still doesn't.
db.Entry(pa).State = EntityState.Unchanged;
}
else
{
// New value exists that didn't before.
db.Entry(pa).State = EntityState.Added;
}
}
// Review each attribute that DID previously exist.
foreach (ProductAttribute pa in product.ProductAttributes
.Intersect(existingAttributes, new ProductAttributeComparer()))
{
if (pa.Value is null)
{
// Value existed and has been cleared.
db.Entry(pa).State = EntityState.Deleted;
}
else
{
if (pa.Value != existingAttributes
.FirstOrDefault(x => x.Attribute == pa.Attribute).Value)
{
// Value has been altered.
db.Entry(pa).State = EntityState.Modified;
}
else
{
// Value has not been altered.
db.Entry(pa).State = EntityState.Unchanged;
}
}
}
db.SaveChanges();
return RedirectToAction("Details", new { id = product.ProductId });
}
return View(product);
}
internal class ProductAttributeComparer : IEqualityComparer<ProductAttribute>
{
public bool Equals(ProductAttribute x, ProductAttribute y)
{
if (string.Equals(x.Attribute, y.Attribute,
StringComparison.OrdinalIgnoreCase))
{
return true;
}
return false;
}
public int GetHashCode(ProductAttribute obj)
{
return obj.Attribute.GetHashCode();
}
}
}

EF 6 Connection pool and stored query strings RAM leak

I have 2 problems.
First one is that Connection pool and (OracleConnectionImpl) is growing steadily over time, till it reaches out of memory exception. As you can see on the image, it has 90 MB over 3-4 hours of running.
I am using short lived contexts everywhere, but it keeps on growing and never clears itself. Is there any way for me to clear it?
Second one is that EF stores too much duplicates of string queries over time.
It mostly stores those which come from .Reload() function, because it is not paramterized, it hardcode the ID into the query.
And then there are strings like "ID" which is somewhere cached 2947x.
Is there any way for the .Reload() function to make it parametrized, or to clear all of those stored strings?
This app is refreshing warehouse jobs and palletes every few seconds, to keep it in sync for all machines and I am not aware of better way than .Reload() because of WPF bindings.
Edit 1
I have simple helper function for reloading many intities at once, even one extension. It doesnt matter that it is passed as object, because the same problem remains even with the last example.
public static void ReloadEntities(bool dispatch, params IEnumerable<object>[] entities)
{
using (var ctx = new eWMSEntities())
{
if (dispatch)
{
Application.Current.Dispatcher.Invoke(DispatcherPriority.Background, (SendOrPostCallback)delegate
{
entities.SelectMany(x => x.Select(s => s)).ToList().ForEach(entity =>
{
ctx.Set(entity.GetType()).Attach(entity);
ctx.Entry(entity).Reload();
ctx.Entry(entity).State = EntityState.Detached;
});
}, null);
}
else
{
entities.SelectMany(x => x.Select(s => s)).ToList().ForEach(entity =>
{
ctx.Set(entity.GetType()).Attach(entity);
ctx.Entry(entity).Reload();
ctx.Entry(entity).State = EntityState.Detached;
});
}
ctx.Dispose();
}
}
public static void ReloadEntity(this object entity, bool dispatch)
{
using (var ctx = new eWMSEntities())
{
ctx.Set(entity.GetType()).Attach(entity);
if (dispatch)
{
Application.Current.Dispatcher.Invoke(DispatcherPriority.Background, (SendOrPostCallback)delegate
{
ctx.Entry(entity).Reload();
}, null);
}
else
{
ctx.Entry(entity).Reload();
}
ctx.Entry(entity).State = EntityState.Detached;
ctx.Dispose();
}
}
while (true && JobLines.Contains(line))
{
using (var ctx = new eWMSEntities())
{
ctx.T_JOB_LINES.attach(line);
ctx.entry(line).Reload();
}
await Task.Delay(3000);
}
This is snapshot after 3 days of running
I'm not recommend but if there are no any other solution...
Maybe you can try
GC.Collect();
Let system force to collect something didn't important in your memory.
Use the "Unit of work" pattern - it will solve many problems. I did not find for WPF, but found for ASP.NET MVC

EntityFramework doesn't refresh navigation properties

I've got a simple relationship
I've created a simple app with the Model like the above one. Model in the application must be updated every time DB changes. I can get the latest changes by calling GetDBChanges Stored Procedure. (see method T1Elapsed)
here is the app:
class Program
{
private static int? _lastDbChangeId;
private static readonly MASR2Entities Model = new MASR2Entities();
private static readonly Timer T1 = new Timer(1000);
private static readonly Timer T2 = new Timer(1000);
private static Strategy _strategy = null;
static void Main(string[] args)
{
using (var ctx = new MASR2Entities())
{
_lastDbChangeId = ctx.GetLastDbChangeId().SingleOrDefault();
}
_strategy = Model.Strategies.FirstOrDefault(st => st.StrategyId == 224);
T1.Elapsed += T1Elapsed;
T1.Start();
T2.Elapsed += T2Elapsed;
T2.Start();
Console.ReadLine();
}
static void T2Elapsed(object sender, ElapsedEventArgs e)
{
Console.WriteLine("All rules: " + Model.StrategyRules.Count());
Console.WriteLine("Strategy: name=" + _strategy.Name + " RulesCount=" + _strategy.StrategyRules.Count);
}
private static void T1Elapsed(object sender, ElapsedEventArgs e)
{
T1.Stop();
try
{
using (var ctx = new MASR2Entities())
{
var changes = ctx.GetDBChanges(_lastDbChangeId).ToList();
foreach (var dbChange in changes)
{
Console.WriteLine("DbChangeId:{0} {1} {2} {3}", dbChange.DbChangeId, dbChange.Action, dbChange.TableName, dbChange.TablePK);
switch (dbChange.TableName)
{
case "Strategies":
{
var id = Convert.ToInt32(dbChange.TablePK.Replace("StrategyId=", ""));
Model.Refresh(RefreshMode.StoreWins, Model.Strategies.AsEnumerable());
}
break;
case "StrategyRules":
{
var id = Convert.ToInt32(dbChange.TablePK.Replace("StrategyRuleId=", ""));
Model.Refresh(RefreshMode.StoreWins, Model.StrategyRules.AsEnumerable());
}
break;
}
_lastDbChangeId = dbChange.DbChangeId;
}
}
}
catch (Exception ex)
{
Console.WriteLine("ERROR: " + ex.Message);
}
finally
{
T1.Start();
}
}
}
When I run it, this is a sample output:
All rules: 222
Strategy: name=Blabla2 RulesCount=6
then I add a row to the child table (Strategy Rule),
DbChangeId:1713 I StrategyRules StrategyRuleId=811
All rules: 223
Strategy: name=Blabla2 RulesCount=7
and finally, I remove the row from StrategyRules
DbChangeId:1714 D StrategyRules StrategyRuleId=811
All rules: 222
Strategy: name=Blabla2 RulesCount=7
Why RulesCount is still 7? How I can force EF to refresh "Navigation Property"?
What I am missing here?
---EDIT--- to cover Slauma's answer
case "StrategyRules":
{
var id = Convert.ToInt32(dbChange.TablePK.Replace("StrategyRuleId=", ""));
if (dbChange.Action == "I")
{
//Model.Refresh(RefreshMode.StoreWins, Model.StrategyRules.AsEnumerable());
}
else if (dbChange.Action == "D")
{
var deletedRule1 = Model.StrategyRules.SingleOrDefault(sr => sr.Id == id);
//the above one is NULL as expected
var deletedRule2 = _strategy.StrategyRules.SingleOrDefault(sr => sr.Id == id);
//but this one is not NULL - very strange, because _strategy is in the same context
//_strategy = Model.Strategies.FirstOrDefault(st => st.StrategyId == 224);
}
}
ObjectContext.Refresh refreshes the scalar properties of the entities you pass into the method along with any keys that refer to related entities. If an entity you pass into the method does not exist anymore in the database because it has been deleted in the meantime Refresh does nothing with the attached entity and just ignores it. (That's a guess from my side, but I could not explain otherwise why you 1) don't get an exception on Refresh (like "cannot refresh entity because it has been deleted") and 2) the entity is apparently still attached to the context.)
Your Insert case does not work because you call Refresh but it works because you load the whole StrategyRules table into memory in this line:
Model.Refresh(RefreshMode.StoreWins, Model.StrategyRules.AsEnumerable())
Refresh enumerates the collection in the second parameter internally. By starting the iteration it triggers the query which is just Model.StrategyRules = load the whole table. AsEnumerable() is only a switch from LINQ-to-Entities to LINQ-to-Objects, that is, every LINQ operator you would apply after AsEnumerable() is performed in memory, not on the database. Since you don't apply anything, AsEnumerable() actually has no effect on your query.
Because you load the whole table the recently inserted StrategyRule will be loaded as well and together will the key to the _strategy entity. The ObjectContext's automatic relationship fixup establishes the relationship to the navigation collection in _strategy and _strategy.StrategyRules.Count will be 7. (You could remove the Refresh call and just call Model.StrategyRules.ToList() and the result would still be 7.)
Now, all this does not work in the Delete case. You still run a query to load the whole StrategyRules table from the database but EF won't remove or detach entities from the context that are not in the result set anymore. (And as far as I know there is no option to force such an automatic removal.) The deleted entity is still in the context with its key refering to strategy and the count will remain 7.
What I am wondering is why you don't leverage that your set of DBChanges apparenty knows exactly what has been removed in the dbChange.TablePK property. Instead of using Refresh couldn't you use something like:
case "StrategyRules":
{
switch (dbChange.Action)
{
case "D":
{
var removedStrategyRule = _strategy.StrategyRules
.SingleOrDefault(sr => sr.Id == dbChange.TablePK);
if (removedStrategyRule != null)
_strategy.StrategyRules.Remove(removedStrategyRule);
}
break;
case ...
}
}
break;

Entity Framework - "The relationship between the two objects cannot be defined" error, but I think I'm using the same context

In my ViewModel I have some code like that:
public class OrderViewModel
{
private UserOrder order;
private DeliveryCentre deliveryCentre;
// This is my EF Container
private CatalogueContainer catalogue = new CatalogueContainer();
// do some stuff...
public void Save()
{
if (order == null)
{
order = catalogue.UserOrders.CreateObject();
}
// do some other stuff...
if ((deliveryCentre == null)
|| (deliveryCentre.Id != deliveryCentreId))
{
deliveryCentre = catalogue.DeliveryCentres.First(centre => centre.Id == deliveryCentreId);
//Causes a context error, not sure why...
order.DeliveryCentre= deliveryCentre;
}
catalogue.SaveChanges();
}
So when the delivery centre is new and the order is new, I am hit by the old "The relationship between the two objects cannot be defined because they are attached to different ObjectContext objects" error, which seems a trifle unfair to me - I just can't figure out what I need to do to make them belong more to the same object context. I assume this is due to some fundamental misunderstanding of the behaviour of Entity Framework.
You are not disposing your context. It may be possible that one of the entities order or deliveryCentre is attached to an old context which still holds references to the entities. You can create and dispose your context with an using statement inside of the Save method instead to using it as a member variable:
public void Save()
{
using (var catalogue = new CatalogueContainer())
{
// your code...
}
}
And remove the private catalogue member.
The solution turned out to only be indirectly related to the error message- #Slauma asked about the //do stuff... placeholders and when I commented those out the error disappeared.
It turned out that there was another relationship there, where I was creating the object as this.Item = new Item() rather than using this.Item = catalogue.Items.CreateObject() so it was being created out of context and when it was added to the order, although the order itself was created from the local context, when the Item was added to it this was somehow dirtying up the context but for some reason this only showed up as a problem when I added the next related object.

DeleteOnSubmit LINQ exception "Cannot add an entity with a key that is already in use"

edit: in case anyone is wondering, the actionhandler invokes code that creates and disposes the same kind of datacontext, in case that might have anything to do with this behaviour. the code doesn't touch the MatchUpdateQueue table, but i figure i should mention it just in case.
double edit: everyone who answered was correct! i gave the answer to the respondent who suffered most of my questioning. fixing the problem allowed another problem (hidden within the handler) to pop up, which happened to throw exactly the same exception. whoops!
I'm having some issues with deleting items in LINQ. The DeleteOnSubmit call in the code below causes a LINQ Exception with the message "Cannot add an entity with a key that is already in use." I'm not sure what I'm doing wrong here, it is starting to drive me up the wall. The primary key is just an integer autoincrement column and I have no other problems until I try to remove an item from the database queue. Hopefully I'm doing something painfully retarded here that is easy to spot for anyone who isn't me!
static void Pacman()
{
Queue<MatchUpdateQueue> waiting = new Queue<MatchUpdateQueue>();
events.WriteEntry("matchqueue worker thread started");
while (!stop)
{
if (waiting.Count == 0)
{
/* grab any new items available */
aDataContext db = new aDataContext();
List<MatchUpdateQueue> freshitems = db.MatchUpdateQueues.OrderBy(item => item.id).ToList();
foreach (MatchUpdateQueue item in freshitems)
waiting.Enqueue(item);
db.Dispose();
}
else
{
/* grab & dispatch waiting item */
MatchUpdateQueue item = waiting.Peek();
try
{
int result = ActionHandler.Handle(item);
if (result == -1)
events.WriteEntry("unknown command consumed : " + item.actiontype.ToString(), EventLogEntryType.Error);
/* remove item from queue */
waiting.Dequeue();
/* remove item from database */
aDataContext db = new aDataContext();
db.MatchUpdateQueues.DeleteOnSubmit(db.MatchUpdateQueues.Single(i => i == item));
db.SubmitChanges();
db.Dispose();
}
catch (Exception ex)
{
events.WriteEntry("exception while handling item : " + ex.Message, EventLogEntryType.Error);
stop = true;
}
}
/* to avoid hammering database when there's nothing to do */
if (waiting.Count == 0)
Thread.Sleep(TimeSpan.FromSeconds(10));
}
events.WriteEntry("matchqueue worker thread halted");
}
You could do something to the effect of
db.MatchUpdateQueues.DeleteOnSubmit(db.MatchUpdateQueues.Single(theItem => theItem == item));
Just a note as other answers hinted towards Attach.. you will not be able to use attach on a context other then the original context the item was received on unless the entity has been serialized.
Try wrapping the entire inside of the while loop in a using statement for a single data context:
Queue<MatchUpdateQueue> waiting = new Queue<MatchUpdateQueue>();
events.WriteEntry("matchqueue worker thread started");
while (!stop)
{
using (var db = new aDataContext())
{
if (waiting.Count == 0)
{
/* grab any new items available */
List<MatchUpdateQueue> freshitems = db.MatchUpdateQueues
.OrderBy(item => item.id)
.ToList();
foreach (MatchUpdateQueue item in freshitems)
waiting.Enqueue(item);
}
...
}
}
Use:
aDataContext db = new aDataContext();
item = new MatchUpdateQueue { id=item.id }; // <- updated
db.MatchUpdateQueues.Attach(item);
db.MatchUpdateQueues.DeleteOnSubmit(item);
db.SubmitChanges();
Since you are using a new datacontext it doesn't know that the object is already in the db.
Remove the first db.Dispose() dispose. It can be the problem code because the entities keep a reference to their data context, so you don't want to dispose it while you are still be working with the instances. This won't affect connections, as they are open/closed only when doing operations that need them.
Also don't dispose the second data context like that, since an exception won't call that dispose code anyway. Use the using keyword, which will make sure to call dispose whether or not an exception occurs. Also grab the item from the db to delete it (to avoid having to serialize/deserialize/attach).
using (aDataContext db = new aDataContext())
{
var dbItem = db.MatchUpdateQueues.Single(i => i.Id == item.Id);
db.MatchUpdateQueues.DeleteOnSubmit(dbItem);
db.SubmitChanges();
}
try this if your TEntity's (here Area) Primary Key is of type Identity column;
Just it, without any change in your SP or Model:
public void InitForm()
{
'bnsEntity is a BindingSource and cachedAreas is a List<Area> created from dataContext.Areas.ToList()
bnsEntity.DataSource = cachedAreas;
'A nominal ID
newID = cachedAreas.LastOrDefault().areaID + 1;
'grdEntity is a GridView
grdEntity.DataSource = bnsEntity;
}
private void tsbNew_Click(object sender, EventArgs e)
{
var newArea = new Area();
newArea.areaID = newID++;
dataContext.GetTable<Area>().InsertOnSubmit(newArea);
bnsEntity.Add(newArea);
grdEntity.MoveToNewRecord();
}

Categories