C
C#3mo ago
Saiyanslayer

✅ Entity Framework One To Many: Adding to the dbContext and getting value '{Id: -2147482644}'

public async Task<Patient?> AddPatientAsync(Patient? patient) {
//validation
if (patient is null) { //no patient info
throw new NotImplementedException("Patient information is missing");
}

if (patient.Id < 0) {
throw new NotImplementedException("Patient already exists");
}

var result = await QueryOnContext(async context => {
var model = patient.ToModel();
await context.CheckGroups.AddRangeAsync(model.CheckGroups);


await context.Patients.AddAsync(model);
await context.SaveChangesAsync();
return await context.Patients.FirstOrDefaultAsync(p => p.Identifier == patient.Identifier && p.Name == patient.Name);
});

if (result is null) return null;
else return new Patient(result);
}
public async Task<Patient?> AddPatientAsync(Patient? patient) {
//validation
if (patient is null) { //no patient info
throw new NotImplementedException("Patient information is missing");
}

if (patient.Id < 0) {
throw new NotImplementedException("Patient already exists");
}

var result = await QueryOnContext(async context => {
var model = patient.ToModel();
await context.CheckGroups.AddRangeAsync(model.CheckGroups);


await context.Patients.AddAsync(model);
await context.SaveChangesAsync();
return await context.Patients.FirstOrDefaultAsync(p => p.Identifier == patient.Identifier && p.Name == patient.Name);
});

if (result is null) return null;
else return new Patient(result);
}
54 Replies
Saiyanslayer
Saiyanslayer3mo ago
Error is: System.InvalidOperationException: 'The instance of entity type 'CheckGroupModel' cannot be tracked because another instance with the key value '{Id: -2147482644}' is already being tracked. When attaching existing entities, ensure that only one entity instance with a given key value is attached.' error occurs on "await context.Patients.AddAsync(model);"
Angius
Angius3mo ago
Don't use AddAsync() first of all Or AddRangeAsync()
Saiyanslayer
Saiyanslayer3mo ago
Patient model:
public class PatientModel {
public int Id { get; set; }
public string Name { get; set; }
public string Identifier { get; set; }
public DateTime WhenCreated { get; set; } = DateTime.UtcNow;
public DateTime? WhenStarted { get; set; }
public bool IsArchived { get; set; }
public DateTime? WhenArchived { get; set; }
public IList<CheckGroupModel> CheckGroups { get; set; } = new List<CheckGroupModel>();


}
public class PatientModel {
public int Id { get; set; }
public string Name { get; set; }
public string Identifier { get; set; }
public DateTime WhenCreated { get; set; } = DateTime.UtcNow;
public DateTime? WhenStarted { get; set; }
public bool IsArchived { get; set; }
public DateTime? WhenArchived { get; set; }
public IList<CheckGroupModel> CheckGroups { get; set; } = new List<CheckGroupModel>();


}
Angius
Angius3mo ago
Second, what's that weird... QueryOnContext method?
Saiyanslayer
Saiyanslayer3mo ago
private async Task<TResult> QueryOnContext<TResult>(Func<ApplicationDbContext, Task<TResult>> query) {
using var context = await _contextFactory.CreateDbContextAsync();
return await query.Invoke(context);
}
private async Task<TResult> QueryOnContext<TResult>(Func<ApplicationDbContext, Task<TResult>> query) {
using var context = await _contextFactory.CreateDbContextAsync();
return await query.Invoke(context);
}
Angius
Angius3mo ago
Why
Saiyanslayer
Saiyanslayer3mo ago
the tutorial i followed used it and it did well for me. I was going to review it once i knew more about what im doing all the calls in the service use it.
Angius
Angius3mo ago
It's just a completely useless method But aight
Saiyanslayer
Saiyanslayer3mo ago
lol, that doesnt surprise me
Angius
Angius3mo ago
Replace your .AddAsync() with .Add()
Saiyanslayer
Saiyanslayer3mo ago
the guy also used AddAsync everywhere
Angius
Angius3mo ago
And .AddRangeAsync() with .AddRange() You should just throw away this tutorial, then It shoes the guy has absolutely zero clue about EF Just a "monke see async monke use async"
Saiyanslayer
Saiyanslayer3mo ago
should I need to add the range? I added it part of troubleshooting
Angius
Angius3mo ago
Idk, do you want to add multiple records to the database at once?
Saiyanslayer
Saiyanslayer3mo ago
the PatientModel has another model within it and I'm not sure if I can add it that way or if i have to make the nested model seperately
public class CheckGroupModel {
public int Id { get; set; }
public int SortOrder { get; set; }
public string Label { get; set; } = string.Empty;
public string Colour { get; set; } = string.Empty;
public bool IsHidden { get; set; }

//references
public PatientModel? Patient { get; set; }
public IList<CheckModel> Checks { get; set; } = new List<CheckModel>();

}
public class CheckGroupModel {
public int Id { get; set; }
public int SortOrder { get; set; }
public string Label { get; set; } = string.Empty;
public string Colour { get; set; } = string.Empty;
public bool IsHidden { get; set; }

//references
public PatientModel? Patient { get; set; }
public IList<CheckModel> Checks { get; set; } = new List<CheckModel>();

}
PatientModel has a list of these
Angius
Angius3mo ago
Two ways you can add a new CheckGroup to a Patient:
var patient = await _context.Patients.FindAsync(id);
patient.CheckGroups.Add(checkGroup);
await _context.SaveChangesAsync();
var patient = await _context.Patients.FindAsync(id);
patient.CheckGroups.Add(checkGroup);
await _context.SaveChangesAsync();
_context.CheckGroups.Add(new CheckGroup {
// ...
PatientId = id
});
await _context.SaveChangesAsync();
_context.CheckGroups.Add(new CheckGroup {
// ...
PatientId = id
});
await _context.SaveChangesAsync();
Saiyanslayer
Saiyanslayer3mo ago
the top on would require a SaveASync too? SaveChangesAsync*
Angius
Angius3mo ago
Yes, good catch
Saiyanslayer
Saiyanslayer3mo ago
great, thank you I've covered the documentation, but do you have a good reference or guide for EF?
Angius
Angius3mo ago
The documentation ¯\_(ツ)_/¯
Saiyanslayer
Saiyanslayer3mo ago
it's easy to find something out there that works, but I'm finding it hard to find something that doesn't make other recoil when you mention it's your guide
Angius
Angius3mo ago
https://www.learnentityframeworkcore.com/ is sometimes useful as well
Saiyanslayer
Saiyanslayer3mo ago
ok great, that's been one of my resources awesome thank you for the help!
Angius
Angius3mo ago
Anytime :Ok:
Saiyanslayer
Saiyanslayer3mo ago
quick followup: if i have a new patient and want an id, I must: add patient to context and save it to generate that Id? Is that the standard approach?
Angius
Angius3mo ago
Yes, the database generates IDs But you can create a whole hierarchy first, and all the IDs will be set up For example
var post = new Blogpost {
// ...
Tags = [ new Tag { ... }, new Tag { ... }]
};
_ctx.Posts.Add(post);
await _ctx.SaveChangesAsync();
var post = new Blogpost {
// ...
Tags = [ new Tag { ... }, new Tag { ... }]
};
_ctx.Posts.Add(post);
await _ctx.SaveChangesAsync();
will create the blogpost, create both of the tags, and will set the ID in those tags properly
post.Id // 1
post.Tags[0].PostId // 1
post.Tags[1].PostId // 1
post.Id // 1
post.Tags[0].PostId // 1
post.Tags[1].PostId // 1
Saiyanslayer
Saiyanslayer3mo ago
sweet, thanks! wierd, still getting that error
public async Task<Patient?> AddPatientAsync(Patient? patient) {
//validation
if (patient is null) { //no patient info
throw new NotImplementedException("Patient information is missing");
}

if (patient.Id < 0) {
throw new NotImplementedException("Patient already exists");
}

var result = await QueryOnContext(async context => {
//add patient
var newPatient = new PatientModel();
newPatient.Name = patient.Name;
newPatient.Identifier = patient.Identifier;
newPatient.WhenCreated = DateTime.UtcNow;
newPatient.WhenStarted = patient.WhenStarted;

//adding CheckGroupModels
newPatient.CheckGroups = new List<CheckGroupModel>();
foreach (var group in patient.CheckGroups) {
var newGroup = group.ToModel();
newGroup.Id = 0;
newGroup.Patient = newPatient;
newPatient.CheckGroups.Add(newGroup);
}

context.Patients.Add(newPatient);
await context.SaveChangesAsync();
return newPatient;
});

if (result is null) return null;
else return new Patient(result);
}
public async Task<Patient?> AddPatientAsync(Patient? patient) {
//validation
if (patient is null) { //no patient info
throw new NotImplementedException("Patient information is missing");
}

if (patient.Id < 0) {
throw new NotImplementedException("Patient already exists");
}

var result = await QueryOnContext(async context => {
//add patient
var newPatient = new PatientModel();
newPatient.Name = patient.Name;
newPatient.Identifier = patient.Identifier;
newPatient.WhenCreated = DateTime.UtcNow;
newPatient.WhenStarted = patient.WhenStarted;

//adding CheckGroupModels
newPatient.CheckGroups = new List<CheckGroupModel>();
foreach (var group in patient.CheckGroups) {
var newGroup = group.ToModel();
newGroup.Id = 0;
newGroup.Patient = newPatient;
newPatient.CheckGroups.Add(newGroup);
}

context.Patients.Add(newPatient);
await context.SaveChangesAsync();
return newPatient;
});

if (result is null) return null;
else return new Patient(result);
}
happens on "context.Patients.Add(newPatient);"
Angius
Angius3mo ago
Why are you setting IDs manually?
No description
Saiyanslayer
Saiyanslayer3mo ago
crap, part of my troubleshooting snuck in there issue remains without it
Angius
Angius3mo ago
Okay, another thought is that the issue might be elsewhere Something also deals with those groups, patients, whatever, and doesn't release them properly Immediate suspects: * any async void method * any unawaited async method call
Saiyanslayer
Saiyanslayer3mo ago
hrm, I know enough to know to completely avoid async voids would those things cause a "Id: -2147482644"? That feels like a int 0 goign back once
MODiX
MODiX3mo ago
Angius
REPL Result: Success
int.MinValue
int.MinValue
Result: int
-2147483648
-2147483648
Compile: 174.163ms | Execution: 15.770ms | React with ❌ to remove this embed.
Angius
Angius3mo ago
Close, but not quite
Saiyanslayer
Saiyanslayer3mo ago
this is being called from a seeding service in the main program. the main program is async task. It appears to be async/await the whole path
public async Task<Patient?> AddPatientAsync(Patient? patient) {
//validation
if (patient is null) { //no patient info
throw new NotImplementedException("Patient information is missing");
}

if (patient.Id < 0) {
throw new NotImplementedException("Patient already exists");
}

var newPatient = new PatientModel();
newPatient.Name = patient.Name;
newPatient.Identifier = patient.Identifier;
newPatient.WhenCreated = DateTime.UtcNow;
newPatient.WhenStarted = patient.WhenStarted;

//adding CheckGroupModels
newPatient.CheckGroups = new List<CheckGroupModel>();
foreach (var group in patient.CheckGroups) {
var newGroup = new CheckGroupModel();
newGroup.Label = group.Label;
newGroup.Colour = group.Colour;
newGroup.Patient = newPatient;
newPatient.CheckGroups.Add(newGroup);
}

_context.Patients.Add(newPatient);
await _context.SaveChangesAsync();
return new Patient(newPatient);

}
public async Task<Patient?> AddPatientAsync(Patient? patient) {
//validation
if (patient is null) { //no patient info
throw new NotImplementedException("Patient information is missing");
}

if (patient.Id < 0) {
throw new NotImplementedException("Patient already exists");
}

var newPatient = new PatientModel();
newPatient.Name = patient.Name;
newPatient.Identifier = patient.Identifier;
newPatient.WhenCreated = DateTime.UtcNow;
newPatient.WhenStarted = patient.WhenStarted;

//adding CheckGroupModels
newPatient.CheckGroups = new List<CheckGroupModel>();
foreach (var group in patient.CheckGroups) {
var newGroup = new CheckGroupModel();
newGroup.Label = group.Label;
newGroup.Colour = group.Colour;
newGroup.Patient = newPatient;
newPatient.CheckGroups.Add(newGroup);
}

_context.Patients.Add(newPatient);
await _context.SaveChangesAsync();
return new Patient(newPatient);

}
took it out of the QueryOnExecute thing and same result as well ok, I think you're right about that async thing, something weird's happening. My breakpoints for this whole method aren't being triggered anymore, but the patient is being created. and this si the only place that could be happening
public async Task<Patient?> AddPatientAsync(Patient? patient) {
//validation
if (patient is null) { //no patient info
throw new NotImplementedException("Patient information is missing");
}

if (patient.Id < 0) {
throw new NotImplementedException("Patient already exists");
}

var result = await QueryOnContext(async context => {
var newPatient = new PatientModel();
newPatient.Name = patient.Name;
newPatient.Identifier = patient.Identifier;
newPatient.WhenCreated = DateTime.UtcNow;
newPatient.WhenStarted = patient.WhenStarted;

context.Patients.Add(newPatient);
await context.SaveChangesAsync();

//adding CheckGroupModels
newPatient.CheckGroups = new List<CheckGroupModel>();
foreach (var group in patient.CheckGroups) {
var newGroup = new CheckGroupModel();
newGroup.Label = group.Label;
newGroup.Colour = group.Colour;
newGroup.Patient = newPatient;
newPatient.CheckGroups.Add(newGroup);
await context.SaveChangesAsync();
}

return newPatient;
});
public async Task<Patient?> AddPatientAsync(Patient? patient) {
//validation
if (patient is null) { //no patient info
throw new NotImplementedException("Patient information is missing");
}

if (patient.Id < 0) {
throw new NotImplementedException("Patient already exists");
}

var result = await QueryOnContext(async context => {
var newPatient = new PatientModel();
newPatient.Name = patient.Name;
newPatient.Identifier = patient.Identifier;
newPatient.WhenCreated = DateTime.UtcNow;
newPatient.WhenStarted = patient.WhenStarted;

context.Patients.Add(newPatient);
await context.SaveChangesAsync();

//adding CheckGroupModels
newPatient.CheckGroups = new List<CheckGroupModel>();
foreach (var group in patient.CheckGroups) {
var newGroup = new CheckGroupModel();
newGroup.Label = group.Label;
newGroup.Colour = group.Colour;
newGroup.Patient = newPatient;
newPatient.CheckGroups.Add(newGroup);
await context.SaveChangesAsync();
}

return newPatient;
});
ok, it's been refactored to save changes after each entity is made. barbaric, but it seemed like the right choice to see whats going on the error is occurring when the second CheckGroupModel is added. Maybe something is screwed up with the EF modelBuilder? that, or calling await in the main Program.cs causes some funky things It's not this. I tossed the Seeding to be called after done with the startup and it still happens banging my head against this issue, the best i can see is that it's got to do with generating the PK for CheckGroupModel. I can make PatientModel entities just fine. I tried adding the patient id directly, doesn't work. It doesn't generate the foriegn key properly then or something I've moved where this code cocurs, issue still remains occurs*
leowest
leowest3mo ago
what db you're using
Saiyanslayer
Saiyanslayer3mo ago
Sqlite Wait, could the dB cause issues with creating pk and fk?
leowest
leowest3mo ago
well i've seen an issue before that was similar to yours where the db was filled with invalid data like null columns etc including in the primary key field causing it to not respect anything being inserted u could use dbeaver or dbrowser and check the sqlite file and see if its fine or not
Saiyanslayer
Saiyanslayer3mo ago
There's no data yet. This error occurs when trying to use the service to insert
leowest
leowest3mo ago
if possible can you create a github of the project? I will take a look when im free
Saiyanslayer
Saiyanslayer3mo ago
Ya, it's currently private atm. I'll share it in a bit
Saiyanslayer
Saiyanslayer3mo ago
GitHub
GitHub - nsmela/BlazorPatients
Contribute to nsmela/BlazorPatients development by creating an account on GitHub.
leowest
leowest3mo ago
mmm Shouldn't the fk be on PatientModelId?
modelBuilder.Entity<PatientModel>()
.HasMany(p => p.CheckGroups)
.WithOne(g => g.Patient)
.HasForeignKey(p => p.Id)
.OnDelete(DeleteBehavior.Cascade);
modelBuilder.Entity<PatientModel>()
.HasMany(p => p.CheckGroups)
.WithOne(g => g.Patient)
.HasForeignKey(p => p.Id)
.OnDelete(DeleteBehavior.Cascade);
Saiyanslayer
Saiyanslayer3mo ago
ill check no i think it's right. PatientModel is using the ChckGroupModel.Id as the foerign key if i change it, i get foriegn key constraint issues wait, let me double-check it I think that was it! I completely removed that model config and everything is workign fine now so far anyways so .HasForeignKey is setting the fk for the child and not the parent? I noticed the generated code for migration refused to house the foriegn key on the parent nvm, hasFK is to set the key on the current item. That's why i got confused ok, triple-checked and that seemed to be the fix! Thank you so much!
leowest
leowest3mo ago
but looking at your code I dont think u want one to many there otherwise u will have duplicated checkgroups
modelBuilder.Entity<PatientModel>()
.HasMany(p => p.CheckGroups)
.WithMany(y => y.Patients)
.UsingEntity<PatientCheckGroupModel>();
modelBuilder.Entity<PatientModel>()
.HasMany(p => p.CheckGroups)
.WithMany(y => y.Patients)
.UsingEntity<PatientCheckGroupModel>();
and for that you would need to change
public class CheckGroupModel {
public int Id { get; set; }
public int SortOrder { get; set; }
public string Label { get; set; } = string.Empty;
public string Colour { get; set; } = string.Empty;
public bool IsHidden { get; set; }

//references
public IList<CheckModel> Checks { get; set; } = new List<CheckModel>();
public IList<PatientCheckGroupModel> PatientGroups { get; set; } = new List<PatientCheckGroupModel>();
public IList<PatientModel> Patients { get; set; } = new List<PatientModel>();
}

public class PatientModel {
public int Id { get; set; }
public string Name { get; set; }
public string Identifier { get; set; }
public DateTime WhenCreated { get; set; } = DateTime.UtcNow;
public DateTime? WhenStarted { get; set; }
public bool IsArchived { get; set; }
public DateTime? WhenArchived { get; set; }

//references
public IList<PatientCheckGroupModel> PatientGroups { get; set; } = new List<PatientCheckGroupModel>();
public IList<CheckGroupModel> CheckGroups { get; set; } = new List<CheckGroupModel>();
}

public class PatientCheckGroupModel
{
public int PatientId { get; set; }
public int CheckGroupId { get; set; }
public PatientModel Patient { get; set; } = null!;
public CheckGroupModel CheckGroup { get; set; } = null!;
}
public class CheckGroupModel {
public int Id { get; set; }
public int SortOrder { get; set; }
public string Label { get; set; } = string.Empty;
public string Colour { get; set; } = string.Empty;
public bool IsHidden { get; set; }

//references
public IList<CheckModel> Checks { get; set; } = new List<CheckModel>();
public IList<PatientCheckGroupModel> PatientGroups { get; set; } = new List<PatientCheckGroupModel>();
public IList<PatientModel> Patients { get; set; } = new List<PatientModel>();
}

public class PatientModel {
public int Id { get; set; }
public string Name { get; set; }
public string Identifier { get; set; }
public DateTime WhenCreated { get; set; } = DateTime.UtcNow;
public DateTime? WhenStarted { get; set; }
public bool IsArchived { get; set; }
public DateTime? WhenArchived { get; set; }

//references
public IList<PatientCheckGroupModel> PatientGroups { get; set; } = new List<PatientCheckGroupModel>();
public IList<CheckGroupModel> CheckGroups { get; set; } = new List<CheckGroupModel>();
}

public class PatientCheckGroupModel
{
public int PatientId { get; set; }
public int CheckGroupId { get; set; }
public PatientModel Patient { get; set; } = null!;
public CheckGroupModel CheckGroup { get; set; } = null!;
}
and then the process of adding seeds would need to change a bit.
using (var context = _contextFactory.CreateDbContext())
{
if (patient is null) throw new InvalidDataException("null patient...");

var newPatient = new PatientModel
{
Name = patient.Name,
Identifier = patient.Identifier,
WhenCreated = patient.WhenCreated,
WhenStarted = patient.WhenStarted,
WhenArchived = patient.WhenArchived,
IsArchived = patient.IsArchived
};

foreach (var check in patient.CheckGroups)
{
var found = await context.CheckGroups.FirstOrDefaultAsync(x => check.Label == x.Label);
if (found is null)
{
var saveCheck = new CheckGroupModel { Label = check.Label };

context.CheckGroups.Add(saveCheck);
await context.SaveChangesAsync();
newPatient.CheckGroups.Add(saveCheck);
}
else
{
newPatient.CheckGroups.Add(found);
}
}

context.Patients.Add(newPatient);
await context.SaveChangesAsync();
return new Patient(newPatient);
}
using (var context = _contextFactory.CreateDbContext())
{
if (patient is null) throw new InvalidDataException("null patient...");

var newPatient = new PatientModel
{
Name = patient.Name,
Identifier = patient.Identifier,
WhenCreated = patient.WhenCreated,
WhenStarted = patient.WhenStarted,
WhenArchived = patient.WhenArchived,
IsArchived = patient.IsArchived
};

foreach (var check in patient.CheckGroups)
{
var found = await context.CheckGroups.FirstOrDefaultAsync(x => check.Label == x.Label);
if (found is null)
{
var saveCheck = new CheckGroupModel { Label = check.Label };

context.CheckGroups.Add(saveCheck);
await context.SaveChangesAsync();
newPatient.CheckGroups.Add(saveCheck);
}
else
{
newPatient.CheckGroups.Add(found);
}
}

context.Patients.Add(newPatient);
await context.SaveChangesAsync();
return new Patient(newPatient);
}
u want to ensure you attribute the id if a check group exist as to not create another redundency and what happens in all this is that you a many to many with a intermediate table and navigation
leowest
leowest3mo ago
No description
No description
No description
Angius
Angius3mo ago
Fetching labels in a loop feels iffy, but if it's just for seeding then it should be fine
leowest
leowest3mo ago
yeah its just for the seeding I couldn't see a way around it as groups are not attached and if I just create a new checkgroupmodel they collide
Saiyanslayer
Saiyanslayer3mo ago
I appreciate the feedback, but I think I want the groups seperate/duplicated. I don't want patients sharing the because the groups are just collections of checks/tasks for each patient Basically, each patient is like a Kan an task board Kanban* Groups and checks can dynamically be added and removed and have no link to what other patients have I debated on even having groups, but it felt better than having a couple of extra fields in the patient or checks Unless I'm misunderstanding what you're recommending For the seeding, I'm considering just incorporating it into the migrations. I've been using the patient service to test run it
leowest
leowest3mo ago
they are separated but what I meant is
Standard Checks
Physics Checks
Chemo Checks
Standard Checks
Physics Checks
Chemo Checks
everytime u would be adding a new item but if u have many to many all the checks above only exists once but in the joined table u have the correlation of patient to groupcheck so you can have a table with all kind of checks that exist and easily link it to patients this would also facilitate if u have a dropdown with checks u want to attribute to a patient but yeah either u do your thing and u have both examples 😉
Saiyanslayer
Saiyanslayer3mo ago
Hrm, I'll have to think about it. The request was that you could have two "Standard Checks" groups that could have entirely different checks and colored background. There will be a template version (reason why I had the navigation value as nullable)
leowest
leowest3mo ago
yeah I dont know your requirements etc, but at least u have both examples and know how to do either now
MODiX
MODiX3mo ago
Use the /close command to mark a forum thread as answered
Saiyanslayer
Saiyanslayer3mo ago
Ya, but nonetheless it's appreciated.