Entity Framework Navigation Properties not loading - c#

I'm new to Entity Framework. I'm using the code first option. Below I have a basic model. The database was created properly and the records persist properly but when I run the test method (defined after the model) a second time my header record loads fine, but my navigation property, Details, does not reload. What am I doing wrong?
class Header
{
public int HeaderId { get; set; }
public string Name { get; set; }
public virtual ICollection<Detail> Details { get; set; } = new List<Detail>();
}
class Detail
{
public int DetailId { get; set; }
public string Name { get; set; }
public int HeaderId { get; set; }
[ForeignKey("HeaderId")]
public virtual Header Header { get; set; }
}
class MyContext : DbContext
{
public MyContext() : base("EfTest")
{
this.Configuration.LazyLoadingEnabled = true;
}
public virtual DbSet<Header> Headers { get; set; }
}
private static void DoIt()
{
using (var ctx = new MyContext())
{
var hdr = (
from header in ctx.Headers
where header.Name == NAME
select header).FirstOrDefault();
if (hdr == null)
{
hdr = new Header();
hdr.Name = NAME;
ctx.Headers.Add(hdr);
MessageBox.Show("Header not found; created.");
}
else
{
MessageBox.Show("Header found!");
}
var det = hdr.Details.FirstOrDefault();
if (det == null)
{
det = new Detail() { Name = "Hi" };
hdr.Details.Add(det);
MessageBox.Show("Detail not found; created.");
}
else
{
MessageBox.Show("Detail found!");
}
ctx.SaveChanges();
}
}

Here Entity Framework use Lazy-Loading. In order to get details you should use Eager-Loading.You should use Include method in System.Data.Entity namespace to accomplish Eager-Loading.Change your query like the following.
var hdr = (
from header in ctx.Headers
where header.Name == NAME
select header).Include(h=>h.Details).FirstOrDefault();

Related

Entity Framework Core 6 - many-to-many update

I'm trying to create a update for EF Core 6 many-to-many on SQL Server but I am really confused. I have stock.cs class and location.cs class
public class Stock : BaseModel
{
public Stock()
{
this.Locations = new List<Location>();
}
[Key]
public int Id { get; set; }
public string Name { get; set; } = string.Empty;
public string Guid { get; set; } = string.Empty;
public string RackBarNumber { get; set; } = string.Empty;
public string ShelveNumber { get; set; } = string.Empty;
public string ShelveName { get; set; } = string.Empty;
public virtual List<Location>? Locations { get; set; }
}
public class Location : BaseModel
{
public Location()
{
this.Stocks = new List<Stock>();
}
[Key]
public int Id { get; set; }
public string Name { get; set; } = string.Empty;
public string? Description { get; set; }
public virtual List<Stock>? Stocks { get; set; }
}
I use this as my DTO for getting all my current locations
public class StockLocations
{
public Stock Stock { get; set; }
public virtual ICollection<Location> currentLocations { get; set; }
}
Now the StockController is the piece of code which updates the fields, I am able to create and delete in the StockLocation table that EF Core creates. But when I try many updates at once it just goes haywire.
This is my last attempt:
[HttpPut("{id}")]
public async Task<IActionResult> PutStock(int id, StockLocations stockLocation)
{
await _userService.ConfirmUser(User);
stockLocation.Stock.UpdatedAt = DateTime.Now;
List<Location> removedLocations = new List<Location>();
if (id != stockLocation.Stock.Id)
{
return BadRequest();
}
_context.Entry(stockLocation.Stock).State = EntityState.Modified;
try
{
await _context.SaveChangesAsync();
// Add new items to the database
foreach (var item in stockLocation.Stock.Locations)
{
if (!stockLocation.currentLocations.Any(x => x.Id == item.Id))
{
_context.Entry(item).State = EntityState.Modified;
await _context.SaveChangesAsync();
}
}
// Create a list of removed locations to be removed from the database
foreach (Location location in stockLocation.currentLocations)
{
if (!stockLocation.Stock.Locations.Any(x => x.Id == location.Id))
{
removedLocations.Add(location);
}
}
foreach (var item in removedLocations)
{
/*
Stock stock = _context.Stocks.Include(x => x.Locations).Single(x => x.Id == id);
Location locationToDelete = stock.Locations.Find(x => x.Id == item.Id);
stock.Locations.Remove(locationToDelete);
await _context.SaveChangesAsync();
*/
}
}
catch (DbUpdateConcurrencyException)
{
return NoContent();
}
return NoContent();
}
Anyone who is willing to tell me how I can approach this properly?
Since you need to update StockLocations I will recommend that tyou just pull the record from the database such as:
var record = await _context.StockLocations
.Include(a=>a.Location).Include(a=>a.Stock)
.FirstOrDefaultAsync(a=>a.id == id);
if(record == null) {
thrown new NotFoundException();
}
stockLocation.Stock.UpdatedAt = DateTime.Now;
await _context.SaveChangesAsync();
// other code
// add new location to the db if they don't exist
var locations = stockLocation.Location;
foreach(var loc in locations) {
var findLocation =
_context.Locations.FirstOrDefault(a=>a.Name.ToLower() ==
loc.Name.ToLower()) ;
if(findLocation == null){
// does not exist and can be added
}
}

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?

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
}
}

