When I try to add a new value to a list, which is a one-to-many relation, the change tracker doesn't recognize the insert. I've already checked some other answers here, but none of them solved my problem. Maybe I'm missing something...
internal sealed class AddCommentHandler(
IPostsDbContext dbContext,
ILogger<AddCommentHandler> logger
) : IRequestHandler<AddCommentCommand>
{
public async Task Handle(AddCommentCommand request, CancellationToken cancellationToken)
{
Post? post = await dbContext.Posts.FindAsync([request.PostId], cancellationToken);
post.AddComment(
request.AuthorId,
request.Content,
request.ParentCommentId
);
await dbContext.SaveChangesAsync(cancellationToken);
logger.LogDebug("Successfully added comment to post {@PostId}", request.PostId);
}
}
When calling SaveChangesAsync, I get this error:
Microsoft.EntityFrameworkCore.DbUpdateConcurrencyException: '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 https://go.microsoft.com/fwlink/?LinkId=527962 for information on understanding and handling optimistic concurrency exceptions.'
Post entity:
public sealed class Post : EntityHasDomainEvents, IAuditableEntity
{
public Guid Id { get; }
public Guid AuthorId { get; private set; }
public string Content { get; private set; }
public DateTime CreatedAtUtc { get; set; }
public DateTime UpdatedAtUtc { get; set; }
private readonly List<Comment> _comments = [];
public IReadOnlyCollection<Comment> Comments => _comments;
public Author Author { get; private set; }
private Post() { }
public Post(Guid authorId, string content)
{
Id = Guid.NewGuid();
AuthorId = authorId;
Content = content;
Raise(new PostCreatedDomainEvent(Id));
}
public void AddComment(Guid authorId, string content, Guid? parentCommentId)
{
var comment = new Comment(Id, authorId, content, parentCommentId);
_comments.Add(comment);
}
}
Comment entity:
public sealed class Comment : IAuditableEntity
{
public Guid Id { get; }
public Guid PostId { get; private set; }
public Guid AuthorId { get; private set; }
public string Content { get; private set; }
public Guid? ParentCommentId { get; private set; }
public DateTime CreatedAtUtc { get; set; }
public DateTime UpdatedAtUtc { get; set; }
public Author Author { get; private set; }
public Post Post { get; private set; }
private Comment() { }
public Comment(Guid postId, Guid authorId, string content, Guid? parentCommentId = null)
{
Id = Guid.NewGuid();
PostId = postId;
AuthorId = authorId;
Content = content;
ParentCommentId = parentCommentId;
}
}
Entity config:
internal sealed class PostConfiguration : IEntityTypeConfiguration<Post>
{
public void Configure(EntityTypeBuilder<Post> builder)
{
builder.HasKey(p => p.Id);
builder.Property(p => p.Content)
.HasMaxLength(515);
builder.HasMany(p => p.Comments)
.WithOne(c => c.Post)
.HasForeignKey(c => c.PostId)
.OnDelete(DeleteBehavior.Cascade);
}
}
internal sealed class CommentConfiguration : IEntityTypeConfiguration<Comment>
{
public void Configure(EntityTypeBuilder<Comment> builder)
{
builder.HasKey(c => c.Id);
builder.Property(c => c.Content)
.IsRequired()
.HasMaxLength(500);
builder.HasOne(c => c.Post)
.WithMany(p => p.Comments)
.HasForeignKey(c => c.PostId)
.OnDelete(DeleteBehavior.Cascade);
builder.HasIndex(c => c.PostId);
builder.HasIndex(c => c.ParentCommentId);
}
}
Commentsinto read-only collection? Try without DDD tricks and then try to make it suitable with your classes. Also do not useFindin such case, you should specify that Comments should be loaded:var post = await dbContext.Posts.Include(p => p.Comments).FirstOrDefaultAsync(p => p.PostId == request.PostId, cancellationToken);