How to save data with EF in the right way (multiple tables) - c#

For Example you have two tables you which are connected (code first) by a ICollection property.
public class EntityEnviroment
{
[Key]
public virtual int env_id { get; set; }
public virtual string env_name { get; set; }
public virtual string env_country { get; set; }
public virtual ICollection<StcEntityFailedReportDetail> failedReportDetails { get; set; }
}
public class EntityFailedReportDetail
{
[Key]
public virtual int failed_reports_details_id { get; set; }
public virtual int report_id { get; set; }
public virtual string report_status { get; set; }
public virtual StcEntityEnviromentStatus StcEntityEnviromentStatus { get; set; }
}
For one table I create a instance of the context and on for my entry and the I add it. At last I save it. For multiple tables I did this:
using (var db = new StatusPlatformContext())
{
var entryDetail = new EntityFailedReportDetail();
foreach (var value in result.failed_report_details)
{
entryDetail.report_id = value.report_id;
entryDetail.report_status = value.report_status;
db.StcEntityFailedReportDetails.Add(entryDetail);
}
var entry = new EntityEnviroment
{
env_name = result.environment_status.env_name,
env_country = "Ger",
failedReportDetails = new List<EntityFailedReportDetail> { entryDetail }
};
entryDetail.EntityEnviroment = entry;
db.EntityEnviromentStat.Add(entry);
db.SaveChanges();
}
If I do it like this only the last entry of the details are saved. If I add a db.SaveChanges() to the foreach I will just get an entry in the foreigen key column for the last of the three detail rows.
How I should structure the code? All examples I found just show one table not multiple. Is there any example with a pattern what I should read?
Thanks

There are two problems
1) You need to create a new EntityFailedReportDetail for each result.
2) Additionally, add each EntityFailedReportDetail to the EntityEnvironment as they are made.
Try this:
var entry = new EntityEnviroment
{
env_name = result.environment_status.env_name,
env_country = "Ger",
failedReportDetails = new List<EntityFailedReportDetail>()
};
foreach (var value in result.failed_report_details)
{
var entryDetail = new EntityFailedReportDetail();
entryDetail.report_id = value.report_id;
entryDetail.report_status = value.report_status;
entry.failedReportDetails.Add(entryDetail);
}

For entities that contain collections of other entities, you should initialize these collections on construction:
public class EntityEnviroment
{
[Key]
public virtual int env_id { get; set; }
public virtual string env_name { get; set; }
public virtual string env_country { get; set; }
public virtual ICollection<StcEntityFailedReportDetail> failedReportDetails { get; set; } = new List<StcEntityFailedReportDetail>();
}
This way you can use these collections on new entities right out of the gate. Anywhere in your code that "sets" a collection should be flagged for investigation. It is not a way to clear a collection from your DB for example so if anywhere other than this (or a constructor) does an = new List<TEntity>() that is a problem.
var entryDetail = new EntityFailedReportDetail();
foreach (var value in result.failed_report_details)
{
entryDetail.report_id = value.report_id;
entryDetail.report_status = value.report_status;
db.StcEntityFailedReportDetails.Add(entryDetail);
}
Your problem with this code is that you are initializing 1 "detail" record, then in a loop, updating it's details and trying to "Add" it to the DbSet. This is one single reference.
Then this code:
failedReportDetails = new List<EntityFailedReportDetail> { entryDetail }
Would simply initialize a collection with that one reference with the latest detail you added.
Adjusting your example:
using (var db = new StatusPlatformContext())
{
var entry = new EntityEnviroment
{
env_name = result.environment_status.env_name,
env_country = "Ger",
};
foreach (var value in result.failed_report_details)
{
var entryDetail = new EntityFailedReportDetail
{
report_id = value.report_id,
report_status = value.report_status,
EntityEnvironment = entry
};
entry.failedReportDetails.Add(entryDetail);
}
db.EntityEnviromentStat.Add(entry);
db.SaveChanges();
}
You don't need to explicitly add each detail to the context's DbSet of details, and if you don't need to query details outside of the entry, your context doesn't even need a DbSet of details. EF will manage the related entities so you only need DbSets for "top level" entities, basically the parent entities that your system references individually. You can always query related entities through their parents.

Related

How to update entity with IntersectionTable in c# ef5 db first approach?

