Entity Framework adding too many records - c#

EDIT: See the bottom of this question for the working code.
I have two tables, Patients and Drugs, that I am updating with a data feed. I get a current list of patients, then iterate through and update or insert records as appropriate. This works without issue.
The trouble comes when I iterate through that patient's current medications. I end up getting multiple copies of the original patient. Drug records are transferred as expected (the records themselves don't change so new records are inserted and existing records ignored). I end up with the original patient record (inserted from UpdatePatients() below) and then one additional patient record for each medication record. Each medication record ends up with a distinct PatientId.
Class definitions:
public class Patient
{
public int PatientId { get; set; }
[Required]
public int FacilityNumber { get; set; }
[Required]
public int PatNo { get; set; }
[Required]
public string Name { get; set; }
[Required]
public int Age { get; set; }
[Required]
public string Gender { get; set; }
[Required]
public DateTime VentStart { get; set; }
[Required]
public DateTime VentEnd { get; set; }
[Required]
public DateTime AdmitDate { get; set; }
public DateTime? DischargeDate { get; set; }
}
public class Drug
{
public int DrugId { get; set; }
[Required]
public int DrugDDI { get; set; }
[Required]
public int OrderId { get; set; }
[Required]
public string DrugName { get; set; }
[Required]
public DateTime DispenseDate { get; set; }
[Required]
public double UnitsDispensed { get; set; }
[ForeignKey("Patient")]
public int PatientId { get; set; }
public virtual Patient Patient { get; set; }
}
Offending code:
private static void UpdatePatients()
{
var Patients = DB2Patient.GetPatients();
foreach (Patient p in Patients)
{
using (var PatientContext = new VAEContext())
{
var ExistingPatientRecord = PatientContext.Patients.FirstOrDefault(
ep => ep.PatNo == p.PatNo
);
if (ExistingPatientRecord != null)
{
ExistingPatientRecord.VentEnd = p.VentEnd;
ExistingPatientRecord.DischargeDate = p.DischargeDate;
PatientContext.SaveChanges();
}
else
{
PatientContext.Patients.Add(p);
PatientContext.SaveChanges();
}
}
UpdateDrugs(p);
}
}
private static void UpdateDrugs(Patient p)
{
var Drugs = DB2Drug.GetDrugs(p.PatNo);
foreach (Drug d in Drugs)
{
using (var DrugContext = new VAEContext())
{
var ExistingDrugRecord = DrugContext.Drugs.FirstOrDefault(
ed => ed.DrugDDI == d.DrugDDI &&
ed.DispenseDate == d.DispenseDate &&
ed.OrderId == d.OrderId
);
if (ExistingDrugRecord == null)
{
d.Patient = p;
DrugContext.Drugs.Add(d);
DrugContext.SaveChanges();
}
}
}
}
EDIT: Working code:
private static void UpdatePatients()
{
var Patients = DB2Patient.GetPatients();
using (var db = new VAEContext())
{
foreach (Patient p in Patients)
{
var ExistingPatientRecord = db.Patients.FirstOrDefault(
ep => ep.PatNo == p.PatNo
);
if (ExistingPatientRecord != null)
{
ExistingPatientRecord.VentEnd = p.VentEnd;
ExistingPatientRecord.DischargeDate = p.DischargeDate;
}
else
{
db.Patients.Add(p);
}
UpdateDrugs(p, db);
}
db.SaveChanges();
}
}
private static void UpdateDrugs(Patient p, VAEContext ctx)
{
var Drugs = DB2Drug.GetDrugs(p.PatNo);
foreach (Drug d in Drugs)
{
var ExistingDrugRecord = ctx.Drugs.FirstOrDefault(
ed => ed.DrugDDI == d.DrugDDI &&
ed.DispenseDate == d.DispenseDate &&
ed.OrderId == d.OrderId
);
if (ExistingDrugRecord == null)
{
d.Patient = p;
ctx.Drugs.Add(d);
}
}
}

