最新消息:雨落星辰是一个专注网站SEO优化、网站SEO诊断、搜索引擎研究、网络营销推广、网站策划运营及站长类的自媒体原创博客

c# - EF Core doesn't detect changes when adding new entity to list (one to many) - Stack Overflow

programmeradmin0浏览0评论

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
  • 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); – Svyatoslav Danyliv 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... :/ – Lucas Constantino Commented Mar 13 at 13:46
Add a comment  | 

3 Answers 3

Reset to default 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()

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);
    }
}
发布评论

评论列表(0)

  1. 暂无评论