I created my database and started developing a web application in c# with EF5 and the DB First approach. I can modify my entities on their own data fields but donĀ“t get it to work when it comes to updating relationships. A simple relationship example is Project <- ProjectCategoryIntersection -> Category
Model:
public class Project
{
public TProject project { get; private set; }
public List<string> Categories { get; set; }
}
public partial class TProject //generated table object
{
public virtual ICollection<TProjectCategoryIntersection> TProjectCategoryIntersection { get; set; }
}
public partial class TProjectCategoryIntersection
{
public int Id { get; set; }
public int ProjectId { get; set; }
public int ProjectCategoryId { get; set; }
public virtual TProject T_Project { get; set; }
public virtual TCategory T_ProjectCategory { get; set; }
}
Save:
public void SaveProject(Project project)
{
var context = new ProjectManagementEntities();
TProject projectToUpdate = new TProject();
projectToUpdate.Id = project.Id;
foreach (var category in project.Categories)
{
var cat = (from c in context.TProjectCategory
where c.Name == category
select c).FirstOrDefault();
var inters = new TProjectCategoryIntersection() { ProjectCategoryId = cat.Id, ProjectId = project.project.Id, TProject = project.project, TProjectCategory = cat };
projectToUpdate.TProjectCategoryIntersection.Add(inters);
}
var entry = context.Entry(projectToUpdate).State = EntityState.Modified; //throws exceptions
context.SaveChanges();
}
exception:
Conflicting changes to the role 'TProject' of the relationship 'ProjectManagementModel.FK_TProjectCategoryIntersection_TProject' have been detected.
I also receive a multiple instances ChangeTracker exception when i try to add the categories directly to the project object:
project.project.TProjectCategoryIntersection.Add(inters);
Should i remove the generated table object from my model?
public class Project
{
public TProject project { get; private set; } //remove this?
public List<string> Categories { get; set; }
}
Solution
I ended up removing the generated table object public TProject project { get; private set; } and changed my code to:
public void SaveProject(Project project)
{
var context = new ProjectManagementEntities();
var projectToUpdate = context.T_Project.Find(project.Id);
foreach (var item in projectToUpdate.T_ProjectCategoryIntersection.ToList())
{
var oldCat = context.T_ProjectCategoryIntersection.Find(item.Id);
context.T_ProjectCategoryIntersection.Remove(oldCat);
}
foreach (var category in project.Categories)
{
var cat = (from c in context.T_ProjectCategory
where c.Name == category
select c).FirstOrDefault();
var inters = new T_ProjectCategoryIntersection() { ProjectCategoryId = cat.Id, ProjectId = project.Id };
context.T_ProjectCategoryIntersection.Add(inters);
}
//more code...
context.Entry(projectToUpdate).State = EntityState.Modified;
context.SaveChanges();
}
Apperantly this happens when you use reference to an object and also an Integer for the ID within the same object and change both of them. When this happens EF can not know which one is the correct reference
Try setting only Ids and set null for references like
var inters = new TProjectCategoryIntersection() { ProjectCategoryId = cat.Id,
ProjectId = project.project.Id};

Entity Framework inserting new rows instead of updating them

