I have two entities
public class Student {
public int Id { get; set; }
public string Name { get; set; }
public StudentAddress Address { get; set; }
}
and
public class StudentAddress {
public int StudentAddressId { get; set; }
public string Address { get; set; }
public string City { get; set; }
public string State { get; set; }
public string Country { get; set; }
public int StudentId { get; set; }
public Student Student { get; set; }
}
basically one-to-one relationship
I've got such code
var students = await context.Students.Include(s => s.Address).ToListAsync();
var student1 = students[0];
var student2 = students[1];
var st1Addr = student1.Address;
var st2Addr = student2.Address;
student1.Address = st2Addr;
student2.Address = st1Addr;
await context.SaveChangesAsync();
When EF saves changes one of the student address records gets deleted from DB.
I need to change references to student addresses. How can I achieve this with EF core?
Because it's a 1-1 relationship, as soon as you set
student1.Address = st2Addr;
student1's old address must be deleted, as it would violate the unique constraint on SudentAddress.StudentId. And it's not just a limitation in the change tracker.
SaveChanges will not be able to run
UPDATE StudentAddress set StudentId = #StudentId where StudentAddressID = #Id
without first deleting the other row, as it would violate the unique constraint. And constraints are generally enforced for each DML statement, not deferred until the transaction is committed. You could do this in SQL with a single update statement, eg
update SudentAddress set StudentID = case when studentId = 1 then 2 else 1 end
where StudentId in (1,2)
If you explicitly set the deleted address back to modified
db.Entry(st1Addr).State = EntityState.Modified;
You'll see SaveChanges fail because it can't come up with a sequence of changes that leaves the database in the desired state.
System.InvalidOperationException: 'Unable to save changes because a
circular dependency was detected in the data to be saved
So just modifying the SudentAddress.StudentId directly won't help here.
I think the only way do do this through the change tracker is to make copies of the StudentAddress entities. Then both old ones will be deleted and the new ones inserted.
Related
I have two entities with relation one-many in DB and corresponding classes generated by reverse engineering the database.
public partial class Assets
{
public int AssetsId { get; set; }
public string Description { get; set; }
public int? PersonId { get; set; }
public virtual Persons Person { get; set; }
}
public partial class Persons
{
public Persons()
{
Assets = new HashSet<Assets>();
}
public int PersonId { get; set; }
public string LastName { get; set; }
public string FirstName { get; set; }
public virtual ICollection<Assets> Assets { get; set; }
}
When I assign one entity to the other do the corresponding ID's are set automatically (i am curious if they are set before SaveChanges is called, so i could use it without committing all current changes).
static void Main(string[] args)
{
Context c = new Context();
var a = new Assets();
a.AssetsId = 1;
var p = new Persons();
p.PersonId = 2;
c.Add(a);
c.Add(p);
a.Person = p; //does it set a.PersonId = 2
p.Assets.Add(a); //does it set a.PersonId = 2
c.SaveChanges();
}
(You can just check the answer to your question in the debugger or with a simple test using your provided code.)
When I assign one entity to the other do the corresponding ID's are set automatically [before SaveChanges is called later?]
No, they are not. The reason is, that your navigation properties here are really just POCO properties (meaning simple .NET properties). The Assets.Persons property is really just a property of type Persons and the Persons.Assets property contains really just the HashSet<Assets> instance that you assigned in the constructor. There is no magic here.
The IDs are being synchronized automatically during change tracking, which happens for example when SaveChanges() is called, but can also manually be triggered by calling context.ChangeTracker.DetectChanges().
As a side note, be aware that you should not set IDs in your code explicitly, if you are using identity/auto increment columns for primary keys, because this can lead to exceptions when saving (depending on the database server being used), due to EF Core trying to insert the explicitly assigned ID to the identity/auto increment column (e.g. this is not allowed by default for SQL Server).
I have a solution which uses Entity Framework to insert invoices to a database table. These invoices reference an order, which in turn also references an order item collection.
In this instance I am trying to add an order to the database, however the code is inside a new DbContext and so I need to attach the order and order items to the context, as these already exist in the database and shouldn't be re-added.
I've cut down the model properties for the sake of demonstration:
public class Invoice {
[Key, DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public int InvoiceId { get; set; }
public string OrderNumber { get; set; }
...
public virtual List<InvoiceLineItem> LineItems { get; set; }
}
public class InvoiceLineItem {
[Key]
public int Id { get; set; }
...
public ShopifyOrderItem { get; set; }
}
public class ShopifyOrder {
[Key, DatabaseGenerated(DatabaseGeneratedOption.None)]
public long Id { get; set; }
public int OrderNumber { get; set; }
...
public OrderInvoiceStatus InvoiceStatus { get; set; }
public virtual List<ShopifyOrderItem> OrderItems { get; set; }
}
public class ShopifyOrderItem {
[Key, DatabaseGenerated(DatabaseGeneratedOption.None)]
public long Id { get; set; }
...
[Required]
public virtual ShopifyOrder ShopifyOrder { get; set; }
}
In the invoice engine, I'm running the following code for each invoice to add it to the database:
ShopifyOrder order = await db.ShopifyOrders.SingleOrDefaultAsync(x => x.OrderNumber.ToString() == inv.OrderNumber);
if (order != null) {
// Attach marketplace entity to the invoice to avoid duplicate primary key exceptions
db.Marketplaces.Attach(inv.Marketplace);
db.Invoices.Add(inv);
order.InvoiceStatus = OrderInvoiceStatus.InProgress;
}
I've tried a number of methods to try and attach the states, however they all throw errors.
inv.LineItems.ForEach(li => {
db.Entry(li).State = EntityState.Unchanged;
db.Entry(li.ShopifyOrderItem).State = EntityState.Unchanged;
db.Entry(li.ShopifyOrderItem.ShopifyOrder).State = EntityState.Modified;
});
The above code returns the following error on save:
EntityFramework: Saving or accepting changes failed because more than one entity of type 'TorroModels.ShopifyOrder' have the same primary key value. Ensure that explicitly set primary key values are unique. Ensure that database-generated primary keys are configured correctly in the database and in the Entity Framework model.
What is the best way to attach the LineItems/ShopifyOrderItems without trying to attach the ShopifyOrder connected property multiple times?
Sorry to say but it seems that you need to follow the best practice first when constructing a relationship. You may follow this link :
http://www.entityframeworktutorial.net/entity-relationships.aspx
In short :
Avoid using only "Id" in every entity, or you can use attributes to map between the physical name and the property name
It seems that you have circular references here, so maybe you could simplify it first
Next, you can read this link :
http://www.entityframeworktutorial.net/EntityFramework5/attach-disconnected-entity-graph.aspx
if you need to know more about what's the best practice of attaching entities, but in my opinion, just don't abuse this feature, because using normal CRUD should be sufficient most of the time.
I'm sorry I cannot help you more than this, because of lack of information I may need, and with my reputation I still cannot comment directly in your post to ask for it.
Sorry for my bad English.
I have EF 6 codefirst models for database.
3 models is usually static, loaded at program startup (using aka foreign keys for last table).
Last model is dynamical - data is loaded too, stored in c# collection, but user can add, edit rows and save added/edited to DB.
For 3 first models i have checkboxes with selecteditem binding.
User can edit last table model entity, select items from checkboxes and save to DB.
This is simple and standart solution.
Partial models without trash fields (to reference from last table).
[Table("Users")]
public partial class User
{
[Key, DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public int ClientId { get; set; }
[StringLength(160)]
public string ClientName { get; set; }
public virtual ICollection<Repair> Repairs { get; set; }
public User()
{
Repairs = new List<Repair>();
}
}
[Table("RepairStatuses")]
public partial class RepairStatus
{
[Key, DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public int Id { get; set; }
[StringLength(10)]
public string Status { get; set; }
public virtual ICollection<Repair> Repairs { get; set; }
public RepairStatus()
{
Repairs = new List<Repair>();
}
}
[Table("CurrentStatuses")]
public partial class CurrentStatus
{
[Key, DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public int StatusId { get; set; }
[StringLength(10)]
public string Status { get; set; }
public virtual ICollection<Repair> Repairs { get; set; }
public CurrentStatus()
{
Repairs = new List<Repair>();
}
}
And main editable table model (partial too w/o trash fields).
[Table("Repairs")]
public partial class Repair
{
[Key, DatabaseGenerated(DatabaseGeneratedOption.None)]
public int Id { get; set; }
[Column(TypeName = "date")]
public DateTime Date { get; set; }
[StringLength(255)]
public string HardwareInfo { get; set; }
public virtual User User { get; set; }
public virtual RepairStatus RepairStatus { get; set; }
public virtual CurrentStatus CurrentStatus { get; set; }
}
In my AddEntity method all working (attach unchanged items from combobox to DbContext, add new row, save changes). Eager loading.
using (ServiceDBContext cntx = new ServiceDBContext())
{
cntx.Users.Attach(SelectedRepair.User);
cntx.CurrentStatuses.Attach(SelectedRepair.CurrentStatus);
cntx.RepairStatuses.Attach(SelectedRepair.RepairStatus);
cntx.Entry(SelectedRepair.RepairStatus).State = EntityState.Modified;
cntx.Entry(SelectedRepair.CurrentStatus).State = EntityState.Modified;
cntx.Entry(SelectedRepair.User).State = EntityState.Modified;
cntx.Repairs.Attach(SelectedRepair);
cntx.Entry(SelectedRepair).State = EntityState.Added;
...
cntx.SaveChanges();
...
But with EditEntity method i have strange behavior (sorry for stupid code...)
using (ServiceDBContext wrk = new ServiceDBContext())
{
var tmp = (((((wrk.Repairs.Where(x => x.Id ==SelectedRepair.Id)).Include(y => y.CurrentStatus)).Include(y => y.RepairStatus)).Include(y => y.Engineer)).Include(y => y.User)).FirstOrDefault();
if (tmp.User.ClientId != SelectedRepair.User.ClientId)
{
tmp.User = SelectedRepair.User;
wrk.Users.Attach(tmp.User);
wrk.Entry(tmp.User).State = EntityState.Modified;
}
if (tmp.RepairStatus.Id != SelectedRepair.RepairStatus.Id)
{
tmp.RepairStatus = SelectedRepair.RepairStatus;
wrk.RepairStatuses.Attach(tmp.RepairStatus);
wrk.Entry(tmp.RepairStatus).State = EntityState.Modified;
}
if (tmp.CurrentStatus.StatusId != SelectedRepair.CurrentStatus.StatusId)
{
tmp.CurrentStatus = SelectedRepair.CurrentStatus;
wrk.CurrentStatuses.Attach(tmp.CurrentStatus);
wrk.Entry(tmp.CurrentStatus).State = EntityState.Modified;
}
...
wrk.Entry(tmp).State = EntityState.Modified;
wrk.SaveChanges();
}
For example: CurrentStatuses table have 2 entities ("1. OK", "2. Bad").
Then user first time changing in Repair table in selected row CurrentStatus foreign key (for example, with id =1 to foreign key with id=2) all is OK.
In VS debugger i can see...
UPDATE [dbo].[CurrentStatuses] SET [Status] = #0 WHERE ([StatusId] = #1)
UPDATE [dbo].[Repairs] SET ... WHERE (([Id] = #12) AND ([CurrentStatus_StatusId] = #13))
If user want to change second time this entity from id=2 to id=1 (reverse) its throwing error "An error occurred while saving entities that do not expose foreign key properties for their relationships..."
AND in debugger we can see some magic with "Reader (INSERT)" attempts to all database relative tables o_O and attempt to INSERT in Repair table dublicate entry (which was been selected to edit).
One INSERT example (Repair, RepairStatus and User have like this INSERTS too):
DECLARE #0 AS SQL_VARIANT;
SET #0 = NULL;
INSERT [dbo].[CurrentStatuses]([Status])
VALUES (#0)
SELECT [StatusId]
FROM [dbo].[CurrentStatuses]
WHERE ##ROWCOUNT > 0 AND [StatusId] = scope_identity()
After program restart we can change CurrentStatus foreign key from id=2 to id=1 normally (but only 1 time too).
Can someone help me to solve this problem?
Thanks!
I have a one-to-many relationship where I am trying to delete a set of instances on the "many" side, but keep getting the exception "Adding a relationship with an entity which is in the Deleted state is not allowed". This is Entity Framework 6.1.1.
The relationship is one-to-many from Teacher to Course. The two classes are defined as:
[Table("Course")]
public partial class Course {
public int Id { get; set; }
public int? TeacherId { get; set; }
public virtual Teacher Teacher { get; set; }
}
[Table("Teacher")]
public partial class Teacher
{
public Teacher()
{
Course = new HashSet<Course>();
}
public int Id { get; set; }
[Required]
[StringLength(50)]
public string TeacherName { get; set; }
public virtual ICollection<Course> Course { get; set; }
}
The code that tries to delete the courses, is part of an import: A set of courses is coming in, and the courses that are in the database but not part of the incoming courses, should be deleted from the database. (In addition, the courses that are part of the incoming set but not in the database, should be created, but this seems to work).
var existingCourses = ctx.Courses.ToList();
var toCreate = incomingCourses.Where(x => !existingCourses.Contains(x)).ToList();
var coursesToDelete = existingCourses.Where(x => !incomingCourses.Contains(x)).ToList();
ctx.Courses.RemoveRange(coursesToDelete);
ctx.SaveChanges(); // The exception occurs here
The incoming set of courses are parsed from an XML file to a DTO. Before comparing them to the existing courses, they are placed in a list as:
var incomingCourses = incomingDtos.Select(x => new Course
{
Teacher = new Teacher { TeacherName = x.TeacherNameFromXml }
}.ToList();
There are other properties on the Course entity that identifies the course, but I have not shown them here as I suppose they are irrelevant.
When debugging, I noticed that the Teacher property of the Courses that are being deleted are non-empty before the call to RemoveRange() but null afterwards.. So it seems that there is some kind of cascade delete taking place.
I have tried to remove all cascade deletes via my DbContext and also specifying the relationship there. This changes nothing.
public class MyDbContext : DbContext {
public MyDbContext() {}
public MyDbContext(string connectionString) {
Database.Connection.ConnectionString = connectionString;
}
public virtual DbSet<Teacher> Teacher { get; set; }
public virtual DbSet<Course> Courses { get; set; }
protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
modelBuilder.Conventions.Remove<ManyToManyCascadeDeleteConvention>();
modelBuilder.Conventions.Remove<OneToManyCascadeDeleteConvention>();
modelBuilder.Entity<Teacher>()
.HasMany(x => x.Course)
.WithOptional(x => x.Teacher)
.HasForeignKey(x => x.TeacherId)
.WillCascadeOnDelete(false);
}
}
The comment from #LukasKabrt pointed me in the right direction. Instead of comparing the Course objects directly, I convert the existing courses to DTOs "temporarily" inside the LINQ and compare DTOs. Now I am able to delete all from coursesToDelete
var coursesToDelete = (
from e in existing
let dto = new CourseDto(e)
where !incomingDtos.Contains(dto)
select e).ToList();
EDIT: The solution I selected probably wasn't the best, but it definitely worked. I'll be going through my code over the next week (once this project is done) and I'll update my question when I understand what went wrong.
I'm using the ASP.NET MVC 4 framework with Entity 5. Here's some code:
The class to be instantiated and saved (fresh) in the database:
public class ClassCancellation
{
[Key]
public int Id { get; set; }
public Faculty Professor { get; set; }
public DateTime CancelledOn { get; set; }
public Course Course { get; set; }
[Required]
public ClassDate ClassCancelled { get; set; }
public Message CancellationMessage { get; set; }
[Required]
public List<Student> Students { get; set; }
}
It's mapped from the viewmodel called CancellationFull (with AutoMapper):
public class CancellationForList
{
[Key]
public int Id { get; set; }
public CourseForList Course { get; set; }
public ClassDateForList ClassCancelled { get; set; }
}
public class CancellationFull : CancellationForList
{
public CancellationFull()
{
this.Students = new List<StudentForList>();
}
public FacultyForList Professor { get; set; }
public MessageForList CancellationMessage { get; set; }
public DateTime CancelledOn { get; set; }
public List<StudentForList> Students { get; set; }
}
This is the repo method that turns a CancellationFull into a ClassCancellation and then saves it to the database:
public CancellationFull createClassCancellation(CancellationFull c)
{
ClassCancellation newCancellation = Mapper.Map<ClassCancellation>(c);
dc.ClassCancellations.Add(newCancellation);
dc.SaveChanges();
return Mapper.Map<CancellationFull>(dc.ClassCancellations.FirstOrDefault(cc => cc.Id == newCancellation.Id));
}
Why, for the love of god why, does the database create new objects for Faculty and Course when the Id (primary key) of each's existing entity counterpart is provided? It might also be doing the same with Student objects but I haven't looked that closely.
Before the ClassCancellation instance is saved to the database the debugger shows that it's attributes Professor of type Faculty and Course of type Course have the correct primary key - that is, the primary key of the already existing entities of those types that I'm trying to update with a reference to the new ClassCancellation object.
Driving me nuts. Feel free to ask for clarification!
EDIT:
Here's the logic where the CancellationFull viewmodel is constructed from form data and viewmodels about existing objects retrieved from their respective repos:
newCancellation = new CancellationFull();
newCancellation.CancelledOn = DateTime.Now;
newCancellation.ClassCancelled = repoClass.getClassDateForListById(Int32.Parse(classIds[i]));
newCancellation.Course = repoCourse.getForList(newCancellation.ClassCancelled.Course.Id);
newCancellation.CancellationMessage = repoMessage.getMessageForList(newMessage.Id);
newCancellation.Professor = repoFac.getFacultyForList((int)Session["facId"]);
var students = repoStudent.getStudentsForListByCourse(newCancellation.Course.Id);
foreach ( var student in students )
{
newCancellation.Students.Add(student);
}
repoCancellation.createClassCancellation(newCancellation);
Here's an example of one of those repo methods (the rest are very similar):
public CourseForList getForList(int? id)
{
return Mapper.Map<CourseForList>(dc.Courses.FirstOrDefault(c => c.Id == id));
}
What I find the easiest solution is when updating a model, clear any related entities, then re add them.
ie:
newCancellation.Students.Clear();
foreach ( var student in students )
{
newCancellation.Students.Add(student);
}
Try using Attach() instead of Add()
dc.ClassCancellations.Attach(newCancellation);
dc.SaveChanges();
Add() is used for new objects that do not already exist in the database. Attach() is used for creating relationships to entities that already exist in the database.
EDIT
Without seeing your code, the best solution I can recommend to attach is to create a 'stub' instance and then attach that to your newCancellation:
var existingCourse = new Course{ Id = newCancellation.ClassCancelled.Course.Id };
db.Courses.Attach(existingCourse);
newCancellation.Course = existingCourse;
The problem is that you have multiple contexts, or units of work. When you add the newCancellation to the dc context, it also adds any related entity in the object graph that is not tracked in the dc context. I think your best option is:
dc.ClassCancellations.Add(newCancellation);
dc.Entry(newCancellation.Course).State = EntityState.Unchanged;
dc.Entry(newCancellation.Faculty).State = EntityState.Unchanged;
See Julie Lerman's article on this issue for an explanation and other options.
In my opinion, EF should recognize entities that have autonumbered keys and not insert them if the key is assigned.