Using Blazor server-side UI with Entity Framework Core.
I have a Student
table. One student can have multiple tests assigned, each test has test subject type which is FK to another lookup table.
public class Student
{
public int Id { get; set;}
public virtual ICollection<TestDetail > TestDetails{ get; set; } = new List< TestDetail >();
}
public class TestDetail
{
public int StudentId { get; set; }
public virtual Student Student { get; set; } = null!;
public int? SubjectTypeId { get; set; }
public virtual SubjectType? SubjectType { get; set; } = null!;
}
public class TestDetailConfiguration
{
entity.Property(e => e. SubjectTypeId).HasColumnName("SUBJECT_TYPE_CD");
entity.HasOne(d => d. SubjectType).WithMany()
.HasForeignKey(d => d.TestSubjectTypeId)
.HasConstraintName("FK_NAME_TO_SUBJECT_TABLE");
entity.HasOne(d => d.Student).WithMany(p => p. TestDetails)
.HasForeignKey(d => d.StudentId)
.OnDelete(DeleteBehavior.ClientCascade)
.HasConstraintName("FK_T_...");
}
In the UI, user can add multiple tests to a student at one time. Subject can be picked from a dropdown which populated with list of subjects from the Subject
table.
Clicking on the AddTest
button creates an object of type TestDetail
and SubjectType
is assigned with the test subject.
Populate the dropdown by querying the data context from a service class where factory is injected
public List<SubjectType> GetSubjects()
{
using (var _context = factory.CreateDbContext())
{
var rows = _context.Set<Subject>();
return rows;
}
}
The UI page has:
[Inject]
private IDbContextFactory<DBContext> factory { get; set; }
private DBContext _dbContext;
private IEnumerable<SubjectType> subjecteTypes = [];
private void Onintialized()
{
_dbContext = factory.CreateDbContext();
subjectTypes = Service.GetSubjects();
Student student = new Student();
_dbContext.Students.Add(student);
}
On add test button click, creates a TestDetail
object and adds to the collection.
student.TestDetails.Add(new TestDetail());
Then later its SubjectType
is populated with the selected subject from the dropdown with values "1 – English", "2 – Science".
When I add two tests with the same subject "1 - English", Entity Framework Core always throwing this exception on save:
The instance of entity type cannot be tracked because another instance of this type with the same key {1} is already being tracked.
I tried using .AsNoTracking()
while getting the Subject
list, but that didn't work either.
Thanks
Using Blazor server-side UI with Entity Framework Core.
I have a Student
table. One student can have multiple tests assigned, each test has test subject type which is FK to another lookup table.
public class Student
{
public int Id { get; set;}
public virtual ICollection<TestDetail > TestDetails{ get; set; } = new List< TestDetail >();
}
public class TestDetail
{
public int StudentId { get; set; }
public virtual Student Student { get; set; } = null!;
public int? SubjectTypeId { get; set; }
public virtual SubjectType? SubjectType { get; set; } = null!;
}
public class TestDetailConfiguration
{
entity.Property(e => e. SubjectTypeId).HasColumnName("SUBJECT_TYPE_CD");
entity.HasOne(d => d. SubjectType).WithMany()
.HasForeignKey(d => d.TestSubjectTypeId)
.HasConstraintName("FK_NAME_TO_SUBJECT_TABLE");
entity.HasOne(d => d.Student).WithMany(p => p. TestDetails)
.HasForeignKey(d => d.StudentId)
.OnDelete(DeleteBehavior.ClientCascade)
.HasConstraintName("FK_T_...");
}
In the UI, user can add multiple tests to a student at one time. Subject can be picked from a dropdown which populated with list of subjects from the Subject
table.
Clicking on the AddTest
button creates an object of type TestDetail
and SubjectType
is assigned with the test subject.
Populate the dropdown by querying the data context from a service class where factory is injected
public List<SubjectType> GetSubjects()
{
using (var _context = factory.CreateDbContext())
{
var rows = _context.Set<Subject>();
return rows;
}
}
The UI page has:
[Inject]
private IDbContextFactory<DBContext> factory { get; set; }
private DBContext _dbContext;
private IEnumerable<SubjectType> subjecteTypes = [];
private void Onintialized()
{
_dbContext = factory.CreateDbContext();
subjectTypes = Service.GetSubjects();
Student student = new Student();
_dbContext.Students.Add(student);
}
On add test button click, creates a TestDetail
object and adds to the collection.
student.TestDetails.Add(new TestDetail());
Then later its SubjectType
is populated with the selected subject from the dropdown with values "1 – English", "2 – Science".
When I add two tests with the same subject "1 - English", Entity Framework Core always throwing this exception on save:
The instance of entity type cannot be tracked because another instance of this type with the same key {1} is already being tracked.
I tried using .AsNoTracking()
while getting the Subject
list, but that didn't work either.
Thanks
Share Improve this question edited Mar 6 at 18:00 marc_s 757k184 gold badges1.4k silver badges1.5k bronze badges asked Mar 6 at 14:06 blue pinkblue pink 413 bronze badges 4 |1 Answer
Reset to default 1TL;DR
Don't use Drop Down Lists to link entity object references, just use them as Id lookups and only assign the ForeignKey fields via drop downs.
.AsNoTracking()
in this case further compounds your issues. If you are using a common context for the page life time, and you want to make changes back to any records that have 1:N links to reference tables, then you really want to enable tracking.
You want tracking so that EF knows that the selected SubjectType
was already loaded from the database.
.AsNoTracking()
is actually irrelevant here, your GetSubjects()
is a different instance of the DbContext that gets disposed anyway, so it isn't being tracked anywhere, that is why it didn't make a difference.
The problem with this is how EF manages entity creation. student
is a new record in your _dbContext
but it has not yet been persisted to the database. When you link related entities to the student
and EF isn't tracking those related entities in the DbContext that is executing the save then EF will try to attach them first if you have ForeignKey fields in the model.
The error you are seeing suggests key fields are in your model and you are using generated keys, that is a good start ;) but it also suggests that due to your code implementation, the same Subject
entities that are linked to multiple records are not the same object reference. Either some level of cloning or serialization has occurred or the GetSubjects()
has been called multiple times for the lifetime of the student
record. EF does try to prevent this happening when the records are attached, but it's not foolproof
- If you are not using ForeignKey fields, then EF assumes that the entire graph needs to be created, so the main student record and any child nodes in the object graph that haven't been tracked by the context will also be created.
You may be able to work around this by manually Attaching the selected Subject
entity to the _dbContext
and checking that all the other references to the same Subject
are also attached and all have the tracking state to Unchanged
but at that point it might have been easier for GetSubjects()
to use the same _dbContext
as the form (and not dispose it!)
- usage of a common
DbContext
is generally regarded as an anti-pattern because it leads to inefficiencies and code that gets complicated to maintain. Even having a common_dbContext
at the form level is just waiting for problems exactly like this. So I don't recommend it for commercial software, but it might be a simple workaround that you can accept for now.
A better design is to use expose your ForeignKey fields as properties in your model (if you are not already) and to set these ForeignKey fields from the dropdowns and not to assign the whole object. In this configuration you would load the GetSubjects()
once, perhaps for the lifetime of the application, or you could cache and reuse the results. Then you bind the Key field from the drop down list selection to the student
record.
- This is generally the concept that dropdown lists were originally designed to support, long before EF
_dbContext = factory.CreateDbContext()
- it is wrong.DbContext
should be allocated and disposed within method, so no assigning in constructor. – Svyatoslav Danyliv Commented Mar 6 at 19:41