I am developing a solution using EF 5 in VS 2012 and I am puzzled about the correct way to specify entity relationships when adding and updating an entity.
These are my main classes where notifier is a person:
public class Notifier : Person
{
public bool IsValid { get; set; }
public int NotifierTypeID { get; set; }
public virtual NotifierType NotifierType { get; set; }
public int MyCaseID { get; set; }
public virtual MyCase MyCase { get; set; }
}
public abstract class Person
{
public int PersonID { get; set; }
public String Name { get; set; }
}
Notifiers belong to cases
public class MyCase
{
public int MyCaseID { get; set; }
public DateTime DateOfNotification { get; set; }
public virtual ICollection<Notifier> Notifiers { get; set; }
}
and have a type:
public class NotifierType
{
public int NotifierTypeID { get; set; }
public string NotifierTypeName { get; set; }
}
I am exposing the foreign keys between notifiers and cases and notifier types.
The method I use to add/update a notifier is:
using (MyContext dbContext = new MyContext(connectionString))
{
notifier.MyCaseID = MyCaseID;
notifier.NotifierTypeID = notifierView.NotifierTypeID;
// **** the puzzling line ****
notifier.NotifierType = dbContext.NotifierTypes.Find(notifierView.NotifierTypeID);
//dbContext.Database.Log = s => System.Diagnostics.Debug.Write(s);
dbContext.Entry(notifier).State = notifier.PersonID == 0 ? EntityState.Added : EntityState.Modified;
dbContext.SaveChanges();
// save the ID in case it's new
notifierViewReturn.PersonID = notifier.PersonID;
}
I am puzzled by the line after the comment **** the puzzling line **** above. I am specifying the foreign keys explicitly and don't need the this line if I am adding a notifier but I do need it if I am updating the object, otherwise it throws an exception.
The exception is
Message=A referential integrity constraint violation occurred:
The property values that define the referential constraints are not
consistent between principal and dependent objects in the relationship.
Can anyone please explain why this line is needed at all. Thanks
When you update an existing entity, before you make changes the entity already has NotifierType (navigation property) and NotifierTypeID populated. If you then change NotifierTypeID but don't update NotifierType, Entity Framework detects a potential inconsistency (NotifierTypeID != NotifierType.NotifierTypeID) and throws the exception you are getting. This is why you need to set both when updating. When adding, you don't have this issue because only one of the IDs is defined (NotifierTypeID, but not NotifierType.NotifierTypeID), so it just uses that one.
If you want to avoid going to retrieve the notifier type for updates, you should be able to just set it to null instead, and in that case there will be no discrepancy and it can just use the NotifierTypeID that you set:
notifier.MyCaseID = MyCaseID;
notifier.NotifierType = null;
notifier.NotifierTypeID = notifierView.NotifierTypeID;
Hope that helps!
Related
I'm using Entity Framework 6 and I keep getting the following error:
Violation of PRIMARY KEY constraint 'PK_AssignmentType'. Cannot insert duplicate key in object 'dbo.AssignmentType'.
As you see I tried to change the state of the entity AssignmentType to unchanged. When I assign the entity Assignment the foreign key of AssignmentType and then set the navigation property AssignmentType to null, I still get the same error (in the foreach loop of Assignments, not in the Comments foreach loop).
Where does Entity Framework keeping track of the entity AssignmentType and why does it still think it's a new entity and not an existing one?
There is a many-to-many relationship between the entity Reporting and Assignment.
[Route("")]
[HttpPost]
public IHttpActionResult Add(ReportingDTO data)
{
Reporting reporting = new Reporting { ID = Guid.NewGuid(), Date = DateTime.Now };
foreach (Assignment assignment in data.Assignments)
{
_db.Entry(assignment.AssignmentType).State = EntityState.Unchanged;
if (assignment.Reporting.Count > 0)
{
_db.Entry(assignment).State = EntityState.Added;
_db.Entry(assignment).State = EntityState.Modified;
}
reporting.Assignment.Add(assignment);
}
foreach (Comment comment in data.Comments)
{
comment.ID = Guid.NewGuid();
comment.AssignmentID = comment.Assignment.ID;
comment.Assignment = null;
comment.ReportingID= reporting.ID;
}
using(var transaction = _db.Database.BeginTransaction())
{
_db.Reporting.Add(reporting);
_db.Comments.AddRange(data.Comments);
_db.SaveChanges();
transaction.Commit();
}
return Ok(reporting);
}
Reporting.cs
[Table("Reporting")]
public partial class Reporting
{
[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Usage", "CA2214:DoNotCallOverridableMethodsInConstructors")]
public Reporting()
{
Assignment= new HashSet<Assignment>();
}
[Key]
public Guid ID{ get; set; }
[Column(TypeName = "date")]
public DateTime Date{ get; set; }
[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Usage", "CA2227:CollectionPropertiesShouldBeReadOnly")]
public virtual ICollection<Assignment> Assignment{ get; set; }
}
Assignment.cs
[Table("Assignment")]
public partial class Assignment
{
[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Usage", "CA2214:DoNotCallOverridableMethodsInConstructors")]
public Assignment()
{
Reporting = new HashSet<Reporting>();
}
[Key]
public Guid OID { get; set; }
[Required]
[StringLength(100)]
public string Name { get; set; }
public Guid AssignmentTypeID { get; set; }
[Required]
[StringLength(100)]
public string Project { get; set; }
public bool Completed { get; set; }
[ForeignKey("AssignmentTypeID")]
public virtual AssignmentType AssignmentType { get; set; }
[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Usage", "CA2227:CollectionPropertiesShouldBeReadOnly")]
public virtual ICollection<Reporting> Reporting { get; set; }
}
AssignmentType
[Table("AssignmentType")]
public partial class AssignmentType
{
[Key]
public Guid ID { get; set; }
[Required]
[StringLength(100)]
public string Name { get; set; }
}
Try something like this:
public IHttpActionResult Add(ReportingDTO data)
{
Reporting reporting = new Reporting { ID = Guid.NewGuid(), Date = DateTime.Now };
foreach (Assignment assignment in data.Assignments)
{
if (assignment.ID != null)
{
_db.Assignments.Attach(assignment);
_db.Entry(assignment).State = EntityState.Modified;
}
else
{
assignment.ID = Guid.NewGuid();
}
if (assignment.AssignmentType != null)
{
assignment.AssignmentTypeID = assignment.AssignmentType.ID;
assignment.AssignmentType = null;
}
}
reporting.Assignment.AddRange(data.Assignments);
foreach (Comment comment in data.Comments)
{
comment.ID = Guid.NewGuid();
comment.AssignmentID = comment.Assignment.ID;
comment.Assignment = null;
comment.ReportingID = reporting.ID;
}
_db.Reporting.Add(reporting);
_db.Comments.AddRange(data.Comments);
_db.SaveChanges();
return Ok(reporting);
}
All of your Entry() operations are useless in your context, except if you are updating an entity. In that case, you must Attach() the updated entity before updating its state.
Plus, to answer your initial question: EF adds a new AssignmentType because it has lost tracks of the existing one. Your Controller is constructed every time a request hit it, so your DbContext is also constructed every time. When you create a new DbContext, it has no knowledge of previous operations, like returning an Assignment or whatever (it's also the case if you specify AsNoTracking() during your query). In other words, if the entity is not tracked by the DbContext, then EF will try to add it.
To prevent that, you can (and really should) use the foreign keys instead of the navigation properties. Or you can Attach() every known entity (but it's useless if you have foreign keys). For ManyToMany relationships, you have to query the database before adding/updating a new entity, otherwise, you will have doubloons.
Or, you can also use what I call "link tables". It's just a class that's between your two primary classes. EF already does it for all your ManyToMany relationships (check your database). Basicaly, it goes something like :
public class A
{
public int Id {get; set;}
public virtual List<AB> ABs { get; set; }
}
public class B
{
public int Id {get; set;}
public virtual List<AB> BAs {get; set; }
}
public class AB
{
public int AId {get;set;}
public virtual A {get; set;}
public int BId {get;set;}
public virtual B {get; set;}
}
With that way, you can just add a new AB (or delete one) with _db.ABs.Add(new AB{AId = 1, BId = 2}) without taking care of the rest.
I apologize on the title as I'm not sure how to summarize the issue we are having in one sentence.
We have two solutions, one of our old code base, another with our latest and greatest, with both solutions sharing a project of code first EF models. The old solution also has another project of older EF models and its own database context. The old code worked with this blend of old and new until I added new classes that inherited an EF one. In our new system, CRUD operations work as expected with the sub classes, but on our old system we get this error:
The entity types 'DiscountActivity' and 'DiscountSeries' cannot share table 'Discounts' because they are not in the same type hierarchy or do not have a valid one to one foreign key relationship with matching primary keys between them.
Here are the classes in question:
[Table("Discounts")]
public class Discount
{
[Column("Id"), Key, DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public int Id { get; set; }
[Column("Type")]
public string Type { get; set; }
[Column("Amount")]
public double Amount { get; set; }
}
public class DiscountActivity : Discount
{
[Column("TypeId")]
public int ActivityId { get; set; }
public virtual Activity Activity { get; set; }
public DiscountActivity()
{
Type = "Activity";
}
}
public class DiscountSeries : Discount
{
[Column("TypeId")]
public int SeriesId { get; set; }
public virtual Series Series { get; set; }
public DiscountSeries()
{
Type = "Series";
}
}
[Table("Activities")]
public class Activity
{
[Column("Id"), Key, DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public int Id { get; set; }
[Column("Name")]
public string Name { get; set; }
public virtual ICollection<DiscountActivity> Discounts { get; set; }
}
[Table("Series")]
public class Series
{
[Column("Id"), Key, DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public int Id { get; set; }
[Column("Name")]
public string Name { get; set; }
public virtual ICollection<DiscountSeries> Discounts { get; set; }
}
If I comment out both ICollection lines in Activity and Series, the error no longer occurs. If I leave one of the two, does not matter which, the error still does not occur. Also, it does not seem to matter if the new or old EF database context is used for the LINQ statements when the ICollection objects are both present, the same error is thrown. So to be clear, when running a LINQ statement on the old database context, we get the above error even though models and collections are in a separate database context.
Here is an example in our old API using the old context:
public class ContractsController : ApiController
{
private readonly DatabaseContext _db = new DatabaseContext();
[ResponseType(typeof(Contract))]
public IHttpActionResult Get(int id)
{
var model = _db.Contracts.Find(id);
if (model == null)
{
return NotFound();
}
return Ok(model);
}
}
Any input is appreciated!
I use Entity Framework 6 (Code First). I have a class:
public class DialogSession {...}
And another class with a list of DialogSession objects:
public class DialogUser
{
public int Id { get; set; }
public List<DialogSession> DialogSessions { get; set; }
}
I add DialogSession object to the list and then execute context.SaveChanges() as follows:
dialogUser.DialogSessions.Add(dialogSession);
context.SaveChanges();
But the foreign key of the dialogSession record still Null:
I've tried using many methods on the web like the follows but withoyt success:
context.DialogUsers.Attach(dialogUser);
context.Entry(dialogUser).State = EntityState.Modified;
context.SaveChangesExtention();
Does anyone know how to save inner objects (like the list) in the database using Entity Framework (6)?
From your question is not clear which relationship type do you have, so I guess you have One-to-Many and something like this should works:
public class DialogSession
{
public int DialogSessionId { get; set; }
public virtual DialogUser DialogUser { get; set; }
}
public class DialogUser
{
public int DialogUserId { get; set; }
public virtual ICollection<DialogSession> DialogSessions { get; set; }
}
Take a look at example how properly configure this type of relationship in this article.
If I am not wrong you should add
dialogUser.DialogSessions.Add(dialogSession);
context.Entry(dialogUser).State = EntityState.Modified;
context.SaveChanges();
This will mark the entity as modified and then the changes should be reflected on the db.
This could be done a more efficiently by marking singular properties as modified
dialogUser.DialogSessions.Add(dialogSession);
context.Entry(dialogUser).Property(u => u.dialogSession).IsModified = true;
context.SaveChanges();
Give it a try :)
Please see: https://msdn.microsoft.com/en-us/library/jj591583(v=vs.113).aspx
You should use a virtual list for the child entity, and ensure that the DialogSessions class also refers back to its parent with a DialogUserId property (so named by convention)
The below works for me.
using System.ComponentModel.DataAnnotations;
public class DialogSession
{
[Key]
public int DialogSessionId { get; set; }
public int DialogUser { get; set; }
}
public class DialogUser
{
[Key]
public int DialogUserId { get; set; }
public virtual ICollection<DialogSession> DialogSessions { get; set; }
}
I'm using ASP.NET 5 with Entity Framework 7.
I have this models:
public class ParentModel
{
public Guid Id { get; set; }
public virtual ChildModel Children { get; set; }
}
public class ChildModel
{
public Guid Id { get; set; }
public virtual AnotherChildModel AnotherChild { get; set; }
}
public class AnotherChildModel
{
public Guid Id { get; set; }
public string Text { get; set; }
}
When I'm trying to add ParentModel to database, it doesn't automatically add ChildModel and AnotherChildModel to database, while ParentModel completely correct in code, for example:
var parent = new ParentModel() { Children = new ChildModel() { AnotherChild = new AnotherChildModel() { Text = "sometext" }}};
So, simple parentSet.Add(parent) doesn't work, is there another way, except for manually adding all models in sets?
EDIT:
Exception I have:
DbUpdateException: An error occurred while updating the entries. See the inner exception for details.
SqlException: The INSERT statement conflicted with the FOREIGN KEY constraint "FK_ParentModel_ChildModel_ChildrenId". The conflict occurred in database "aspnet5-WebApplication1-922849d0-b7da-4169-8150-9a2d05240a47", table "dbo.ChildModel", column 'Id'. The statement has been terminated.
In the current RC1 version of EF7, Add() only recursively adds adhering objects in collections (i.e. true children), not referenced entities, as EF6 did.
So if you'd have the following model (that's more consistent with the names you chose) ...
public class ParentModel
{
public Guid Id { get; set; }
public virtual ICollection<ChildModel> Children { get; set; }
}
... the children would also be added by the single statement parentSet.Add(parent).
I don't know if this is intended behavior. The RC has already proven to come with issues a "release candidate" shouldn't have. But maybe it's an OO-inspired design decision that parents encapsulate their children and not the reverse.
I am apparently having a real devil of a time understanding Entity Framework 6 which I am using with ASP.NET MVC 5.
The core of the matter is that I have a really quite simple data model that is typical of any real world situation where I have various business objects that have other business objects as properties (and of course they child objects may in turn have other child business objects) and also various types of lookup/type data (Country, State/Province, LanguageType, StatusType etc.) and I cannot figure out how to save/update it properly.
I keep going back and forth between two error states:
1) I either run into the situation where saving a parent business object results in unwanted duplicate values being inserted into my lookup/type tables (for example saving a business object that has been assigned an existing LanguageType of 'English' will result in another LanguageType for 'English' being inserted into the LanguageType table), or
2) I use some of the suggestions I've seen here and elsewhere on the net (e.g. Saving Entity causes duplicate insert into lookup data, Prevent Entity Framework to Insert Values for Navigational Properties ) to solve issue 1 and then find myself fighting against this same issue: An object with the same key already exists in the ObjectStateManager. The ObjectStateManager cannot track multiple objects with the same key .
I will now provide a few code snippets to help build the picture of what I am trying to do and what I am using to do it. First, an example of the entities involved:
public class Customer : BaseEntity
{
public string Name { get; set; }
[LocalizedDisplayName("Contacts")]
public virtual List Contacts { get; set; }
}
public class Contact : BaseEntity
{
[Required]
public string FirstName { get; set; }
[Required]
public string LastName { get; set; }
public int? LanguageTypeID { get; set; }
[Required]
[ForeignKey("LanguageTypeID")]
public virtual LanguageType Language { get; set; }
}
public class LanguageType : Lookup
{
[LocalizedDisplayName("CultureName")]
public string CultureName { get; set; }
}
public class Lookup : BaseEntity
{
public string DisplayName { get; set; }
public int DisplayOrder { get; set; }
public string Name { get; set; }
public string Description { get; set; }
}
public class BaseEntity
{
public int ID { get; set; }
public DateTime? CreatedOn { get; set; }
public DateTime? UpdatedOn { get; set; }
public DateTime? DeletedOn { get; set; }
public bool Deleted { get; set; }
public bool Active { get; set; }
public ApplicationUser CreatedByUser { get; set; }
public ApplicationUser UpdatedByUser { get; set; }
}
In my controller, I have some code like the following:
foreach(Contact contact in lstContacts)
{
customer.Contacts.Add(contact);
}
if (ModelState.IsValid)
{
repository.Add(customer);
}
Let us suppose that each of the contacts has the same LanguageType of 'English' assigned (and in this example it is the fact that I am trying to save multiple contacts that have the same LanguageType that triggers the ObjectStateManager error). Initially, the repository.Add() code just did a context.SaveChanges() which did not work as expected, so now it looks something like this (Entity variable is a Customer):
try
{
if(Entity.Contacts != null)
{
foreach(Contact contact in Entity.Contacts)
{
var entry = this.context.Entry(contact.Language);
var key = contact.Language.ID;
if (entry.State == EntityState.Detached)
{
var currentEntry = this.context.LanguageTypes.Local.SingleOrDefault(l => l.ID == key);
if (currentEntry != null)
{
var attachedEntry = this.context.Entry(currentEntry);
//attachedEntry.CurrentValues.SetValues(entityToUpdate);
attachedEntry.State = EntityState.Unchanged;
}
else
{
this.context.LanguageTypes.Attach(contact.Language);
entry.State = EntityState.Unchanged;
}
}
}
}
context.Customers.Add(Entity);
context.SaveChanges();
}
catch(Exception ex)
{
throw;
}
Is it fundamentally wrong to expect this to have worked? How am I supposed to save and example like this? I have similar problems saving similar object graphs. When I look at tutorials and examples for EF, they are all simple and they all just call SaveChanges() after doing something very similar to what I am doing here.
I've just recently been using the ORM capabilities of ColdFusion (which is hibernate under the covers) and there are would simply load the LanguageType entity, assign it to the Contact entity, save the Contact entity, assign it to the Customer and then save the Customer.
In my mind, this is the most basic of situations and I cannot believe that it has caused me so much pain - I hate to say it, but using plain old ADO.NET (or heaven forbid, ColdFusion which I really don't enjoy) would have been MUCH simpler. So I am missing SOMETHING. I apparently have a key flaw in my understanding/approach to EF and If somebody could help me to make this work as expected and help me to figure out just where my misunderstanding lies, I would greatly appreciate it. I have spend too many hours and hours on this and it is a waste of time - I have/will have countless examples just like this one in the code I am building so I need to adjust my thinking with respect to EF right now so I can be productive and do approach things in the expected way.
Your help will mean so much and I thank you for it!
Let's consider the following object graph in which a teacher instance is the root object,
Teacher --[has many]--> courses
Teacher --[Has One]--> Department
In entity framework's DbContext, each instance of an object has a State indicating whether the object is Added, Modified, Removed or Unchanged. What happens apparently is the following :
Creating the root object for the first time
In this case, in addition to the newly created root object Teacher, ALL the child objects in the graph will have the State Added as well even if they're already created. The solution for this problem is to include the foreign key property for each child element and use it instead, i.e. Teacher.DepartmentId = 3 for example.
Updating the root object and one of its child elements' properties
Suppose you fetch a teacher object from the db, and you change the Teacher.Name property as well as the Teacher.Department.Name property; in this case, only the teacher root object will have the State marked as Modified, the department's State on the other hand remains Unchanged and the modification won't be persisted into DB; Silently without any warning.
EDIT 1
I used your classes as follows and I don't have a problem with persisting the objects :
public class Customer : BaseEntity
{
public string Name { get; set; }
public virtual List<Contact> Contacts { get; set; }
}
public class Contact : BaseEntity
{
public string FirstName { get; set; }
public string LastName { get; set; }
public int? LanguageTypeID { get; set; }
public Customer Customer { get; set; }
[ForeignKey("LanguageTypeID")]
public LanguageType Language { get; set; }
}
public class LanguageType : Lookup
{
public string CultureName { get; set; }
}
public class Lookup : BaseEntity
{
public string DisplayName { get; set; }
public int DisplayOrder { get; set; }
public string Name { get; set; }
public string Description { get; set; }
}
public class BaseEntity
{
public int ID { get; set; }
public DateTime? CreatedOn { get; set; }
public DateTime? UpdatedOn { get; set; }
public DateTime? DeletedOn { get; set; }
public bool Deleted { get; set; }
public bool Active { get; set; }
public ApplicationUser CreatedByUser { get; set; }
public ApplicationUser UpdatedByUser { get; set; }
}
public class ApplicationUser
{
public int ID { get; set; }
public string UserName { get; set; }
public string Password { get; set; }
}
And used the following Context :
public class Context : DbContext
{
public Context() : base("name=CS") { }
public DbSet<Customer> Customers { get; set; }
public DbSet<Contact> Contacts { get; set; }
public DbSet<LanguageType> LanguageTypes { get; set; }
public DbSet<ApplicationUser> ApplicationUsers { get; set; }
protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
//I'm generating the database using those entities you defined;
//Here we're demanding not add 's' to the end of table names
modelBuilder.Conventions.Remove<PluralizingTableNameConvention>();
}
}
Then I created a unit tests class with the following :
[TestMethod]
public void TestMethod1()
{
//our context
var ctx = new Infrastructure.EF.Context();
//our language types
var languageType1 = new LanguageType { ID = 1, Name = "French" };
var languageType2 = new LanguageType { ID = 2, Name = "English" };
ctx.LanguageTypes.AddRange(new LanguageType[] { languageType1, languageType2 });
//persist our language types into db before we continue.
ctx.SaveChanges();
//now we're about to start a new unit of work
var customer = new Customer
{
ID = 1,
Name = "C1",
Contacts = new List<Contact>() //To avoid null exception
};
//notice that we're assigning the id of the language type and not
//an object.
var Contacts = new List<Contact>(new Contact[] {
new Contact{ID=1, Customer = customer, LanguageTypeID=1},
new Contact{ID=2, Customer = customer, LanguageTypeID=2}
});
customer.Contacts.AddRange(Contacts);
//adding the customer here will mark the whole object graph as 'Added'
ctx.Customers.Add(customer);
//The customer & contacts are persisted, and in the DB, the language
//types are not redundant.
ctx.SaveChanges();
}
It all worked smoothly without any problems.
As far as i know there is no build in support for reattaching modified graphs (like the SaveOrUpdate method of nHibernate). Perhaps this or this can help you.