Skip to content

EF Core 9 : the database operation was expected to affect 1 row(s), but actually affected 0 row(s) #35195

@Arman-Espiar

Description

@Arman-Espiar

Ask a question

I am using DDD architecture and the following code changes the aggregate root (basically adds a comment which is an entity to the aggregate root).

public sealed class AddCommentToPostCommandHandlers : ICommandHandler<AddCommentToPostCommand, Guid>
{
	private readonly IPostCommandRepository _postRepository;
	private readonly ILogger<AddCommentToPostCommandHandlers> _logger;

	public AddCommentToPostCommandHandlers(IPostCommandRepository postRepository, ILogger<AddCommentToPostCommandHandlers> logger)
	{
		_postRepository = postRepository;
		_logger = logger;
	}

	public async Task<Result<Guid>> Handle(AddCommentToPostCommand request, CancellationToken cancellationToken)
	{
		var post = await _postRepository.GetGraphByAsync(request.PostId, cancellationToken);

		if (post is not null)
		{
			post.AddComment(request.DisplayName, request.Email, request.CommentText);

			if (post.Result.IsSuccess)
			{
				_postRepository.UpdateBy(post);

				await _postRepository.CommitAsync(cancellationToken);

				return post.Id;
			}

			return post.Result;
		}

		return Result.Fail(ErrorMessages.NotFound(request.ToString()));
	}
}

This code worked fine with EF Core 8, but when I upgraded to EF Core 9, I get the following 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.

This error also occurs when editing a comment, whereas in the previous version (EF Core 8) this error did not occur.

Aggregate root code:

namespace ContentService.Core.Domain.Aggregates.Posts;

public class Post : AggregateRoot<Post>
{
	public Title Title { get; private set; }
	public Description Description { get; private set; }

	public Text Text { get; private set; }

	private readonly List<GuidId> _categoryIds;

	public virtual IReadOnlyList<GuidId> CategoryIds => _categoryIds;

	#region بارگذاری تنبل در سطح دامنه

	private List<Comment> _comments;

	public virtual IReadOnlyList<Comment> Comments
	{
		get
		{
			if (_comments == null)
			{
				LoadComments();
			}

			return _comments.AsReadOnly();
		}
	}

	private void LoadComments()
	{
		// Load comments from the data source here.
		// This is just a placeholder. You will need to replace this with your actual data loading logic.

		_comments = new List<Comment>();
	}

	#endregion End بارگذاری تنبل در سطح دامنه

	public Post()
	{
		_categoryIds = new List<GuidId>();
	}

	private Post(string? title, string? description, string? text) : this()
	{
		var titleResult = Title.Create(title);

		Result.WithErrors(titleResult.Errors);

		var descriptionResult = Description.Create(description);

		Result.WithErrors(descriptionResult.Errors);

		var contentResult = Text.Create(text);

		Result.WithErrors(contentResult.Errors);

		if (Result.IsSuccess)
		{
			Title = titleResult.Value;
			Description = descriptionResult.Value;
			Text = contentResult.Value;
		}
	}

	public Post Create(string? title, string? description, string? text)
	{
		var checkValidations = new Post(title, description, text);

		Result.WithErrors(checkValidations.Result.Errors);

		if (Result.IsFailed) 
            return this;

		if (Result.IsSuccess)
		{
			this.Text = checkValidations.Text;
			this.Title = checkValidations.Title;
			this.Description = checkValidations.Description;

			RaiseDomainEvent(new PostCreatedEvent(Id, this.Title.Value, this.Description.Value, this.Text.Value));
		}

		return this;
	}

	public Post UpdatePost(string? title, string? description, string? text)
	{
		var checkValidations = new Post(title, description, text);

		Result.WithErrors(checkValidations.Result.Errors);

		if (Result.IsFailed) 
            return this;

		if (Result.IsSuccess)
		{
			this.Title = checkValidations.Title;
			this.Description = checkValidations.Description;
			this.Text = checkValidations.Text;

			RaiseDomainEvent(new PostUpdatedEvent(Id, Title.Value!, Description.Value!, Text.Value!));

            Result.WithSuccess(SuccessMessages.SuccessUpdate(DataDictionary.Post));
		}

		return this;
	}

	public Post RemovePost(Guid? id)
	{
		var guidResult = GuidId.Create(id);

		if (guidResult.IsFailed)
		{
			Result.WithErrors(guidResult.Errors);
			return this;
		}

		// Note: if have IsDeleted property (soft delete) we can change to true here
		RaiseDomainEvent(new PostRemovedEvent(id));

		Result.WithSuccess(SuccessMessages.SuccessDelete(DataDictionary.Post));

		return this;
	}

	#region Category