Why new context every time something needs to be inserted? Both methods UpdatePatients and UpdateDrugs are private, you can use the same context for all linked operations and I'm sure you won't get the duplicates:
private static void UpdateDrugs(Patient p, VAEContext context)
...
Also there's probably no need to save on every drug, doing so likely decreases performance and doesn't do much in terms of data integrity. Consider saving the context changes once per linked updates (say after UpdateDrugs is called in UpdatePatients)
Other than that you can check out the ObjectContext.Attach and related methods on how to link the Patient object to your newly created Drugs context instance
http://msdn.microsoft.com/en-us/library/system.data.objects.objectcontext.attach.aspx

Related

Why am I getting new records created in table a when I edit a record of another table?

I have an Invoice object that has a list of items and other properties. Whenever I execute the service that edits an invoice, each item get doubled for some reason.
For example, here's a snapshot of the items table after creating an invoice with two items:
Items table
And here's a snapshot of it after executing EditInvoice service: Items table after editing
Data Model
Invoice
public class Invoice
{
public string InvoiceId { get; set; } = GenerateID.GenerateInvoiceID();
public string Description { get; set; }
public List<InvoiceItem> Items { get; set; }
public DateTime InvoiceDate { get; set; }
public string PaymentTerms { get; set; }
public DateTime PaymentDue { get; set; }
public int TotalPrice { get; set; }
public string Status { get; set; } = "pending";
public Client Client { get; set; }
public string ClientId { get; set; }
public string BillFromAddress { get; set; }
public string BillFromCity { get; set; }
public string BillFromCountry { get; set; }
public string BillFromPostal { get; set; }
}
InvoiceItem
public class InvoiceItem
{
public string InvoiceItemId { get; set; } = GenerateID.GenerateItemID();
public string InvoiceId { get; set; }
[Required]
public string Name { get; set; }
[Required]
public int Quantity { get; set; }
[Required]
public int Price { get; set; }
public InvoiceItem()
{
}
public InvoiceItem(string itemName, int quantity, int price)
{
Name = itemName;
Quantity = quantity;
Price = price;
}
}
My InputModel
public class InputModel
{
[Required]
public string Description { get; set; }
[Required]
[DataType(DataType.Date)]
public DateTime InvoiceDate { get; set; }
public string PaymentTerms { get; set; }
public DateTime PaymentDue { get; set; }
public Client Client { get; set; }
public List<InvoiceItem> Items { get; set; } = new List<InvoiceItem>(16);
[Required]
public string BillFromAddress { get; set; }
[Required]
public string BillFromCity { get; set; }
[Required]
public string BillFromCountry { get; set; }
[Required]
public string BillFromPostal { get; set; }
public void PopulateItems()
{
for (int i = 0; i < Items.Capacity; i++)
{
Items.Add(new InvoiceItem());
}
}
}
My Page Models
CreateInvoiceModel
public class CreateInvoiceModel : PageModel
{
public readonly InvoiceService _service;
[BindProperty]
public InputModel Input { get; set; }
public CreateInvoiceModel(InvoiceService service)
{
_service = service;
}
public void OnGet()
{
Input = new InputModel();
Input.PopulateItems();
}
public async Task<IActionResult> OnPost()
{
if (ModelState.IsValid)
{
_service.AddInvoice(Input);
return RedirectToPage("/Index");
}
return Page();
}
}
EditInvoiceModel
public class EditInvoiceModel : PageModel
{
public readonly InvoiceService _service;
[BindProperty]
public InputModel Input { get; set; }
public string InvoiceId { get; set; }
public EditInvoiceModel(InvoiceService service)
{
_service = service;
}
public async void OnGet(string id)
{
Invoice invoice = await _service.GetInvoice(id);
InvoiceId = invoice.InvoiceId;
Input = new InputModel();
Input.Items = invoice.Items;
Input.BillFromAddress = invoice.BillFromAddress;
Input.BillFromCity = invoice.BillFromCity;
Input.BillFromPostal = invoice.BillFromPostal;
Input.BillFromCountry = invoice.BillFromCountry;
Input.Client = invoice.Client;
Input.InvoiceDate = invoice.InvoiceDate;
Input.PaymentTerms = invoice.PaymentTerms;
Input.Description = invoice.Description;
}
public async Task<IActionResult> OnPost(string id)
{
if(ModelState.IsValid)
{
_service.EditInvoice(Input, id);
return RedirectToPage("/ViewInvoice", new { id = id });
}
return Page();
}
}
Services
AddInvoice
public async void AddInvoice(InputModel input)
{
Invoice invoice = new Invoice();
invoice.Description = input.Description;
invoice.Items = input.Items;
invoice.InvoiceDate = input.InvoiceDate;
invoice.PaymentTerms = input.PaymentTerms;
invoice.Client = input.Client;
invoice.BillFromAddress = input.BillFromAddress;
invoice.BillFromCity = input.BillFromCity;
invoice.BillFromCountry = input.BillFromCountry;
invoice.BillFromPostal = input.BillFromPostal;
//Attaching the invoice id to each item in the invoice
foreach (var item in invoice.Items)
{
item.InvoiceId = invoice.InvoiceId;
}
//IndexModel.invoices.Add(invoice);
_context.Add(invoice);
await _context.SaveChangesAsync();
}
EditInvoice
public async void EditInvoice(InputModel input, string id)
{
var invoice = await _context.Invoices.FindAsync(id);
if (invoice == null) { throw new Exception("Unable to find the invoice"); }
invoice.Items = input.Items;
invoice.Description = input.Description;
invoice.InvoiceDate = input.InvoiceDate;
invoice.PaymentTerms = input.PaymentTerms;
invoice.Client = input.Client;
invoice.BillFromAddress = input.BillFromAddress;
invoice.BillFromCity = input.BillFromCity;
invoice.BillFromCountry = input.BillFromCountry;
invoice.BillFromPostal = input.BillFromPostal;
await _context.SaveChangesAsync();
}
Your code has multiple problems:
First of all in your InvoiceItem you have the following line
public string InvoiceItemId { get; set; } = GenerateID.GenerateItemID();
which means that whenever n InvoiceItem is instanciated a new Id is generated which is not correct because there is a difference between actually creating an InvoiceItem and just creating an instance of the class InvoiceItem. A new Id should only be generated if a new InvoiceItem is created but not for example if an existing InvoiceItem is loaded from the database (in both cases an instance of the class InvoiceItem is created but only in the first one an actual InvoiceItem shall be created). So removing the id generation from the property declaration and only performing is when an InvoiceItem shall actually be created will fix this part of the problem.
The second problem is in EditInvoice where you call
var invoice = await _context.Invoices.FindAsync(id);
// ...
invoice.Items = input.Items;
in the first line you are loading the invoice from the database but you are not including the invocies and therefore they do not get loaded and EF does not even know they exist.
So when you are calling invoice.Items = input.Items you are assigning the list with the new InvoiceItems with the newly generated ids (as explained above) and they will therefore be added to the database and hence duplicated.
So instead of replacing the whole list you should be editing existing items and only adding InvoiceItems which have actually been created to the list.
Instead of
invoice.Items = input.Items;
You can work with:
For adding a new InvoiceItem record, presume the invoice item doesn't have InvoiceId (foreign key value), use .Add() as adding a new record.
For the existing InvoiceItem record, presume the invoice item has InvoiceId (foreign key value), set the entity, and modify its state as Modified. Make sure that the entity's Id (primary key (value)) matches with the record in the database table. Reference: Attaching an existing but modified entity to the context
public async void EditInvoice(InputModel input, string id)
{
var invoice = await _context.Invoices.FindAsync(id);
if (invoice == null) { throw new Exception("Unable to find the invoice"); }
foreach (var item in input.Items)
{
if (String.IsNullOrEmpty(item.InvoiceId))
{
invoice.Add(item);
}
else
{
context.Entry(item).State = EntityState.Modified;
}
}
invoice.Description = input.Description;
invoice.InvoiceDate = input.InvoiceDate;
invoice.PaymentTerms = input.PaymentTerms;
invoice.Client = input.Client;
invoice.BillFromAddress = input.BillFromAddress;
invoice.BillFromCity = input.BillFromCity;
invoice.BillFromCountry = input.BillFromCountry;
invoice.BillFromPostal = input.BillFromPostal;
await _context.SaveChangesAsync();
}
Or you can work as:
foreach (var item in input.Items)
{
if (String.IsNullOrEmpty(item.InvoiceId))
{
item.InvoiceId = invoice .InvoiceId;
context.Entry(item).State = EntityState.Added;
}
else
{
context.Entry(item).State = EntityState.Modified;
}
}
Reference: Insert or update pattern
Although the provided documentation is regarding Entity Framework 6, it supports in Entity Framework Core.

