I have two classes with an parent-child (1:n) relationship and want to detect when the parent has been changed by adding a child to it.
I have overriden the SaveChanges method of the context:
public override int SaveChanges()
{
var ctx = ((IObjectContextAdapter)this).ObjectContext;
ctx.DetectChanges();
var entries = ctx.ObjectStateManager.GetObjectStateEntries(EntityState.Added | EntityState.Deleted | EntityState.Modified);
foreach (var entry in entries)
{
if (entry.IsRelationship)
{
Console.WriteLine("Relationship");
}
else
{
Console.WriteLine("Entity");
}
}
return base.SaveChanges();
}
As expected, when adding a child to the parent I get an entity entry for the child and a relationship entry for the parent-child relationship. But as soon as the child class contains a key property for the parent, I no longer get any relationship entries for this relationship.
class Class1
{
[Key, DatabaseGenerated(DatabaseGeneratedOption.Identity)] public int Id { get; set; }
public ICollection<Class2> Class2s { get; set; }
}
class Class2
{
[Key, DatabaseGenerated(DatabaseGeneratedOption.Identity)] public int Id { get; set; }
public Class1 Parent { get; set; }
public int ParentId { get; set; } // <-- no relationship entries if present
}
Is this expected behavior? How can I detect relationship changes when the id column is present?
Related
Can someone shed light onto this?
I have three objects. Firstly, a Root object
public class Root
{
[Key]
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public int Id { get; set; }
}
This is the basis for my composite key, used in Parent,Child:
public class Parent
{
[Key, Column(Order = 0)]
public int RootId { get; set; }
public Root Root { get; set; }
[Key, Column(Order = 1)]
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public int Id { get; set; }
public ICollection<Child> Children { get; set; } = new Collection<Child>();
}
public class Child
{
[Key, Column(Order = 0)]
public int RootId { get; set; }
public Root Root { get; set; }
[Key, Column(Order = 1)]
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public int Id { get; set; }
public int ParentId { get; set; }
[ForeignKey("RootId, ParentId")]
public Parent Parent { get; set; }
}
I add a new parent object with multiple children
Parent result = new Parent
{
RootId = rootId
};
foreach (var child in children)
{
result.Children.Add(new Child
{
RootId = rootId
});
}
_dbContext.Parents.Add(result);
_dbContext.Save();
However, I get an exception whenever I have multiple Root objects in my database:
The INSERT statement conflicted with the FOREIGN KEY constraint "FK_dbo.Children_dbo.Parents_RootId_ParentId". The conflict occurred in database "Test", table "dbo.Parents".
When I delete the foreign key constraint in my database, I can see that EF is somehow ignoring the RootId = rootId that I set. It uses a different one instead. The ParentId is correctly set, but obviously this results in a FK that doesn't exist.
I want to commit the parent and its children in one Save call. I've tried adding the children to _dbContext.Children after adding the parent to _dbContext.Parents, I've tried first adding the Children, to no avail.
Can someone shed light on this? I'm running out of ideas on what to check to understand what I'm doing wrong..
Edit: I'm using Entity Framework 6.1.3.
I am attempting to implement auditing in my MVC 4 with EF application as per this post http://jmdority.wordpress.com/2011/07/20/using-entity-framework-4-1-dbcontext-change-tracking-for-audit-logging/.
I have a class that inherits from DbContext and overrides the SaveChanges method. In this new SaveChanges method I am inserting a record into an Audit table.
An entity that can be audited:
public class Fruit
{
[Key, DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public long FruitId{ get; set; }
public string FruitName{ get; set; }
}
And the model of the FruitAudit table:
public class FruitAudit
{
[Key, DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public long FruitAuditId{ get; set; }
public long FruitId{ get; set; }
// Auditing properties like modifying user and timestamps.
}
And my overriden SaveChanges method:
public int SaveChanges()
{
var entriesToAudit = this.ChangeTracker.Entries().Where(p =>
(p.State == System.Data.EntityState.Added || p.State == System.Data.EntityState.Deleted || p.State == System.Data.EntityState.Modified);
foreach (var entity in entriesToAudit)
{
using (var context = new StoreContext())
{
Fruit fruit = entity.Entity as Fruit;
FruitAudit audit = new FruitAudit()
{
FruitId = Fruit.FruitId;
// Other auditing properties set here
}
context.FruitAudits.Add(audit);
context.SaveChanges();
}
}
return base.SaveChanges();
}
An error occurs when a Fruit is inserted because the FruitId is not yet set when trying to save the FruitAudit record. The FruitId is auto-generated from the database but the actual Fruit object isn't saved until AFTER the audit record is inserted. How can I associated this FruitAudit with the Fruit being inserted?
I would suggest using a virtual navigation property to FruitAudit so that you have something like:
public class FruitAudit
{
public int FruitAuditId { get; set; }
public int FruitId { get; set; }
public virtual Fruit Fruit { get; set; }
}
And your Fruit looks like:
public class Fruit
{
public int FruitId { get; set; }
public string FruitName { get; set; }
}
Then, when you go to SaveChanges() you just say:
Fruit fruit = entity.Entity as Fruit;
FruitAudit audit = new FruitAudit()
{
Fruit = fruit;
// Other auditing properties set here
}
...
context.Fruits.Add(fruit);
context.FruitAudits.Add(audit);
I ended up making a copy of all the entity changes and then calling base.SaveChanges(). That will insert all the Fruits. Then I go through the copy of entity changes and insert audit records for each.
Getting the following error upon save of my context:
The operation failed: The relationship could not be changed because one or more of the foreign-key properties is non-nullable. When a change is made to a relationship, the related foreign-key property is set to a null value. If the foreign-key does not support null values, a new relationship must be defined, the foreign-key property must be assigned another non-null value, or the unrelated object must be deleted.
The controller code:
#region CreateStory
[HttpPost]
[Authorize]
public ActionResult CreateStory(Userstory story)
{
if (ModelState.IsValid)
{
string _Username = User.Identity.Name.ToLower();
using (var _Db = new NimbleContext())
{
Project _Project = _Db.Projects.First(p => p.Owner.ToLower() == _Username);
if (_Project != null)
{
ProjectGroup _Group = _Db.Groups.First(g => g.GroupID == story.GroupID);
if (_Group != null)
{
_Db.Userstories.Add(story);
_Db.SaveChanges(); <--- Error Occurs here!
return Json(new { Success = true, GroupID = _Group.GroupID });
}
}
}
}
return Json(new { Success = false });
}
#endregion
The Model structure:
Project 1 -> * Groups 1 -> * Userstories
I can prevent the error from occurring by removing the following declarations:
Project _Project = _Db.Projects.First(p => p.Owner.ToLower() == _Username);
ProjectGroup _Group = _Db.Groups.First(g => g.GroupID == story.GroupID);
However I need to make these checks to ensure that the user belongs to the project etc. I really don't see what is causing the error, none of those statements should have an impact on the data to be saved right?
Its all in your model design. If you are explicitly declaring your Foreign Key properties, do them like this:
public class Project {
[Key]
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public int ProjectId { get; set; }
public virtual ICollection<Group> Groups { get; set; }
}
public class Group {
[Key]
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public int GroupId { get; set; }
[ForeignKey("Project"), DatabaseGenerated(DatabaseGeneratedOption.None)]
public int? ProjectId { get; set; }
public virtual Project Project { get; set; }
public virtual ICollection<Userstory> Userstories{ get; set; }
}
public class Userstory {
[Key]
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public int UserstoryId{ get; set; }
[ForeignKey("Group"), DatabaseGenerated(DatabaseGeneratedOption.None)]
public int? GroupId { get; set; }
public virtual Group Group { get; set; }
}
Note: This should work if you are following Entity Framework Code First, not sure about other methodologies like Model First and Database First
Is there any way to save a child object in a parents child object collection and also setting it to a navigation property on the parent without round tripping to the database? example below doesn't work
public class Parent
{
int Id { get; set; }
int? ChildId { get; set; }
Child Child { get; set; }
public virtual ICollection<Child> Children { get; set; }
}
public class Child
{
int Id { get; set; }
public Parent Parent { get; set; }
}
....
var p = new Parent();
var c = new Child();
p.Child = c;
p.Children.Add(c);
Context.Set<Parent>().Add(p);
Context.SaveChanges();
EDIT
The example above throws this error when 'savechanges()' is called.
Unable to determine a valid ordering for dependent operations. Dependencies may exist due to foreign key constraints, model requirements, or store-generated values.
I am using MVC 3, EF 4.1, and dbContext. I need to know how to delete an entity in one-to-many relation with a non-nullable foreign-key.
When I Remove the child entity and execute SaveChanges I get the error:
The operation failed: The relationship could not be changed because one or more of the foreign-key properties is non-nullable. When a change is made to a relationship, the related foreign-key property is set to a null value. If the foreign-key does not support null values, a new relationship must be defined, the foreign-key property must be assigned another non-null value, or the unrelated object must be deleted.
From other posts, I understand that using Remove(entity) marks the entity for delete. During SaveChanges, EF sets the the foreign-key to Null and the above error occurs.
I have found some posts that use DeleteObject on the child entity rather than Remove; however, the DeleteObject approach seems to have been dropped because of addition to dbContext and DbSet.
I have found posts that suggest modifying the EDMX foreign-key relation to be Nullable. Modifying the EDMX is fine, but whenever an Update Model for Database is done, these changes get nuked and must be reapplied. Not optimal.
Another post suggested creating a proxy entity with the foreign-key relations set to Nullable but I do not understand that approach. It seems to suffer from the same issue as modifying the EDMX in that the context gets automatically updated when changes to the EDMX are saved.
My simplified model is:
public partial class User
{
public User()
{
this.UserContacts = new HashSet<UserContact>();
}
public long userId { get; set; }
public string userEmail { get; set; }
public string userPassword { get; set; }
public string userFirstName { get; set; }
public string userLastName { get; set; }
. . .
public virtual Country Country { get; set; }
public virtual State State { get; set; }
public virtual ICollection<UserContact> UserContacts { get; set; }
}
}
public partial class UserContact
{
public long userContactId { get; set; }
public long userContactUserId { get; set; }
public long userContactTypeId { get; set; }
public string userContactData { get; set; }
public virtual ContactType ContactType { get; set; }
public virtual User User { get; set; }
}
The userContactUserId and userContactTypeId are required foreign-keys.
In the dbContext container both Users and UserContact are DbSet.
I have a ViewModel for the User and a ViewModel for UserContact as follows
public class UserContactViewModel
{
[HiddenInput]
public long UserContactId { get; set; }
[HiddenInput]
public long UserContactUserId { get; set; }
[Display(Name = "Contact")]
[Required]
public string ContactData { get; set; }
[Required]
public long ContactType { get; set; }
[HiddenInput]
public bool isDeleted { get; set; }
}
public class MyProfileViewModel
{
[HiddenInput]
public long UserId { get; set; }
[Required]
[Display(Name = "First Name")]
[StringLength(100)]
public string FirstName { get; set; }
[Required]
[StringLength(100)]
[Display(Name = "Last Name")]
public string LastName { get; set; }
....
public IEnumerable<UserContactViewModel> Contacts { get; set; }
}
When saving changes to the user profile, I loop over the list of UserContactViewModel entities to determine which have been added, modified, or deleted.
foreach (var c in model.Contacts)
{
UserContact uc = usr.UserContacts.Single(con => con.userContactId == c.UserContactId);
if (uc != null)
{
if (c.isDeleted == true) // Deleted UserContact
{
ctx.UserContacts.Remove(uc); // Remove doesn't work
}
else // Modified UserContact
{
uc.userContactData = c.ContactData;
uc.userContactTypeId = c.ContactType;
ctx.Entry(uc).State = EntityState.Modified;
}
}
else // New UserContact
{
usr.UserContacts.Add(new UserContact { userContactUserId = model.UserId, userContactData = c.ContactData, userContactTypeId = c.ContactType });
}
}
I'd appreciate any help.
I managed to solve the problem as follows:
First, I was able to fetch the ObjectContext by casting my DbContext (eg "ctx") to an IObjectContextAdapter and then obtaining reference to the ObjectContext.
Next, I simply called the DeleteObject method passing the UserContact record to be deleted.
When SaveChanges gets the deletes in the database happen as expected.
if (c.isDeleted == true) // Deleted UserContact
{
ObjectContext oc = ((IObjectContextAdapter)ctx).ObjectContext;
oc.DeleteObject(uc)
}
Here is a snippet of the relevant code:
foreach (var c in model.Contacts)
{
UserContact uc = null;
if (c.UserContactId != 0)
{
uc = ctx.UserContacts.Find(c.UserContactId);
}
if (uc != null)
{
if (c.isDeleted == true) // Deleted UserContact
{
ObjectContext oc = ((IObjectContextAdapter)ctx).ObjectContext;
oc.DeleteObject(uc);
}
else // Modified UserContact
{
uc.userContactData = c.ContactData;
uc.userContactTypeId = c.ContactType;
ctx.Entry(uc).State = EntityState.Modified;
}
}
else // New UserContact
{
usr.UserContacts.Add(new UserContact { userContactData = c.ContactData, userContactTypeId = c.ContactType });
}
}
ctx.Entry(usr).State = EntityState.Modified;
ctx.SaveChanges();
Hope this helps someone in future.
I solved the same problem following the section "Cascade Delete Rules on Relationships" at the MSDN guide page here http://msdn.microsoft.com/en-us/library/bb738695.aspx
Hope tu be helpfull :D
well, implement your own ICollection and mark those child objects for deletion along with removing it. And then, in your own SaveChanges method override, delete those objects.
You can configure the relation to cascade ... this will propagate the delete to dependent entities.
But it's very dangerous :)
I prefer setting a flag in the row that prevents the data tier from including it in future queries, most applications do not need physical delete (and will be a chance to undo).