	public Post AddCategory(Guid? categoryId)
	{
		var guidResult = GuidId.Create(categoryId);

		if (guidResult.IsFailed)
		{
			Result.WithErrors(guidResult.Errors);
			return this;
		}

		if (!_categoryIds.Contains(guidResult.Value))       //جلوگیری از تکراری بودن دسته بندی	
		{
			_categoryIds.Add(guidResult.Value);

			RaiseDomainEvent(new PostCategoryAddedEvent(Id, (Guid)categoryId!));
		}

		return this;
	}

	public Post ChangeCategory(Guid? oldCategoryId, Guid? newCategoryId)
	{
		var oldGuidResult = GuidId.Create(oldCategoryId);
		var newGuidResult = GuidId.Create(newCategoryId);

		if (oldGuidResult.IsFailed)
		{
			Result.WithErrors(oldGuidResult.Errors);
			return this;
		}

		if (newGuidResult.IsFailed)
		{
			Result.WithErrors(newGuidResult.Errors);
			return this;
		}

		if (_categoryIds.Contains(oldGuidResult.Value))
		{
			var indexOldCategory = _categoryIds.IndexOf(oldGuidResult.Value);

			if (!_categoryIds.Contains(newGuidResult.Value))
			{
				_categoryIds.RemoveAt(indexOldCategory);
				_categoryIds.Insert(indexOldCategory, newGuidResult.Value);
			}
			else
			{
				_categoryIds.RemoveAt(indexOldCategory);
			}

			RaiseDomainEvent(new CategoryPostChangedEvent(Id, (Guid)oldCategoryId!, (Guid)newCategoryId!));
		}
		else
		{
			Result.WithError(ErrorMessages.NotFound(DataDictionary.Category));
		}

		return this;
	}

	public Post RemoveCategory(Guid? categoryId)
	{
		var guidResult = GuidId.Create(categoryId);

		if (guidResult.IsFailed)
		{
			Result.WithErrors(guidResult.Errors);
			return this;
		}

		if (_categoryIds.Contains(guidResult.Value))
		{
			_categoryIds.Remove(guidResult.Value);

			RaiseDomainEvent(new CategoryPostRemovedEvent(Id, (Guid)categoryId!));
		}

		return this;
	}

	#endregion End Category

	#region Comments

	public Post AddComment(string? name, string? email, string? text)

	{

		var commentResult = Comment.Create(this, name, email, text);

		Result.WithErrors(commentResult.Errors);

		if (Result.IsFailed)

		{

			return this;

		}

		var hasAny = Comments

			.Any(c => c.Name == commentResult.Value.Name

					  && c.Email == commentResult.Value.Email

					  && c.CommentText == commentResult.Value.CommentText);

		if (hasAny)

		{

			var errorMessage = ValidationMessages.Repetitive(DataDictionary.Comment);

			Result.WithError(errorMessage);

			return this;

		}

		_comments.Add(commentResult.Value);

		RaiseDomainEvent(new CommentAddedEvent(this.Id, commentResult.Value.Id, commentResult.Value.Name.Value, commentResult.Value.Email.Value, commentResult.Value.CommentText.Value));

		return this;

	}

	public Post ChangeCommentText(string? name, string? email, string? text, string? newText)

	{

		var commentOldResult = Comment.Create(this, name, email, text);

		var commentNewResult = Comment.Create(this, name, email, newText);

		Result.WithErrors(commentOldResult.Errors);

		Result.WithErrors(commentNewResult.Errors);

		var emailGuardResult = Guard.CheckIf(commentNewResult.Value.Email, DataDictionary.Email)

			.Equal(commentOldResult.Value.Email);

		Result.WithErrors(emailGuardResult.Errors);

		var nameGuardResult = Guard.CheckIf(commentNewResult.Value.Name, DataDictionary.Name)

			.Equal(commentOldResult.Value.Name);

		Result.WithErrors(nameGuardResult.Errors);

		var commentTextGuardResult = Guard.CheckIf(commentNewResult.Value.CommentText, DataDictionary.CommentText)

			.NotEqual(commentOldResult.Value.CommentText);

		Result.WithErrors(commentTextGuardResult.Errors);

		if (Result.IsFailed)

		{

			return this;

		}

		LoadComments();

		var hasAny = Comments

			.Any(c => c.Name == commentNewResult.Value.Name

					  && c.Email == commentNewResult.Value.Email

					  && c.CommentText == commentNewResult.Value.CommentText);

		if (hasAny)

		{

			var errorMessage = ValidationMessages.Repetitive(DataDictionary.Comment);

			Result.WithError(errorMessage);

			return this;

		}

		//var commentIndex = _comments

		//	.FindIndex(c => c.Name == commentOldResult.Value.Name

		//			  && c.Email == commentOldResult.Value.Email

		//			  && c.CommentText == commentOldResult.Value.CommentText);

		var commentIndex = Comments

			.Select((c, i) => new { Comment = c, Index = i })

			.FirstOrDefault(x => x.Comment.Name == commentOldResult.Value.Name

								 && x.Comment.Email == commentOldResult.Value.Email

								 && x.Comment.CommentText == commentOldResult.Value.CommentText)?.Index;

		if (commentIndex >= 0)

		{

			_comments.RemoveAt((int)commentIndex);

			_comments.Insert((int)commentIndex, commentNewResult.Value);

			RaiseDomainEvent(new CommentEditedEvent(this.Id, commentNewResult.Value.Id, commentNewResult.Value.Name.Value, commentNewResult.Value.Email.Value, commentNewResult.Value.CommentText.Value));

		}

		return this;

	}