EF Core table first not saving entities in the database

I'm new to EF (table first) and I don't know why these related entities are not saving at all to my database.
These are the related entities, UserProfile has a set of Carts
public partial class UserProfile
{
public UserProfile()
{
Cart = new HashSet<Cart>();
Naquestions = new HashSet<Naquestions>();
}
public int Id { get; set; }
public string BotUserId { get; set; }
public int? PrestashopId { get; set; }
public bool Validated { get; set; }
public int Permission { get; set; }
public DateTime CreationDate { get; set; }
public ICollection<Cart> Cart { get; set; }
public ICollection<Naquestions> Naquestions { get; set; }
}
Cart has a set of OrderLines
public partial class Cart
{
public Cart()
{
OrderLine = new HashSet<OrderLine>();
OrderRequest = new HashSet<OrderRequest>();
}
public int Id { get; set; }
public int UserId { get; set; }
public bool Active { get; set; }
public UserProfile User { get; set; }
public ICollection<OrderLine> OrderLine { get; set; }
public ICollection<OrderRequest> OrderRequest { get; set; }
}
And when I try to add them:
public async Task AddOrderLineToUser(string botId, OrderLine orderLine)
{
using (var context = ServiceProvider.CreateScope())
{
var db = context.ServiceProvider.GetRequiredService<GretaDBContext>();
var user = await UserController.GetUserByBotIdAsync(botId);
var latestCart = user.Cart.OrderByDescending(c => c.Id).FirstOrDefault();
if (latestCart != null && latestCart.Active)
{
latestCart.OrderLine.Add(orderLine);
}
else
{
var newCart = new Cart()
{
Active = true,
};
newCart.OrderLine.Add(orderLine);
user.Cart.Add(newCart);
}
await db.SaveChangesAsync();
}
}
Nothing is saving to the database once db.SaveChangesAsync() is called.
As #Caius Jard said in the comments it seems that user comes from another context. Try
if (latestCart != null && latestCart.Active)
{
orderLine.CartId = latestCart.Id;
db.OrderLines // I assume it is name of your orderlines DbSet
.Add(orderLine);
}
else
{
var newCart = new Cart()
{
Active = true,
UserId = user.Id,
};
newCart.OrderLine.Add(orderLine);
db.Carts // also assuming name of DbSet
.Add(newCart);
}
Also you can take a look at Attach method.
But I would say that in general you are doing something not good. Usually creating new scope is not needed, and db context should be injected in corresponding class via ctor. If you still need to create new scope it would make sense to resolve UserController also. Also is UserController an ASP controller?