I have a problem when I am updating data to database. When I want to update data, Entitiy Framework adds new rows to tables that can have multiple rows (tables that have foreign key).
Database model:
When I update Phone/Contact or Tags entity, Entity Framework automatically adds new row instead of updating it
Here is code that I used:
public string UpdateContact(Contact contact)
{
if (contact != null)
{
int id = Convert.ToInt32(contact.id);
Contact Updatecontact = db.Contacts.Where(a => a.id == id).FirstOrDefault();
Updatecontact.firstname = contact.firstname;
Updatecontact.lastname = contact.lastname;
Updatecontact.address = contact.address;
Updatecontact.bookmarked = contact.bookmarked;
Updatecontact.city = contact.city;
Updatecontact.notes = contact.notes;
Updatecontact.Emails1 = contact.Emails1;
Updatecontact.Phones1 = contact.Phones1;
Updatecontact.Tags1 = contact.Tags1;
db.SaveChanges();
return "Contact Updated";
}
else
{
return "Invalid Record";
}
}
EDIT:
Here is EF Model code:
Contact:
public partial class Contact
{
public Contact()
{
this.Emails1 = new HashSet<Email>();
this.Phones1 = new HashSet<Phone>();
this.Tags1 = new HashSet<Tag>();
}
public int id { get; set; }
public string firstname { get; set; }
public string lastname { get; set; }
public string address { get; set; }
public string city { get; set; }
public Nullable<byte> bookmarked { get; set; }
public string notes { get; set; }
public virtual ICollection<Email> Emails1 { get; set; }
public virtual ICollection<Phone> Phones1 { get; set; }
public virtual ICollection<Tag> Tags1 { get; set; }
}
Emails/Tags and Phone have same model (with different name for value)
public partial class Email
{
public int id { get; set; }
public int id_contact { get; set; }
public string email1 { get; set; }
public virtual Contact Contact1 { get; set; }
}
Update properties rather than set new objects.
Updatecontact.Emails1.email1 = contact.Emails1.email1;
Updatecontact.Phones1.number = contact.Phones1.number;
Updatecontact.Tags1.tag1 = contact.Tags1.tag1;
Edit: seems that your contact model has lists of emails, phones and tags. If this is so, then simple assignment won't work. Instead, when sent from the client, you have to find one-by-one and update:
foreach ( var email in contact.Emails1 )
{
// first make sure the object is retrieved from the database
var updateemail = Updatecontact.Emails1.FirstOrDefault( e => e.id == email.id );
// then update its properties
updateemail.email1 = email.email1;
}
// do the same for phones and tags
It's doing that because you're setting the different HashSet values to the values of a completely different collection, namely from what you call contact in that method. In order for you to properly do an update, you're going to have to loop through the emails, phones, and tags to check if those need to be added/updated/deleted on the actual object that you're trying to update.
First, why do you have to search for the contact if you are already receiving it by parameter? That makes me think that you are creating a new one because you are in a different context, if so, then it creates a new record because you have 2 different object in 2 different context.
Try using just one object in the same context to update, EF should mark the object to modification by itself, if not then try making sure before saving that your object has EntityState.Modified.

Recursive Entity Update

I have an entity which holds a list of entities (same as root entity) to represent a Folder structure:
public class SopFolder
{
public int Id { get; set; }
public string Name { get; set; }
public DateTime? LastUpdated { get; set; }
public int Status { get; set; }
public virtual ICollection<SopField> SopFields { get; set; }
public virtual ICollection<SopFolder> SopFolderChildrens { get; set; }
public virtual ICollection<SopBlock> Blocks { get; set; }
public virtual ICollection<SopReview> Reviews { get; set; }
}
This entity is stored in my DB using Code-First Approach which is working fine. I then print the entity to a KendoUI Treeview, let the user modify it and on "save" post it back to the Server to an Action as IEnumerable<TreeViewItemModel> items.
I then look for the ROOT entity with all it's children (there is only one root) and convert it back into an SopFolder object.
To get the full object updated in the database I do the following:
List<SopFolder> sopfolderlist = ConvertTree(items.First());
SopFolder sopfolder = sopfolderlist[0];
if (ModelState.IsValid)
{
SopFolder startFolder = new SopFolder { Id = sopfolder.Id };
//db.SopFolders.Attach(startFolder);
// db.SopFolders.Attach(sopfolder);
startFolder.Name = sopfolder.Name;
startFolder.LastUpdated = sopfolder.LastUpdated;
startFolder.SopFields = sopfolder.SopFields;
startFolder.SopFolderChildrens = sopfolder.SopFolderChildrens;
startFolder.Status = sopfolder.Status;
db.Entry(startFolder).State = EntityState.Modified;
db.SaveChanges();
return Content("true");
}
However this is not working. The model is not updated at all. If I shift the "entityState.Modified" before the modifications, it just creates a complete fresh duplicate of my data in the database (modified of course).
Is my approach correct or do I have to go a different path? What am I missing here? I guess there is another "hidden" id which lets the EF map the entities to the db entries but I am not sure about this. Thanks for help!
UPDATE:
Instead of creatinga new instance of SopFolder I also tried db.SopFolders.Find(sopfolder.Id) and this works for entries with no children. If I have entities with children, it creates a duplicate.
Regards,
Marcus
This is typical Disconnected Graph scenario. Please see this question for possible solutions:
Disconnected Behavior of Entity Framework when Updating Object Graph
You have already figure out the first solution - that is: update entities separately. Actually, what you should do is to fetch the original data from database and then do comparison of what have changed. There are some generic ways of doing that, some of them are described in "Programming EF DbContext" book by J.Lerman, which I strongly recommend to you before doing more coding using EF.
P.S. IMHO this is the worse downside of EF.
Replace SopFolder startFolder = new SopFolder { Id = sopfolder.Id }; with
SopFolder startFolder = db.SopFolders.FirstOrDefault(s=>s.Id.Equals(sopfolder.Id));
// then validate if startFolder != null
I recommend you to create your entity model with ParentId, not children object list. When you need treeview model collect it with recursive function from database.
public class SopFolder
{
public int Id { get; set; }
public string Name { get; set; }
public DateTime? LastUpdated { get; set; }
public int Status { get; set; }
public virtual ICollection<SopField> SopFields { get; set; }
//public virtual ICollection<SopFolder> SopFolderChildrens { get; set; }
public int? ParentFolderId { get; set; }
public virtual ICollection<SopBlock> Blocks { get; set; }
public virtual ICollection<SopReview> Reviews { get; set; }
}
When you create children folders, select it's parent, so collect your data. In childrens case try this :
List<SopFolder> sopfolderlist = ConvertTree(items.First());
SopFolder sopfolder = sopfolderlist[0];
if (ModelState.IsValid)
{
SopFolder startFolder = new SopFolder { Id = sopfolder.Id };
//db.SopFolders.Attach(startFolder);
// db.SopFolders.Attach(sopfolder);
startFolder.Name = sopfolder.Name;
startFolder.LastUpdated = sopfolder.LastUpdated;
startFolder.SopFields = sopfolder.SopFields;
startFolder.SopFolderChildrens = sopfolder.SopFolderChildrens;
foreach (var child in sopfolder.SopFolderChildrens)
{
db.SopFolders.CurrentValues.SetValues(child);
db.SaveChanges();
}
startFolder.Status = sopfolder.Status;
db.Entry(startFolder).State = EntityState.Modified;
db.SaveChanges();
return Content("true");
}

