Entity framework inserts wrong entity into db on savechanges - c#

I am trying to write a program to scan a directory containing tv show folders, look up some details about the shows using tvrage API and then save the details to a database using entity framework.
My TVShow table pkey is the same value as taken from the tvrage database show id, and I am having issues when duplicate or similar folder names are returning the same Show info. In a situation where I have a directory containing three folders, "Alias", "Alias 1" , "Band of Brothers" I get the following output from my code
* TV SHOWS *
Alias....... NO MATCH......ADDING........DONE
Alias 1 ...... NO MATCH.....ADDING....CANT ADD, ID ALREADY EXISTS IN DB
Band of Brothers ...... NO MATCH..ADDING....
Before getting an UpdateException on the context.SaveChanges(); line
Violation of PRIMARY KEY constraint 'PK_TVShows'.
I can see using SQL profiler that the problem is that my app is trying to perform an insert on the alias show for a second time with duplicate key, but I can't see why. When I step through the code on the second interaction of the foreach loop (second "alias" folder), the code to save the show entity to the database is bypassed.
It is only on the next iteration of the foreach loop when I have created a new TVShow entity for "Band of Brothers" do I
actually reach the code which adds a Tvshow to context and saves, at which point the app crashes. In visual studio I can see
at the point of the crash that;
"show" entity in context.TVShows.AddObject(show) is "Band of Brothers" w/ a unique ID
context.TVShows only contains one record, the first Alias Entity
But SQL profiler shows that EntityFramework is instead inserting Alias for a second time, and I am stumped by why this is
private void ScanForTVShowFolders( GenreDirectoryInfo drive ) {
IEnumerable<DirectoryInfo> shows = drive.DirInfo.EnumerateDirectories();
foreach (DirectoryInfo d in shows) {
//showList contains a list of existing TV show names previously queried out of DB
if (showList.Contains(d.Name)) {
System.Console.WriteLine(d.Name + ".....MATCH");
} else {
System.Console.Write(d.Name + "......NO MATCH..ADDING....");
TVShow show = LookUpShowOnline(d.Name, drive.GenreName);
if (show.Id == -1) { // id of -1 means online search failed
System.Console.Write("..........CANT FIND SHOW" + Environment.NewLine);
} else if (context.TVShows.Any(a => a.Id == show.Id)) { //catch duplicate primary key insert
System.Console.Write(".......CANT ADD, ID ALREADY EXISTS IN DB" + Environment.NewLine);
} else {
context.TVShows.AddObject(show);
context.SaveChanges();
System.Console.Write("....DONE" + Environment.NewLine);
}
}
}
private TVShow LookUpShowOnline( string name, string genre ) {
string xmlPath = String.Format("http://services.tvrage.com/feeds/search.php?show='{0}'", name);
TVShow aShow = new TVShow();
aShow.Id = -1; // -1 = Can't find
XmlDocument xmlResp = new XmlDocument();
try { xmlResp.Load(xmlPath); } catch (WebException e) { System.Console.WriteLine(e); }
XmlNode root = xmlResp.FirstChild;
if (root.NodeType == XmlNodeType.XmlDeclaration) { root = root.NextSibling; }
XmlNode tvShowXML;
//if (showXML["episode"] == null)
// return false;
tvShowXML = root["show"];
if (tvShowXML != null) {
aShow.Id = System.Convert.ToInt16(tvShowXML["showid"].InnerText);
aShow.Name = tvShowXML["name"].InnerText.Trim();
aShow.StartYear = tvShowXML["started"].InnerText.Trim();
aShow.Status = tvShowXML["status"].InnerText.Trim();
aShow.TVGenre = context.TVGenres.Where(b => b.Name.Trim() == genre).Single();
}
return aShow;
}
}
Edit
Doing some more reading I added context.ObjectStateManager to my debug watchlist and I can see everytime I create a new TVShow entity a new record is added to _addedEntityStore. Actually if I remove context.TVShows.AddObject(show) the code still updates the database so manually adding to the context seems redundant.

If your are inserting object by foreach loop > better to keep the Primary Key outside and make it increment!
eg: int newID= Shows.Select(d=>d.Id).Max();
foreach(............)
{
show.Id = newID++;
.
.
. //remaining fields
.
context.TVShows.AddObject(show);
}
context.SaveChanges();
it works for me...!!

Turns out context.TVShows.AddObject(show) is unnecessary in my case, I was inadvertently adding all created show entities to the context when this query runs
aShow.TVGenre = context.TVGenres.Where(b => b.Name.Trim() == genre).Single();
This is not what I wanted, I just wanted to create the object, then decide whether to add it. Will be pretty easy to fix now I know why it's happening.

Related

Problem with selecting rows from msi file

I wrote a config tool to easily configure msi installers I create with a Visual Studio Setup project. I successfully edited entries in the InstallExecuteSequence table. Now I would like the change something in the Control table as well but the select query returns 0 entries.
using (Database db = new Database(path, DatabaseOpenMode.Transact))
{
using (var vw = db.OpenView(db.Tables["Control"].SqlSelectString))
{
vw.Execute();
Record record = vw.Fetch(); // <= this always returns null
while (record != null)
{
record = vw.Fetch();
if (record == null)
break;
if (record["Dialog_"].ToString().ToLower().Contains("CustomCheckA") && record["Control"].ToString().ToLower().Contains("Text"))
{
tbName.Text = record["Text"].ToString();
}
if (record["Dialog_"].ToString().ToLower().Contains("CustomCheckA") && record["Control"].ToString().ToLower().Contains("BodyText"))
{
tbDescription.Text = record["Text"].ToString();
}
}
if (String.IsNullOrEmpty(eintrag.IDString))
MessageBox.Show("This file does not contain the searched keywords");
vw.Close();
}
db.Close();
}
I believe you need to add more information of the result you want, but I see something here.
if (record["Dialog_"].ToString().ToLower().Contains("CustomCheckA")
You are converting it to lower and then checking if contains that word, but the word is not all lowercase. So the result is always false.

Insert into multiple tables using WCF Transactions

I am trying to add an entry into a table and use the primary key of that added entry to create an additional entry into another table.
The error I am getting is
The transaction manager has disabled its support for remote/network
transactions. (Exception from HRESULT: 0x8004D024)
I believe this is caused by creating multiple connections within a single TransactionScope, but I am doing everything within one context / using statement, so I do not believe that I should be receiving this error.
Service
[OperationBehavior(TransactionScopeRequired = true)]
public void CreateGroup(NewGroupData data)
{
var groupRepo = _GroupRepo ?? new InvestigatorGroupRepository();
groupRepo.CreateGroup(data.UserId, data.InvestigatorGroupName, data.HasGameAssignment, data.InstitutionId);
}
Repository
public void CreateGroup(string userId, string investigatorGroupName, bool hasGameAssignment, int institutionId)
{
using (var context = new GameDbContext())
{
var newGroup = new InvestigatorGroup()
{
InvestigatorGroupName = investigatorGroupName,
HasGameAssignment = hasGameAssignment,
InstitutionId = institutionId,
IsTrashed = false
};
int institutionUserId =
context.InstitutionUsers.Where(
iu => !iu.IsTrashed && iu.APUser.UserId == userId && iu.InstitutionId == institutionId).Select(iu => iu.InstitutionUserId).Single();
var newGroupUser = new InvestigatorGroupUser()
{
InstitutionUserId = institutionUserId,
InvestigatorGroup = newGroup,
CreationDate = DateTime.Now
};
context.InvestigatorGroupUsers.Add(newGroupUser);
context.SaveChanges();
}
}
You start with a wrong assumption.
The line...
int newGroupId = context.InvestigatorGroups.Add(newGroup).InvestigatorGroupId;
...will always assign 0 to newGroupId. The Add method only marks the entity for insert, but doesn't actually insert it. Only SaveChanges writes data to the database, not any other method in Entity Framework.
So the assignment...
InvestigatorGroupId = newGroupId,
...is faulty as well. You have to assign the new InvestigatorGroup to a navigation property in InvestigatorGroupUser:
InvestigatorGroup = newGroup,
Add this navigation property to InvestigatorGroupUser if you haven't got it yet.
If you have that, it's enough to execute these lines:
context.InvestigatorGroupUsers.Add(newGroupUser);
context.SaveChanges();
No need to Add the newGroup object too, It will be added by adding newGroupUser.
So if you do that, the only transaction you need is the one that SaveChanges uses internally by default. For the code you show, you don't need a TransactionScope. If this is part of a greater WCF transaction the story may be different, but I think at least you needed some misconceptions to be straightened out.

Entity Framework 4.3 - Code First - Update List Property

As a follow-up to my earlier question, I now know that EF doesn't just save all of the changes of the entire entity for me automatically. If my entity has a List<Foo>, I need to update that list and save it. But how? I've tried a few things, but I can't get the list to save properly.
I have a many-to-many association between Application and CustomVariableGroup. An app can have one or more groups, and a group can belong to one or more apps. I believe I have this set up correctly with my Code First implementation because I see the many-to-many association table in the DB.
The bottom line is that the Application class has a List<CustomVariableGroup>. My simple case is that the app already exists, and now a user has selected a group to belong to the app. I want to save that change in the DB.
Attempt #1
this.Database.Entry(application).State = System.Data.EntityState.Modified;
this.Database.SaveChanges();
Result: Association table still has no rows.
Attempt #2
this.Database.Applications.Attach(application);
var entry = this.Database.Entry(application);
entry.CurrentValues.SetValues(application);
this.Database.SaveChanges();
Result: Association table still has no rows.
Attempt #3
CustomVariableGroup group = application.CustomVariableGroups[0];
application.CustomVariableGroups.Clear();
application.CustomVariableGroups.Add(group);
this.Database.SaveChanges();
Result: Association table still has no rows.
I've researched quite a bit, and I've tried more things than I've shown, and I simply don't know how to update an Application's list with a new CustomVariableGroup. How should it be done?
EDIT (Solution)
After hours of trial and error, this seems to be working. It appears that I need to get the objects from the DB, modify them, then save them.
public void Save(Application application)
{
Application appFromDb = this.Database.Applications.Single(
x => x.Id == application.Id);
CustomVariableGroup groupFromDb = this.Database.CustomVariableGroups.Single(
x => x.Id == 1);
appFromDb.CustomVariableGroups.Add(groupFromDb);
this.Database.SaveChanges();
}
While I consider this a bit of a hack, it works. I'm posting this in the hopes that it helps someone else save an entire day's worth of work.
public void Save(Application incomingApp)
{
if (incomingApp == null) { throw new ArgumentNullException("incomingApp"); }
int[] groupIds = GetGroupIds(incomingApp);
Application appToSave;
if (incomingApp.IdForEf == 0) // New app
{
appToSave = incomingApp;
// Clear groups, otherwise new groups will be added to the groups table.
appToSave.CustomVariableGroups.Clear();
this.Database.Applications.Add(appToSave);
}
else
{
appToSave = this.Database.Applications
.Include(x => x.CustomVariableGroups)
.Single(x => x.IdForEf == incomingApp.IdForEf);
}
AddGroupsToApp(groupIds, appToSave);
this.Database.SaveChanges();
}
private void AddGroupsToApp(int[] groupIds, Application app)
{
app.CustomVariableGroups.Clear();
List<CustomVariableGroup> groupsFromDb2 =
this.Database.CustomVariableGroups.Where(g => groupIds.Contains(g.IdForEf)).ToList();
foreach (CustomVariableGroup group in groupsFromDb2)
{
app.CustomVariableGroups.Add(group);
}
}
private static int[] GetGroupIds(Application application)
{
int[] groupIds = new int[application.CustomVariableGroups.Count];
int i = 0;
foreach (CustomVariableGroup group in application.CustomVariableGroups)
{
groupIds[i] = group.IdForEf;
i++;
}
return groupIds;
}

dbset.local updating database: a duplicate value cannot be inserted into a unique index

I am getting an error when calling entities.savechanges() on my EF 4.3.1. My database is a sql ce v4 store and I am coding in the mvvm pattern. I have a local version of my context that I send to an observable collection and modify etc. This works fine, and when I call savechanges() when no rows exist in the database the objects persist fine. When I reload the application, the objects are populated in my listbox as they should, however if I add another object and call savechanges() I get an error saying that a duplicate value cannot be inserted into a unique index.
From my understanding it means that the context is trying to save my entities to the datastore, but it seems to be adding my untouched original objects as well as the new one. I thought it would leave them alone, since their state is unchanged.
private void Load()
{
entities.Properties.Include("Images").Load();
PropertyList = new ObservableCollection<Property>();
PropertyList = entities.Properties.Local;
//Sort the list (based on previous session stored in database)
var sortList = PropertyList.OrderBy(x => x.Sort).ToList();
PropertyList.Clear();
sortList.ForEach(PropertyList.Add);
propertyView = CollectionViewSource.GetDefaultView(PropertyList);
if (propertyView != null) propertyView.CurrentChanged += new System.EventHandler(propertyView_CurrentChanged);
private void NewProperty()
{
try
{
if (PropertyList != null)
{
Property p = new Property()
{
ID = Guid.NewGuid(),
AgentName = "Firstname Lastname",
Address = "00 Blank Street",
AuctioneerName = "Firstname Lastname",
SaleTitle = "Insert a sales title",
Price = 0,
NextBid = 0,
CurrentImage = null,
Status = "Auction Pending",
QuadVis = false,
StatVis = false, //Pause button visibility
Sort = PropertyList.Count + 1,
};
PropertyList.Add(p);
SaveProperties();
}
private void SaveProperties()
{
try
{
foreach (var image in entities.Images.Local.ToList())
{
if (image.Property == null)
entities.Images.Remove(image);
}
}
catch (Exception ex)
{
System.Windows.MessageBox.Show(ex.Message);
}
entities.SaveChanges();
}
Without commenting on all the code here this is the bit that's causing the specific problem you bring up:
//Sort the list (based on previous session stored in database)
var sortList = PropertyList.OrderBy(x => x.Sort).ToList();
PropertyList.Clear();
sortList.ForEach(PropertyList.Add);
This code:
Starts with entities that have been queried and are being tracked by the context as Unchanged entities. That is, entities that are known to already exist in the database.
Creates a new sorted list of these entities.
Calls Clear on the local collection causing each tracked entity to be marked as deleted and removed from the collection.
Adds each entity back to the context putting it now in an Added state meaning that it is new and will be saved to the database when SaveChanges is called,
So effectively you have told EF that all the entities that exist in the database actually don't exist and need to be saved. So it tries to do this and it results in the exception you see.
To fix this don't clear the DbContext local collection and add entities back. Instead you should sort in the view using the local collection to back the view.
It sounds like you're adding the existing entities to the context (which marks them for insertion) instead of attaching them (which marks them as existing, unmodified).
I'm also not sure that new Guid() isn't returning the same guid... I always use Guid.NewGuid() http://msdn.microsoft.com/en-us/library/system.guid.newguid.aspx

bulk insert and update with ADO.NET Entity Framework

I am writing a small application that does a lot of feed processing. I want to use LINQ EF for this as speed is not an issue, it is a single user app and, in the end, will only be used once a month.
My questions revolves around the best way to do bulk inserts using LINQ EF.
After parsing the incoming data stream I end up with a List of values. Since the end user may end up trying to import some duplicate data I would like to "clean" the data during insert rather than reading all the records, doing a for loop, rejecting records, then finally importing the remainder.
This is what I am currently doing:
DateTime minDate = dataTransferObject.Min(c => c.DoorOpen);
DateTime maxDate = dataTransferObject.Max(c => c.DoorOpen);
using (LabUseEntities myEntities = new LabUseEntities())
{
var recCheck = myEntities.ImportDoorAccess.Where(a => a.DoorOpen >= minDate && a.DoorOpen <= maxDate).ToList();
if (recCheck.Count > 0)
{
foreach (ImportDoorAccess ida in recCheck)
{
DoorAudit da = dataTransferObject.Where(a => a.DoorOpen == ida.DoorOpen && a.CardNumber == ida.CardNumber).First();
if (da != null)
da.DoInsert = false;
}
}
ImportDoorAccess newIDA;
foreach (DoorAudit newDoorAudit in dataTransferObject)
{
if (newDoorAudit.DoInsert)
{
newIDA = new ImportDoorAccess
{
CardNumber = newDoorAudit.CardNumber,
Door = newDoorAudit.Door,
DoorOpen = newDoorAudit.DoorOpen,
Imported = newDoorAudit.Imported,
RawData = newDoorAudit.RawData,
UserName = newDoorAudit.UserName
};
myEntities.AddToImportDoorAccess(newIDA);
}
}
myEntities.SaveChanges();
}
I am also getting this error:
System.Data.UpdateException was unhandled
Message="Unable to update the EntitySet 'ImportDoorAccess' because it has a DefiningQuery and no element exists in the element to support the current operation."
Source="System.Data.SqlServerCe.Entity"
What am I doing wrong?
Any pointers are welcome.
You can do multiple inserts this way.
I've seen the exception you're getting in cases where the model (EDMX) is not set up correctly. You either don't have a primary key (EntityKey in EF terms) on that table, or the designer has tried to guess what the EntityKey should be. In the latter case, you'll see two or more properties in the EDM Designer with keys next to them.
Make sure the ImportDoorAccess table has a single primary key and refresh the model.

Categories

Resources