How to update data in a related table in EF?

There are two such models:
public class Form
{
[Key]
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public Guid FormId { get; set; }
public string Title { get; set; }
public string Description { get; set; }
public List<BlockWorkingForm> BlocksWorkingForm { get; set; }
}
public class BlockWorkingForm
{
[Key]
[Column(Order = 1)]
public string Header { get; set; }
[Key]
[Column(Order = 2)]
public Guid FormId { get; set; }
public Form Form { get; set; }
public string Field { get; set; }
public bool MandatoryQuestion { get; set; }
public override bool Equals(object obj)
{
if (obj == null)
{
return false;
}
if (!(obj is BlockWorkingForm m))
{
return false;
}
return m.Header == this.Header
&& m.Field == this.Field
&& m.Type == this.Type
&& m.MandatoryQuestion == this.MandatoryQuestion;
}
}
And there is such a method for updating the model.
public void UpdateForm(Form form)
{
EditorFormContext context = new EditorFormContext();
var formDb = this.context.Forms.Include(x => x.BlocksWorkingForm).Single(x => x.FormId == form.FormId);
this.context.Entry(formDb).CurrentValues.SetValues(form);
foreach (var itemForm in form.BlocksWorkingForm)
{
if (itemForm.FormId == Guid.Empty)
{
itemForm.FormId = formDb.FormId;
this.context.BlocksWorkingForm.Add(itemForm);
}
foreach (var itemFormDb in formDb.BlocksWorkingForm)
{
if (itemForm.Header != itemFormDb.Header)
{
continue;
}
if (!itemForm.Equals(itemFormDb))
{
this.context.Entry(itemFormDb)
.CurrentValues.SetValues(itemForm);
}
}
}
this.context.SaveChanges()
}
Now it only allows updating the Title and Description fields in the Database in the Form, as well as adding new blocks (BlockWorkingForm) for the form. But it is still necessary to implement the removal of these blocks.
To remove blocks, I need to compare what is in the database and what came in the Update method, but how can this be done?
This this.context.Entry(formDb).CurrentValues.SetValues(form); is where your properties (Title and Description) are set in the DB object. But The list of BlocksWorkingForm is not set (or not set properly).
If you add the BlocksWorkingForms in the form yourself, the insert should work properly.
This should work.
public void UpdateForm(Form form)
{
EditorFormContext context = new EditorFormContext();
var formDb = this.context.Forms.Include(x => x.BlocksWorkingForm).Single(x => x.FormId == form.FormId);
this.context.Entry(formDb).CurrentValues.SetValues(form);
formDb.BlocksWorkingForm = form.BlocksWorkingForm;
this.context.SaveChanges()
}

