I am building a habit tracking application in Blazor with an ASP.NET Core 6 Web API for backend which uses Entity Framework. I have a Habit
entity which owns many Implementation entities like this:
modelBuilder.Entity<Habit>().OwnsMany(h => h.Implementations);
I am trying to add an implementation to a habit which already has an existing implementation. This is the code within my controller action method:
public async Task<IActionResult> AddHabitReview([FromBody] HabitReviewModel model)
{
try
{
var habit = await _context.Habits.FindAsync(model.Id);
if (habit == null)
return NotFound("No habit found");
habit.AddImplementation(/*implementation parameters*/);
_context.Habits.Update(habit);
await _context.SaveChangesAsync();
return Ok();
}
catch (Exception ex)
{
var message = ex.Message;
return BadRequest(message);
}
}
but I am getting the following exception:
The database operation was expected to affect 1 row(s), but actually affected 0 row(s); data may have been modified or deleted since entities were loaded. See http://go.microsoft.com/fwlink/?LinkId=527962 for information on understanding and handling optimistic concurrency exceptions.
I also checked the DB query via SQL Server profiler. For some reason, it is creating an UPDATE
command for Implementation
table with Id
as my new implementation ID. I expected it to generate a CREATE command for Implementation table, since I was creating a new implementation and attaching it to the habit entity.
Can someone point out what I am doing wrong?
short answer
public class Configuration : IEntityTypeConfiguration<Habit>
{
public void Configure(EntityTypeBuilder<Habit> builder)
{
builder.HasKey(w => w.Id); // maybe
builder.OwnsMany(w => w.Implementations, a =>
{
a.WithOwner().HasForeignKey(w => w.HabitId);
a.Property<int>("Id"); // in case you have not in an entity
a.HasKey("Id");
});
}}
By the way you can do
var habit = await _context.Habits.FindAsync(model.Id);
if (habit == null)
return NotFound("No habit found");
var implementation = new Implementation();
_context.Add(implementation);
habit.Implementations.Add(implementation);
await _context.SaveChangesAsync();
return Ok();
long answer
Owned types need a primary key. If there are no good candidates properties on the .NET type, EF Core can try to create one. However, when owned types are defined through a collection, it isn't enough to just create a shadow property to act as both the foreign key into the owner and the primary key of the owned instance, as we do for OwnsOne: there can be multiple owned type instances for each owner, and hence the key of the owner isn't enough to provide a unique identity for each owned instance.
Look at this Collections of owned types
And you need to add into collection to generate CREATE into database
here you can see how it works DetectChanges honors store-generated key values