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 /?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);
}
}
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/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);
}
}
Share
Improve this question
edited Mar 13 at 14:09
marc_s
756k184 gold badges1.4k silver badges1.5k bronze badges
asked Mar 13 at 13:20
Lucas ConstantinoLucas Constantino
254 bronze badges
2
|
3 Answers
Reset to default 1A DbUpdateConcurrencyException
can occur if EF has already made changes before with the SAME DB CONTEXT. So, the entity can be deleted, updated on the database.
You can retry with another dbContext or use:
// Clean the ChangeTracker to avoid tracking
dbContext.ChangeTracker.Clear();
before adding your entity and SaveChanges()
EF Core supports encapsulating fields with property, but the documentation makes use of a full property instead of a computed property. See if it works if you use a full property.
private readonly List<Comment> _comments = [];
public IReadOnlyCollection<Comment> Comments
{
get => _comments;
set => _comments = value;
}
(Adjust the setter as needed, make it private or remove it to disallow assigning a new collection, but I suggest first testing if it works with a setter included).
This property-field relationship should be discovered by naming convention described in the linked article, but in any case you can also configure it explicitly with .Property(b => b.Comments).HasField("_comments");
in your entity configuration.
I figured it out by reading this microsoft page:
https://docs.microsoft/en-us/ef/core/what-is-new/ef-core-3.0/breaking-changes#detectchanges-honors-store-generated-key-values
After I added `ValueGeneratedNever()`, it started working normally
By setting the Id property as ValueGeneratedNever, you are telling EF Core that the key value is manually generated and not by the database. This prevents EF Core from confusing the new Comment with an existing entity.
internal sealed class CommentConfiguration : IEntityTypeConfiguration<Comment>
{
public void Configure(EntityTypeBuilder<Comment> builder)
{
builder.HasKey(c => c.Id);
builder.Property(c => c.Id)
.ValueGeneratedNever(); // new line added
builder.Property(c => c.Content)
.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);
}
}
Comments
into read-only collection? Try without DDD tricks and then try to make it suitable with your classes. Also do not useFind
in 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);
– Svyatoslav Danyliv Commented Mar 13 at 13:35