How do I iteratively assign to a nested list within an object in C#?

I've been asked to replicate a complex xml structure we use within our internal systems, against data retrieved from another system. Unfortunately the xml structure is improvised and we have no specification for it.
I've been mapping out it's structure in C# classes so that I can assign those properties with values from the database and ultimately serialise it as xml.
I've hit a bit of a road block in terms of iteratively adding new list items to a nested list within an object that's already being intialised as a list and being looped through. To make matters more complicated I need to use a value from the iteration to filter down the dataset being used to instantiate the second loop round.
Sorry for the poor explanation - open to rewording... hopefully however the example I've written will demonstrate what I'm trying to do:
using System;
using System.Collections.Generic;
using System.Data;
public class TransactionModel
{
public int Id { get; set; }
public string Description { get; set; }
public DateTime SysDate { get; set; }
public List<TransactionItemModel> Trade { get; set; } = new List<TransactionItemModel>();
}
public class TransactionItemModel
{
public int ItemId { get; set; }
public int TransactionId { get; set; }
public string ItemDescription { get; set; }
public decimal ItemNetAmount { get; set; }
}
public class Program
{
public void Main()
{
DataTable tranResultSet = MethodToReturnResultsFromTranQuery();
DataTable itemResultSet = MethodToReturnResultsFromItemQuery();
var transactions = new List<TransactionModel>();
foreach (DataRow tran in tranResultSet.Rows)
{
transactions.Add(
new TransactionModel() {
Id = (dynamic)tran["Id"],
Description = (dynamic)tran["Description"],
SysDate = (dynamic)tran["SysDate"],
//Trade = <Stuck Here>
// Need to iterate through itemResultSet, adding to TransactionModel.Trade
// where item["TransactionId"] = tran["Id"]
}
);
}
}
}
This approach doesn't set the Trade collection initially, but populates it once you go through the Items. There's likely a lot of optimization that can be added, but this might get you started.
public class TransactionModel
{
public int Id { get; set; }
public string Description { get; set; }
public DateTime? SysDate { get; set; }
public List<TransactionItemModel> Trade { get; set; }
public TransactionModel(DataRow row)
{
if(row == null)
throw new ArgumentNullException(nameof(row));
Id = row.Field<int>("Id");
Description = row.Field<string>("Description");
SysDate = row.Field<DateTime?>("SysDate");
Trade = new List<TransactionItemModel>();
}
}
public class TransactionItemModel
{
public int ItemId { get; set; }
public int TransactionId { get; set; }
public string ItemDescription { get; set; }
public decimal? ItemNetAmount { get; set; }
public TransactionItemModel(DataRow row)
{
if(row == null)
throw new ArgumentNullException(nameof(row));
ItemId = row.Field<int>("Id");
TransactionId = row.Field<int>("TransactionId");
ItemDescription = row.Field<string>("ItemDescription");
ItemNetAmount = row.Field<decimal?>("ItemNetAmount");
}
}
public static class Program
{
private static void Main()
{
DataTable tranResultSet = MethodToReturnResultsFromTranQuery();
DataTable itemResultSet = MethodToReturnResultsFromItemQuery();
var transactions = tranResultSet.AsEnumerable()
.Select(r => new TransactionModel(r));
foreach(TransactionModel transaction in transactions)
{
var items = itemResultSet.AsEnumerable()
.Where(r => r.Field<int>("TransactionId") == transaction.Id)
.Select(r => new TransactionItemModel(r));
transaction.Trade.AddRange(items);
}
}
}
It's likely going to be ideal to query your ItemResultSet based on the current TransactionId instead of grabbing them all up front. You could implement a DataReader, or use Dapper.