	public Post RemoveComment(string? name, string? email, string? text)

	{

		var commentResult = Comment.Create(this, name, email, text);

		Result.WithErrors(commentResult.Errors);

		if (Result.IsFailed)

		{

			return this;

		}

		var commentFounded = Comments

			.FirstOrDefault(c => c.Name?.Value?.ToLower() == commentResult.Value.Name?.Value?.ToLower()

								 && c.Email?.Value?.ToLower() == commentResult.Value?.Email?.Value?.ToLower()

								 && c.CommentText.Value?.ToLower() == commentResult?.Value?.CommentText.Value?.ToLower());

		if (commentFounded is null)

		{

			var errorMessage = ErrorMessages.NotFound(DataDictionary.Comment);

			Result.WithError(errorMessage);

			return this;

		}

		_comments.Remove(commentFounded);

		Result.WithSuccess(SuccessMessages.SuccessDelete(DataDictionary.Comment));

		RaiseDomainEvent(new CommentRemovedEvent(Id, name, email, text));

		return this;

	}

	#endregion

}

and comment entity is:

namespace ContentService.Core.Domain.Aggregates.Posts.Entities;

public class Comment : Entity

{

	public DisplayName Name { get; private set; }

	public Email Email { get; private set; }

	public CommentText CommentText { get; private set; }

	public Guid PostId { get; private set; }

	private Comment()

	{

	}

	private Comment(Guid postId, DisplayName name, Email email, CommentText text) : this()

	{

		PostId = postId;

		Name = name;

		Email = email;

		CommentText = text;

	}

	public static Result<Comment> Create(Guid? postId, string? name, string? email, string? text)

	{

		Result<Comment> result = new();

		if (!postId.HasValue || postId == Guid.Empty)

		{

			var errorMessage = ValidationMessages.Required(DataDictionary.Post);

			result.WithError(errorMessage);

		}

		var displayNameResult = DisplayName.Create(name);

		result.WithErrors(displayNameResult.Errors);

		var emailResult = Email.Create(email);

		result.WithErrors(emailResult.Errors);

		var textResult = CommentText.Create(text);

		result.WithErrors(textResult.Errors);

		if (result.IsFailed)

		{

			return result;

		}

		var returnValue = new Comment((Guid)postId!, displayNameResult.Value, emailResult.Value, textResult.Value);

		result.WithValue(returnValue);

		return result;

	}

}

and ef config:

internal sealed class PostConfiguration : IEntityTypeConfiguration<Post>

{

	public void Configure(EntityTypeBuilder<Post> builder)

	{

		builder.Property(p => p.CategoryIds)

			.HasConversion(

				v => string.Join(',', v.Select(c => c.Value)),

				v => v.Split(',', StringSplitOptions.RemoveEmptyEntries).Select(c => GuidId.Create(c).Value).ToList()

			);

		builder.Property(p => p.Title)

			.IsRequired(true)

			.HasMaxLength(Title.Maximum)

			.HasConversion(p => p.Value, p => Title.Create(p).Value);

		builder.Property(p => p.Description)

			.IsRequired(true)

			.HasMaxLength(Description.Maximum)

			.HasConversion(d => d.Value, d => Description.Create(d).Value);

		builder.Property(p => p.Text)

			.IsRequired(true)

			.HasConversion(t => t.Value, t => Text.Create(t).Value);

		builder.OwnsMany<Comment>(c => c.Comments, cc =>

		{

			cc.ToTable("Comments");

			cc.Property(c => c.Email)

				.IsRequired(true)

				.HasConversion(e => e.Value, e => Email.Create(e).Value);

			cc.Property(c => c.Name)

				.IsRequired(true)

				.HasMaxLength(DisplayName.Maximum)

				.HasConversion(e => e.Value, e => DisplayName.Create(e).Value);

			cc.Property(c => c.CommentText)

				.IsRequired(true)

				.HasMaxLength(CommentText.Maximum)

				.HasConversion(e => e.Value, e => CommentText.Create(e).Value);

		});

	}

}

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions