EntityFramework CF strange behavior at editing entity - c#

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!

Related

EF Core one-to-one relationship swapping references issue

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.

Cannot insert the value NULL into column 'Id', table '; column does not allow nulls

I'm seeing a strange behavior from Entity Framework. I'm using code-first approach to define a One-To-Many relationship between two entities:
public class IncomingCheck : AuditedEntityBase
{
[Key]
public int Id { get; set; }
[Required]
public virtual CheckType Type { get; set; }
[Required]
public virtual Bank Bank { get; set; }
public string Branch { get; set; }
public virtual IList<IncomingCheckHistory> History { get; set; }
}
public class IncomingCheckHistory
{
[Key]
public int Id { get; set; }
public string LongDescription { get; set; }
}
And here's I I'm trying to add an item to the History:
using (var db = new CheckDataContext())
{
foreach (var check in SelectedItems)
{
var dbCheck = await db.IncomingChecks.FindAsync(check.Id);
var history = new IncomingCheckHistory()
{
LongDescription = "something",
};
dbCheck.History.Add(history);
await db.SaveChangesAsync(); //throws the exception
}
}
But it throws an exception saying that "Cannot insert the value NULL into column 'Id'". However I've always done it like this. The database is supposed to fill the Id column itself with a unique number.
What am I missing?
Update:
Using SSProfiler, I got the query that runs on the database. It's as follows:
exec sp_executesql N'INSERT [dbo].[IncomingCheckHistories]([LongDescription], [IncomingCheck_Id])
VALUES (#0, #1)
SELECT [Id]
FROM [dbo].[IncomingCheckHistories]
WHERE ##ROWCOUNT > 0 AND [Id] = scope_identity()',N'#0 nvarchar(max) ,#1 int',#0=N'Something',#1=1
Note that scope_identity() should be getting the Id itself, correct?
Shouldn't the Id be set as the Identity?
Like this:
public class IncomingCheckHistory
{
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public int Id { get; set; }
[Key]
public string LongDescription { get; set; }
}
Hope that this is the solution to your problem!
Well for some strange reason the exact code worked after completely deleting the database and migrations and starting from scratch. I'm not sure why it wouldn't work without migrations. Also I had tried deleting the database before but at the time I was trying a little different code.
In any case the posted code/query should work. Also I checked the database and the columns to see if anything is different and it wasn't. I would appreciate if someone could shed some light on why it wasn't working and it is now.

How to get primary key in Entity Framework for a new entry and set FK Relationship

In my ASP.NET MVC 5 application, I have situation where I need to get the primary key that got generated for a particular entry in database. Also, I need to get that id and set a foreign key relationship in database. I have tried below attempt and it seems to be working but I had to call _context.SaveChanges() TWICE.
UI Behaviour: the user has a dropdown with a list of companies in referral form. When a company is not found, user will select "Others" in the dropdown. And it will show a textbox, where user will enter the company name. So I need to add this company to the database and link it to the referral row.
Relationship between tables Referral and Company:
dBModelBuilder.Entity<Company>()
.HasMany(u => u.Referrals)
.WithRequired(u => u.Company)
.HasForeignKey(u => u.CompanyId);
Model classes:
public class Company
{
[Key]
public int CompanyId { get; set; }
public string CompanyName { get; set; }
public ICollection<Referral> Referrals { get; set; }
public ICollection<CoverLetter> CoverLetters { get; set; }
}
public class Referral
{
[Key]
public int ReferralId { get; set; }
[Display(Name = "REFERRALS")]
public string ReferralName { get; set; }
public int? CoverLetterId { get; set; }
public virtual CoverLetter CoverLetter { get; set; }
public int CompanyId { get; set; }
public virtual Company Company { get; set; }
}
ViewModel:
public class Referral
{
[Key]
public int ReferralId { get; set; }
[Display(Name = "REFERRALS")]
public string ReferralName { get; set; }
public int? CoverLetterId { get; set; }
public virtual CoverLetter CoverLetter { get; set; }
public int CompanyId { get; set; }
public virtual Company Company { get; set; }
}
My attempt inside the controller action:
[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult Create(ReferralViewModel viewModel)
{
var candidateId = User.Identity.GetUserId();
var referral = new Referral
{
ReferralName = viewModel.ReferralName,
};
if (viewModel.CompanyId.HasValue)
// if it is option 4 then we need to add it to the company table
{
if (!string.IsNullOrEmpty(viewModel.TempCompany))
{
var f = new Company
{
CompanyName = viewModel.TempCompany
};
_context.Companies.Add(f);
// ********FIRST CALL
_context.SaveChanges();
_context.Referrals.Add(referral);
referral.CompanyId = f.CompanyId;
// **********SECOND CALL
_context.SaveChanges(); // SECOND CALL ------
}
else
{
referral.CompanyId = viewModel.CompanyId.Value;
_context.Referrals.Add(referral);
_context.SaveChanges();
}
}
return RedirectToAction("ReferralCenter");
}
Question: can I do these steps in one call to _context.SaveChanges(); ?
EDIT
With this code, I get a NullReferenceException:
if (!string.IsNullOrEmpty(viewModel.TempCompany))
{
var f = new Company
{
CompanyName = viewModel.TempCompany
};
f.Referrals.Add(referral); // Referrals is NULL(EXCEPTION)
_context.Companies.Add(f);
// _context.SaveChanges();
//_context.Referrals.Add(referral);
// referral.CompanyId = f.CompanyId;
_context.SaveChanges();
}
It is my understanding that FK constraints are managed within EF, so long as they are configured in the DB.
If in your EF model the Referral is a child of Company then there should be a referrals collection accessible from the Company object instance.
UPDATE
I just built a simple sample MVC Proj, 2x SQL tables (Referrals and Company FK on CompanyId) 4 x Companies, 4th being OTHER, built an EF model with Reverse POCO for speed, added a Controller with scaffolding and views and modified my controller to the below. (I just insert the date time rather than tempCompanyName. The syntax generated here is more like that in the answer by #ashin, but it works for me.
[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult Create([Bind(Include = "ReferralId,CompanyId")] Referral referral)
{
if (ModelState.IsValid)
{
if (referral.CompanyId == 4)
{
var f = new Company() { Name = DateTime.Now.ToString() };
referral.Company = f;
}
db.Referrals.Add(referral);
db.SaveChanges();
return RedirectToAction("Index");
}
ViewBag.CompanyId = new SelectList(db.Companies, "CompanyId", "Name", referral.CompanyId);
return View(referral);
}
My Company model does instanciate a new Referrals Collection
public Company()
{
Referrals = new System.Collections.Generic.List<Referral>();
}
If you have the navigation property defined, you could just save both the Company and Referral in one shot as follows:
if (!string.IsNullOrEmpty(viewModel.TempCompany))
{
var f = new Company
{
CompanyName = viewModel.TempCompany
};
referral.Company = f;
_context.SaveChanges();
}
EF will take care of setting the FK.
EDIT
Have updated the code to include _context.Referrals.Add(referral) as mentioned in the comments:
if (!string.IsNullOrEmpty(viewModel.TempCompany))
{
var f = new Company
{
CompanyName = viewModel.TempCompany
};
referral.Company = f;
_context.Referrals.Add(referral);
_context.SaveChanges();
}

Entity framework [Required] attribute on relationships behaves differently when used in seed method

I'm using EF code first with automatic migrations enabled.
I have below entities.
public class Order
{
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
[Key]
public int OrderNo { get; set; }
[Required]
public int BranchId { get; set; }
[Required]
[ForeignKey("BranchId")]
public virtual Branch Branch { get; set; }
}
public class Branch
{
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
[Key]
public int Id{ get; set; }
public virtual ICollection<Order> Order { get; set; }
}
Below is my seed on Configuration.cs and it works fine when I run Update-Database command via package manager console.
protected override void Seed(DataAccessLayer.ApplicationDbContext db)
{
Branch branch = db.Branch.FirstOrDefault(x => x.Id == 1); // Id = 1 already exists in the table
Order order = new Order()
{
BranchId = branch.Id
}
db.Order.Add(order);
db.SaveChanges();
}
But when I use above code in a normal class it throws an Entity Framework validation error
private void AddOrder()
{
using (var db = new ApplicationDbContext())
{
Branch branch = db.Branch.FirstOrDefault(x => x.Id == 1); // Id = 1 already exists in the table
Order order = new Order()
{
BranchId = branch.Id
}
db.Order.Add(order);
db.SaveChanges();
}
}
Above gives below error
ErrorMessage = "The Branch field is required."
So when I change above to below code with additional two lines it works fine
private void AddOrder()
{
using (var db = new ApplicationDbContext())
{
Branch branch = db.Branch.FirstOrDefault(x => x.Id == 1); // Id = 1 already exists in the table
Order order = new Order()
{
BranchId = branch.Id
Branch = branch
}
db.Entry(branch).State = EntityState.Unchanged; // needed to avoid duplicating branch information
db.Order.Add(order);
db.SaveChanges();
}
}
It makes sense why the validation error due to obvious reasons, but my question is why is this not enforced in the seed method?
I'd prefer to use the code same as from seed method in my other classes, is there a way to achieve this ?

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");
}

Categories

Resources