EF6 Interceptor to set a value on Insert or Update

I am having troubles trying to figure out how to use the EF6 interceptors to set a value on Insert/Update.
What I wanted to do is to have an interceptor to automatically create a new instance of Audit like so:
public class FooContext : DbContext
{
public DbSet<Invoice> Invoices { get; set; }
public DbSet<Audit> Audits { get; set; }
}
public class Invoice
{
public int Id { get; set; }
public string Name { get; set; }
public Audit AuditAndConcurrencyKey { get; set; }
}
public class InvoiceItem
{
public int Id { get; set; }
public Invoice Header { get; set; }
public string Name { get; set; }
public decimal Price { get; set; }
//For legacy reasons. I know this design is wrong :(
public Audit AuditAndConcurrencyKey { get; set; }
}
public class Audit
{
public int Id { get; set; }
public int InstanceId { get; set; }
public string Message { get; set; }
}
[Test]
public void WillCreateAudit()
{
using (var db = new FooContext())
{
var inv = new Invoice {Name = "Foo Invoice"};
var invLine = new InvoiceItem {Header = inv, Price = 1, Name = "Apple"};
db.Invoices.Add(inv);
db.SaveChanges();
//Inceptors should figure out that we are working with "Invoice" and "InvoiceLine"
//And automatically create an "Audit" instance
Assert.That(inv.AuditAndConcurrencyKey != null);
Assert.That(invLine.AuditAndConcurrencyKey != null);
Assert.That(inv.AuditAndConcurrencyKey == invLine.AuditAndConcurrencyKey)
}
}
The first thing I checked is this example for SoftDeleteInterceptor. I don't think this is what I want because it looks like at the point where we are already generating the expression tree, we are no longer aware of the type of object you are working with.
I checked this example as well, but again, it looks like we are injecting strings instead of setting object references.
Ideally I want something like this:
public class AuditInterceptor
{
public void Intercept(object obj)
{
if (!(obj is Invoice) && !(obj is InvoiceItem))
return; //not type we are looking for, by-pass
//Set the audit here
}
}

Categories