Retrieve navigation properties's data in a recently added record

I have 3 entities in my case. Invoice, InvoiceDetail and Item.
Invoice has a collection of InvoiceDetail.And each InvoiceDetail has an Item.
Please see the code below:
var ctx = new TestEntities();
var newInvoice = new Invoice
{
CreationDate = DateTime.Now,
UserId = 14
};
newInvoice.InvoiceDetails.Add(new InvoiceDetail
{
ItemId = 345,
ItemCount = 10
});
newInvoice.InvoiceDetails.Add(new InvoiceDetail
{
ItemId = 534,
ItemCount = 10
});
ctx.Invoices.Add(newInvoice);
ctx.SaveChanges();
// workaround
// ctx.Items.ToList();
foreach (var i in newInvoice.InvoiceDetails)
{
// In this line I get NullReferenceException
Console.WriteLine(i.Item.Title);
}
I get NullReferenceException when I want to retrieve each InvoiceDetail's Item data.
The problem is solved when I uncomment, commented part of the code. (ctx.Items.ToList())
UPDATE 1 :
And also this is the Item class:
public partial class Item
{
public Item()
{
this.InvoiceDetails = new HashSet<InvoiceDetail>();
}
public long Id { get; set; }
public string Title { get; set; }
public virtual ICollection<InvoiceDetail> InvoiceDetails { get; set; }
}
UPDATE 2:
public partial class InvoiceDetail
{
public long Id { get; set; }
public long InvoiceId { get; set; }
public long ItemId { get; set; }
public int ItemCount { get; set; }
public virtual Invoice Invoice { get; set; }
public virtual Item Item { get; set; }
}
[NOTE: I am assuming EF5]
The problem could be related to the way you create instances of Invoice and InvoiceDetail. You are newing up instances so they are not EF proxies with all of the necessary components for lazy loading.
I suggest you try using the DbSet.Create() method instead of new
var newInvoice = ctx.Set<Invoice>().Create();
newInvoice.CreationDate = DateTime.Now;
newInvoice.UserId = 14;
var detail1 = ctx.Set<InvoiceDetail>().Create();
detail1.ItemId = 345;
detail1.ItemCount = 10;
newInvoice.InvoiceDetails.Add(detail1);
//...
I can't promise this will fix your problem as EF is such an intricate and varied beast but it is worth giving this a try ...

