Attaching objects retrieved from multiple sources - c#

I have three classes, Fish (which contains two properties of type Chips and MushyPeas respectively), MushyPeas (which contains a property of type Chips) and Chips (which has a Name property).
I am running the following piece of hypothetical code:
int chipsId;
using (var db = new FishContext())
{
var creationChips = new Chips() { Name = "A portion of chips" };
db.Chips.Add(creationChips);
db.SaveChanges();
chipsId = creationChips.ChipsId;
}
Chips retrievedChips1;
using (var db = new FishContext())
{
retrievedChips1 = db.Chips.Where(x => x.ChipsId == chipsId).ToList()[0];
}
Chips retrievedChips2;
using (var db = new FishContext())
{
retrievedChips2 = db.Chips.Where(x => x.ChipsId == chipsId).ToList()[0];
}
using (var db = new FishContext())
{
db.Chips.Attach(retrievedChips1);
db.Chips.Attach(retrievedChips2);
var mushyPeas = new MushyPeas() { Chips = retrievedChips2 };
var fish = new Fish() { Chips = retrievedChips1, MushyPeas = mushyPeas };
db.Fish.Add(fish);
db.ChangeTracker.DetectChanges();
db.SaveChanges();
}
This is to simulate a situation in my real app, in which EF objects (which may actually represent the same database record) are loaded from a variety of different DbContexts and then added to an object tree in another DbContext.
If I don't call the two db.Chips.Attach lines, then brand new Chips entities are created when the Fish object is saved to the database, and assigned new IDs.
Calling db.Chips.Attach solves this issue for one of the retrieved obejcts, but the second Attach call fails with the exception "An object with the same key already exists in the ObjectStateManager. The ObjectStateManager cannot track multiple objects with the same key."
What is the best way to achieve what I want to achieve here?

As a grizzled EF vet, I've come to the conclusion that it's best to avoid using Attach in many cases.
The exception "An object with the same key already exists in the ObjectStateManager. The ObjectStateManager cannot track multiple objects with the same key" is usually misleading since the object that you're trying to attach isn't actually attached to the data context. What happens when you attach an object is it recursively attaches any entities that it references. So, if you attach an entity to the data context, and then attach another entity that references any entity that was implicitly attached previously, you will get this error. The solution is pretty simple:
using (var db = new FishContext())
{
var chips1 = db.Chips.Find(retrievedChips1.Id);
var chips2 = db.Chips.Find(retrievedChips2.Id);
var mushyPeas = new MushyPeas() { Chips = chips2 };
var fish = new Fish() { Chips = chips1, MushyPeas = mushyPeas };
db.Fish.Add(fish);
db.ChangeTracker.DetectChanges();
db.SaveChanges();
}
This guarantees that both entities will be attached to the data context without any sort of ObjectStateManager issues.

You could query the Local collection to check if an entity with the same key is already attached and if yes, use the attached entity:
using (var db = new FishContext())
{
var attachedChips1 = db.Chips.Local
.SingleOrDefault(c => c.ChipsId == retrievedChips1.ChipsId);
if (attachedChips1 == null)
{
db.Chips.Attach(retrievedChips1);
attachedChips1 = retrievedChips1;
}
var attachedChips2 = db.Chips.Local
.SingleOrDefault(c => c.ChipsId == retrievedChips2.ChipsId);
if (attachedChips2 == null)
{
db.Chips.Attach(retrievedChips2);
attachedChips2 = retrievedChips2;
}
var mushyPeas = new MushyPeas() { Chips = attachedChips2 };
var fish = new Fish() { Chips = attachedChips1, MushyPeas = mushyPeas };
//...
}
(The first check doesn't make sense in this simple example because a new context is empty with nothing attached to it. But you get the idea...)
However, in the case that you also want to update the related entities (for example by setting the state to Modified after attaching) it would be a problem if retrievedChips1 and retrievedChips2 have (except the key value) different property values. You had to decide somehow which is the "correct one". But that would be business logic. You just have to hand over one of them to EF and only one. In your scenario it wouldn't matter which one you use because you are only creating a relationship and for this EF will only care about the key value.
Side note: Instead of ...ToList()[0] the more natural way would be ...First() (or Single() in this case because you are querying the key).

Related

Update Object without Select EF6 MySQL

Is it possible to update objects with Entity Framework, without grabbing them first?
Example: Here, I have a function that provides a Primary Key to locate the objects, pulls them, then updates them. I would like to eliminate having to pull the objects first, and simply run an UPDATE query. Removing the need for the SELECT query being generated.
public async Task<int> UpdateChecks(long? acctId, string payorname, string checkaccountnumber, string checkroutingnumber, string checkaccounttype)
{
using (var max = new Max(_max.ConnectionString))
{
var payments = await
max.payments.Where(
w =>
w.maindatabaseid == acctId && (w.paymentstatus == "PENDING" || w.paymentstatus == "HOLD")).ToListAsync();
payments.AsParallel().ForAll(payment =>
{
payment.payorname = payorname;
payment.checkaccountnumber = checkaccountnumber;
payment.checkroutingnumber = checkroutingnumber;
payment.checkaccounttype = checkaccounttype;
payment.paymentmethod = "CHECK";
payment.paymentstatus = "HOLD";
});
await max.SaveChangesAsync();
return payments.Count;
}
}
You can use the Attach() command to attach an entity you already know exists and then call SaveChanges() will will call the appropriate update method. Here is some sample code from the MSDN article on the topic:
on the subject:
var existingBlog = new Blog { BlogId = 1, Name = "ADO.NET Blog" };
using (var context = new BloggingContext())
{
context.Entry(existingBlog).State = EntityState.Unchanged;
// Do some more work...
context.SaveChanges();
}
Note that this is general EF logic, not related to any specific database implementation.

Violation of Unique Constraint when creating new entity in Entity Framework

I have, errr had a working wpf application that manipulates database info (using Entity Framework, database first).
The structure of the data is 4 tables of finance info (all 1:1 mapped to the main table of the 5), with a couple of lookup tables with foreign key refs in the main table.
I added a table (another 1:1 mapping to the main table) in SqlServer and then ran the 'Update Model from Database...' wizard to add the new table to the model. Everything looks alright in the .edmx file, including the '0..1' relationship link.
However, when I try to save, I am receiving a 'Violation of Unique Constraint' error.
My creation code:
private void AddNewStatementsQuery(LGFinanceEntities lGFinanceEntities)
{
StatementsMain newStatement = StatementsMain.CreateStatementsMain(9999, this.LocalGovt.StakeholderID, 161, this.Year.FinancialYearID);
StatementsIncome newInc = StatementsIncome.CreateStatementsIncome(newStatement.StatementsMainID);
StatementsNote newNote = StatementsNote.CreateStatementsNote(newStatement.StatementsMainID);
StatementsRSSFinPos newRSSFinPos = StatementsRSSFinPos.CreateStatementsRSSFinPos(newStatement.StatementsMainID);
StatementsSurplusDeficit newSurplusDeficit = StatementsSurplusDeficit.CreateStatementsSurplusDeficit(newStatement.StatementsMainID);
lGFinanceEntities.StatementsMains.Context.AddObject("StatementsMains", newStatement);
lGFinanceEntities.StatementsMains.Context.AddObject("StatementsIncomes", newInc);
lGFinanceEntities.StatementsMains.Context.AddObject("StatementsNotes", newNote);
lGFinanceEntities.StatementsMains.Context.AddObject("StatementsRSSFinPos", newRSSFinPos);
lGFinanceEntities.StatementsMains.Context.AddObject("StatementsSurplusDeficit", newSurplusDeficit);
if (lGFinanceEntities.SaveChanges() != 1) // this is causing the exception
{
MessageBox.Show("Error. New Statements not created", "Database Error");
}
}
Prior to adding the new table, the above code was working. The only change was the addition of the lines:
StatementsSurplusDeficit newSurplusDeficit =
StatementsSurplusDeficit.CreateStatementsSurplusDeficit(newStatement.StatementsMainID);
...
lGFinanceEntities.StatementsMains.Context.AddObject("StatementsSurplusDeficit",
newSurplusDeficit);
Interestingly, something is creating a record somewhere, because when I check SqlServer I do have new records for the 5 tables. Also interestingly, each time I try something and run the method, the primary key has been incremented by 2. It looks like the same record is being added twice, but I can't work out how.
Edit:
Following a comment suggestion, I changed the 'AddNewStatementsQuery' so lines that looked like:
lGFinanceEntities.StatementsMains.Context.AddObject("StatementsMains", newStatement);
were changed to:
lGFinanceEntities.StatementsMains.AddObject(newStatement);
and then to:
lGFinanceEntities.AddObject("StatementsMains", newStatement);
This did not solve the key violation error.
How do I find out where/how the data is being saved twice (ie, other than lGFinanceEntities.SaveChanges() in the if statement)?
Hmm. Looking at your code, I can see it being simplified down to:
// Create the new objects
var statement = new StatementsMain()
{
this.LocalGovt.StakeholderID, 161, this.Year.FinancialYearID
};
var income = new StatementsIncome()
{
StatementsMain = statement
};
var note = new StatementsNote()
{
StatementsMain = statement
};
var rss = new StatementsRSSFinPos()
{
StatementsMain = statement
};
var surplus = new StatementsSurplusDeficit()
{
StatementsMain = statement
};
// Add the objects into the context
lGFinancialEntities.AddObject(statement);
lGFinancialEntities.AddObject(income);
lGFinancialEntities.AddObject(note);
lGFinancialEntities.AddObject(rss);
lGFinancialEntities.AddObject(surplus);
// Persist the objects to the data storage
lGFinancialEntities.SaveChanges();
Or, even better:
// Create the main object
var statement = new StatementsMain()
{
this.LocalGovt.StakeholderID, 161, this.Year.FinancialYearID
};
// Add the objects into the context
lGFinancialEntities.AddObject(statement);
lGFinancialEntities.AddObject(new StatementsIncome() { StatementsMain = statement });
lGFinancialEntities.AddObject(new StatementsNote() { StatementsMain = statement });
lGFinancialEntities.AddObject(new StatementsRSSFinPos() { StatementsMain = statement });
lGFinancialEntities.AddObject(new StatementsSurplusDeficit() { StatementsMain = statement });
// Persist the objects to the data storage
lGFinancialEntities.SaveChanges();
But, this tells me there is a lot about your data schema that's not obvious here. For instance, what does the value 161 reference in the StatementsMain object?
FYI, assigning the primary object to the objects in which it is a foreign-key lets EF do the work of assigning the new ID to the other objects as they are persisted.

Ternary relationships in Entity Framework

In Entity Framework 4.2 I have a Trips entity that can have 0..* PlacesOfInterest and 0..* Photos. Places of Interest has 1 Trip and 0..* Photos. Photos have 1 Trip and 0..1 Places of Interest.
When I try to add a Photo, I use this method:
public static Guid Create(string tripId, Model.Photo instance)
{
var context = new Model.POCOTripContext();
var cleanPhoto = new Model.Photo();
cleanPhoto.Id = Guid.NewGuid();
cleanPhoto.Name = instance.Name;
cleanPhoto.URL = instance.URL;
//Relate the POI
cleanPhoto.PlaceOfInterest = Library.PlaceOfInterest.Get(instance.PlaceOfInterestId);
context.PlacesOfInterest.Attach(cleanPhoto.PlaceOfInterest);
//Relate the trip
cleanPhoto.Trip = Library.Trip.Get(new Guid(tripId));
context.Trips.Attach(cleanPhoto.Trip);
//Add the photo
context.Photos.AddObject(cleanPhoto);
context.SaveChanges();
return cleanPhoto.Id;
}
When I test this, I get the following when the Trip is attached:
An object with the same key already exists in the ObjectStateManager. The ObjectStateManager cannot track multiple objects with the same key.
The Trip does appear in the context object, but the PlacesOfInterest does too before the Attach statement. I don't understand how this works, can someone clarify?
EDIT: Here are the POI and Trip Getters
public static Model.Trip Get(Guid tripId)
{
using (Model.POCOTripContext context = new Model.POCOTripContext())
{
var tripEntity = context.Trips.Include("PlacesOfInterest").Include("PlacesOfInterest.PoiAttributes").Include("Photos").FirstOrDefault(c => c.Id == tripId) ?? new Model.Trip();
return tripEntity;
}
}
public static Model.PlaceOfInterest Get(Guid poiId)
{
using (Model.POCOTripContext context = new Model.POCOTripContext())
{
var poiEntity = context.PlacesOfInterest.Include("PoiAttributes").FirstOrDefault(c => c.Id == poiId) ?? new Model.PlaceOfInterest();
return poiEntity;
}
}
Thanks
S
This...
context.Trips.Include("PlacesOfInterest")....
...will load the PlacesOfInterest with the trip. When you attach the trip to the other context trip.PlacesOfInterest get attached as well. Because you already have attached a PlaceOfInterest before (which has an Id of a PlaceOfInterest in the collection) you are attaching two objects of the same type with the same key. This causes the exception.
You can actually simplify your code: You don't need to load the entities because you have their primary key. Then you can just create new instances with that key and attach it:
cleanPhoto.PlaceOfInterest = new PlaceOfInterest
{ Id = instance.PlaceOfInterestId };
context.PlacesOfInterest.Attach(cleanPhoto.PlaceOfInterest);
cleanPhoto.Trip = new Trip { Id = new Guid(tripId) };
context.Trips.Attach(cleanPhoto.Trip);

C# Linq is removing a value from my entity

So, in a desperate attempt to wrangle EntityFramework into being usable. I am here..
private MyEntity Update(MyEntity orig)
{
//need a fresh copy so we can attach without adding timestamps
//to every table....
MyEntity ent;
using (var db = new DataContext())
{
ent = db.MyEntities.Single(x => x.Id == orig.Id);
}
//fill a new one with the values of the one we want to save
var cpy = new Payment()
{
//pk
ID = orig.ID,
//foerign key
MethodId = orig.MethodId,
//other fields
Information = orig.Information,
Amount = orig.Amount,
Approved = orig.Approved,
AwardedPoints = orig.AwardedPoints,
DateReceived = orig.DateReceived
};
//attach it
_ctx.MyEntities.Attach(cpy, ent);
//submit the changes
_ctx.SubmitChanges();
}
_ctx is an instance variable for the repository this method is in.
The problem is that when I call SubmitChanges, the value of MethodId in the newly attached copy is sent to the server as 0, when it is in fact not zero if I print it out after the attach but before the submit. I am almost certain that is related to the fact that the field is a foreign key, but I still do not see why Linq would arbitrarily set it to zero when it has a valid value that meets the requirements of the constraint on the foreign key.
What am I missing here?
You should probably set Method = orig.Method, but I can't see your dbml, of course.
I think you need to attach the foreign key reference
var cpy = new Payment()
{
//pk
ID = orig.ID,
//other fields
Information = orig.Information,
Amount = orig.Amount,
Approved = orig.Approved,
AwardedPoints = orig.AwardedPoints,
DateReceived = orig.DateReceived
};
//create stub entity for the Method and Add it.
var method = new Method{MethodId=orig.MethodId)
_ctx.AttachTo("Methods", method);
cpy.Methods.Add(method);
//attach it
_ctx.MyEntities.Attach(cpy, o);
//submit the changes
_ctx.SubmitChanges();

Problem while working with many-to-many relationships in Entity Framework

I create some items from a class X.
I add them to de base, do SaveChanges and all this...
The class Y has a relationship many-to-many with X.
Using another Context, I create a Y instance, putting into the collection of X the elements I've created.
I add Y to Y entity set, it is fine.
When I do Context.SaveChanges(), I get:
A value shared across entities or associations is generated in more than one location. Check that mapping does not split an EntityKey to multiple server-generated columns.
Have you ever seen this error?
EDIT: at the beginning, I've put 1-to-many, after I've noticed it is in fact many-to-many.
EDIT 2: showing the way this is being done. Unlike many people while using .net, we do use layers (business, data...). This is the test case:
[TestMethod]
public void WorksWithAreaCategories()
{
using (new TransactionScope())
{
//arrange
var context = ContextFactory.Create();
var categoryBusiness = new CategoryBusiness(context);
var category = new Category
{
Name = "TestCategory###"
};
categoryBusiness.Add(category);
var areaBusiness = new AreaBusiness(context);
var area = new Area
{
Name = "TestArea###",
Description = "TestAreaDescription###",
Categories = new List<Category> {category}
};
//act
areaBusiness.Add(area);
//assert
var areaFromDb = areaBusiness.FindById(area.AreaID);
Assert.IsNotNull(areaFromDb.Categories);
Assert.IsTrue(areaFromDb.Categories.Count > 0);
Assert.IsTrue(areaFromDb.Categories.Any(c => c.CategoryID == category.CategoryID));
}
}
They share the same context. The business layers call SaveChanges in the end of each Add.
How I resolved it:
After adding both of them, I established the relationship with Update.
[TestMethod]
public void WorksWithAreaCategories()
{
using (new TransactionScope())
{
//arrange
var context = ContextFactory.Create();
var categoryBusiness = new CategoryBusiness(context);
var category = new Category
{
Name = "TestCategory###"
};
categoryBusiness.Add(category);
var areaBusiness = new AreaBusiness(context);
var area = new Area
{
Name = "TestArea###",
Description = "TestAreaDescription###",
};
areaBusiness.Add(area);
//act
area.Categories = new List<Category> { category };
areaBusiness.Update(area);
//assert
var areaFromDb = areaBusiness.FindById(area.AreaID);
Assert.IsNotNull(areaFromDb.Categories);
Assert.IsTrue(areaFromDb.Categories.Count > 0);
Assert.IsTrue(areaFromDb.Categories.Any(c => c.CategoryID == category.CategoryID));
}
}
When the error says "more than one location", it really means more than one context. Data "belongs" in a specific context and you get problems if you try to move it between contexts.
There are 2 ways to fix it:
Do everything in the same context
When you create the Y instance, read the X elements from disk, so that they are in the same context, before adding the X elements to Y.
EDIT based on comment:
Try using a single save changes.

Categories