EF Core 9: ожидалось, что операция базы данных повлияет на 1 строку (строки), но фактически затронула 0 строк.C#

Место общения программистов C#
Ответить
Anonymous
 EF Core 9: ожидалось, что операция базы данных повлияет на 1 строку (строки), но фактически затронула 0 строк.

Сообщение Anonymous »

Я использую архитектуру DDD, и следующий код изменяет корневой элемент агрегата (по сути, добавляет комментарий, который является объектом в корень агрегата).
public sealed class AddCommentToPostCommandHandlers : ICommandHandler
{
private readonly IPostCommandRepository _postRepository;
private readonly ILogger _logger;

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

public async Task 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()));
}
}

Этот код отлично работал с EF Core 8, но при обновлении до EF Core 9 я получаю следующую ошибку:

Microsoft.EntityFrameworkCore.DbUpdateConcurrencyException: ожидалось, что
операция с базой данных повлияет на 1 строку(и), но на самом деле
затронула 0 строк; данные могли быть изменены или удалены после загрузки
объектов.

Эта ошибка также возникает при редактировании комментария, тогда как в предыдущей версии ( EF Core 8) эта ошибка не произошла.
Агрегированный корневой код:
namespace ContentService.Core.Domain.Aggregates.Posts;

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

public Text Text { get; private set; }

private readonly List _categoryIds;

public virtual IReadOnlyList CategoryIds => _categoryIds;

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

private List _comments;

public virtual IReadOnlyList 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();
}

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

public Post()
{
_categoryIds = new List();
}

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 Create(Guid? postId, string? name, string? email, string? text)

{

Result 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;

}

}

и конфигурация ef:
internal sealed class PostConfiguration : IEntityTypeConfiguration

{

public void Configure(EntityTypeBuilder 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(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);

});

}

}


Подробнее здесь: https://stackoverflow.com/questions/792 ... t-actually
Ответить

Быстрый ответ

Изменение регистра текста: 
Смайлики
:) :( :oops: :roll: :wink: :muza: :clever: :sorry: :angel: :read: *x)
Ещё смайлики…
   
К этому ответу прикреплено по крайней мере одно вложение.

Если вы не хотите добавлять вложения, оставьте поля пустыми.

Максимально разрешённый размер вложения: 15 МБ.

Вернуться в «C#»