1

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);
    }
}
2
  • 2
    How EF Core should load Comments into read-only collection? Try without DDD tricks and then try to make it suitable with your classes. Also do not use Find 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); Commented Mar 13 at 13:35
  • Isn't the config file enough to let efcore know about the Comments prop? Also, I tried adding your code but it still doesn't work... :/ Commented Mar 13 at 13:46

3 Answers 3

1

A 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()

Sign up to request clarification or add additional context in comments.

Comments

0

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.

Comments

0

I figured it out by reading this microsoft page:
https://docs.microsoft.com/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);
    }
}

1 Comment

Yes, if you generate IDs yourself, by default EF will treat it as existing entity. You could also fix that by removing ID initialization

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.