There has to be a better way to add these using LINQ, right?

I am new to LINQ and and come up with the below to add new information to my DB using LINQ and EF5 but I am sure there is a more efficant, better, way to do this I just don't know it. I was hoping to get some input on what I can do to acceive the same but with less/more efficant code.
using (var db = new FullContext())
{
if (ddlItemType.SelectedValue == "Other")
{
var NewItemType = new ItemType { Name = tbNewType.Text };
db.ItemTypes.Add(NewItemType);
db.SaveChanges();
}
if (ddlRegion.SelectedValue == "Other")
{
var NewRegion = new ReleaseRegion { Name = tbNewRegion.Text };
db.Regions.Add(NewRegion);
db.SaveChanges();
}
var NewItemTypeID = byte.Parse((from i in db.ItemTypes
where i.Name == tbNewType.Text
select new { i.ID }).ToString());
var NewRegionID = byte.Parse((from r in db.Regions
where r.Name == tbNewRegion.Text
select new { r.ID }).ToString());
var NewItem = new Item
{
Name = tbItemName.Text,
TypeID = NewItemTypeID,
RegionID = NewRegionID,
Condition = ddlCondition.SelectedValue.ToString(),
UPC = tbUPC.Text,
ISBN = tbISBN.Text,
IsColleciton = cbIsCollection.Checked,
CollectionID = Convert.ToInt16(ddlCollection.SelectedValue),
Notes = tbNotes.Text
};
db.Items.Add(NewItem);
db.SaveChanges();
}
Item.cs:
using System;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
namespace FFCollection.DAL
{
[Table("Items")]
public class Item
{
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public Int16 ID { get; set; }
[Required]
public string Name { get; set; }
public byte TypeID { get; set; }
[ForeignKey("TypeID")]
public virtual ItemType Type { get; set; }
public byte RegionID { get; set; }
[ForeignKey("RegionID")]
public virtual ReleaseRegion Region { get; set; }
[Required]
public string Condition { get; set; }
public string UPC { get; set; }
public string ISBN { get; set; }
public string Notes { get; set; }
[Required]
public Boolean IsColleciton { get; set; }
public Int16 CollectionID { get; set; }
[ForeignKey("CollectionID")]
public virtual Item InCollectionID { get; set; }
}
}
ItemType.cs:
using System.ComponentModel.DataAnnotations.Schema;
namespace FFCollection.DAL
{
public class ItemType
{
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public byte ID { get; set; }
public string Name { get; set; }
}
}
The databinding to DDL:
using (var db = new FullContext())
{
ddlItemType.DataSource = (from t in db.ItemTypes
select new { t.ID, t.Name }).ToList();
ddlItemType.DataTextField = "Name";
ddlItemType.DataValueField = "ID";
ddlItemType.DataBind();
ddlItemType.Items.Insert(0, new ListItem("Other", "Other"));
}
Part of the trouble isn't Linq, it's how you're using EF. Based on that example code you're using it as a data layer wrapper rather than an ORM. When constructing an object graph you should deal with the objects where you can, not foreign key IDs. The power of an ORM is that you can deal specifically with object graphs that are mapped to data, so that when you tell the ORM to save an object (and it's associated relatives) the ORM takes out all of the work of inserting/updating new records and wiring up keys. You're doing all that extra work in code, where an ORM like EF should allow you to accomplish what you want with a handful of lines.
For a start, when dealing with combo boxes, bind them to a data structure that includes the lookup value's ID that you can resolve instances of existing ItemTypes or Regions to associate with your new Item. (or in the case of selections of "other".
What I'd be looking at would be to bind the combo boxes to ItemType/Regions with the "Other" being a specific place-holder that the code will substitute with a new object if selected based on entries in the text fields. Then rather than saving the new objects before appending to the "Item", you simply set the references and save the Item which should cascade insert operations for the new lookup objects.
After this code executes EF will automatically put an ID into your NewItemType entity. You don't need to go and find it again, you could just say NewItemType.ID. This will only work after you have already called db.SaveChanges().
if (ddlItemType.SelectedValue == "Other")
{
var NewItemType = new ItemType { Name = tbNewType.Text };
db.ItemTypes.Add(NewItemType);
db.SaveChanges();
}

Categories

Resources