Is this the way to implement a collection participating in and EF code first many to many relationship?

I have a situation where the code I've arrived at doesn't match any examples I find so I wonder if I'm missing something.
Basically, I want an EF code first Entity that contains a collection of Entities participating in a many-to-many relationship.
Then, I'd like to be able to:
Add to collection at the same time as creating an entity
Not get a warning about accessing a virtual member from constructor
Here's what I have:
public class NotificationUser
{
private ICollection<NotificationUserGroup> _userGroups = new HashSet<NotificationUserGroup>();
public int UserId { get; set; }
public string UserName { get; set; }
public bool IsActive { get; set; }
public virtual ICollection<NotificationUserGroup> UserGroups
{
get { return _userGroups; }
set { _userGroups = value; }
}
}
Is there a better/different way to accomplish my goal?
This example might help
public class NotificationUser
{
public NotificationUser()
{
UserGroups = new HashSet<NotificationUserGroup>();
}
public int NotificationUserId { get; set; }
public string UserName { get; set; }
public bool IsActive { get; set; }
public virtual ICollection<NotificationUserGroup> UserGroups { get; set; }
}
public class NotificationUserGroup
{
public int NotificationUserGroupId { get; set; }
public string GroupName { get; set; }
}
public class Context : DbContext
{
public Context()
: base()
{
}
public DbSet<NotificationUser> NotificationUsers { get; set; }
public DbSet<NotificationUserGroup> NotificationUserGroup { get; set; }
}
class Program
{
static void Main(string[] args)
{
Database.SetInitializer(new DropCreateDatabaseAlways<Context>());
using (var ctx = new Context())
{
var user = new NotificationUser() { UserName = "Name1" };
user.UserGroups.Add(new NotificationUserGroup() { GroupName = "Group1" });
user.UserGroups.Add(new NotificationUserGroup() { GroupName = "Group2" });
ctx.NotificationUsers.Add(user);
ctx.SaveChanges();
}
using (var ctx = new Context())
{
foreach (var user in ctx.NotificationUsers)
{
foreach (var group in user.UserGroups)
Console.WriteLine("Group Id: {0}, Group Name: {1}, UserName: {2}", group.NotificationUserGroupId, group.GroupName,user.UserName);
}
foreach (var group in ctx.NotificationUserGroup)
{
Console.WriteLine("Group Id: {0}, Group Name: {1}", group.NotificationUserGroupId, group.GroupName);
}
}
Console.ReadKey();
}
}

Entity Framework adding too many records

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

Categories