При добавлении записи у связывающей сущности генерируется рандомный Id EF CORE

Всем привет. Суть проблемы: я отправляю запрос через swagger с такими данными

{
  "title": "string",
  "content": "string",
  "userId": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
  "categories": [
    "3fa85f64-5717-4562-b3fc-2c963f66afa6"
  ]
}

При добавление записи после прохождения всех этапов в репозитории на моменте вызова метода AddAsync у Categories генерируется случайный ID. По сути, создается новая сущность категории с рандомным Id и пустым Name и добавляется в бд Вот так выглядит БД после нескольких попыток добавления поста

UPD: Добавлю что ID категории, который я передаю в запросе, существует в БД. Всем спасибо за помощь

Вот весь код:

/// <summary>
/// DTO добавления поста.
/// </summary>
/// <param name="Title">Заголовок.</param>
/// <param name="Content">Контент.</param>
/// <param name="UserId">Id автора поста.</param>
/// <param name="Categories">Список категорий поста в виде <see cref="Guid"/>.</param>
public record AddPostRequest(string Title, string Content, Guid UserId, List<Guid> Categories);

/// <summary>
/// Добавление поста. 
/// </summary>
/// <param name="request">Данные поста.</param>
/// <param name="repository"><see cref="IPostRepository"/>.</param>
/// <param name="cancellationToken"><see cref="CancellationToken"/>.</param>
    private static async Task<IResult> AddPost(AddPostRequest request,
        IPostRepository repository, CancellationToken cancellationToken)
    {
        var post = request.ToModel();

        await repository.Create(post, cancellationToken);

        return Results.Ok(new Response(StatusCodes.Status200OK, String.Empty));
    }

public static Post ToModel(this AddPostRequest request)
    {
        var postId = Guid.NewGuid();

        var post = new Post()
        {
            Id = postId,
            Title = request.Title,
            UserId = request.UserId,
            Content = request.Content,
            PostCategories = request.Categories.Select(id => new PostCategory()
            {
                PostId = postId,
                CategoryId = id
            }).ToList(),
            CreatedOn = DateTime.UtcNow,
            ModifiedOn = DateTime.UtcNow
        };

        return post;
    }

public async Task<Result<Post>> Create(Post entity, CancellationToken cancellationToken)
    {
        if (!entity.PostCategories.Any())
        {
            Result<Post>.Failed($"{nameof(entity.PostCategories)} не может быть пустым.", ResultType.BadRequest);
        }

        try
        {
            await context.Database.BeginTransactionAsync(cancellationToken);
            await context.Posts.AddAsync(entity, cancellationToken); // тут у категорий почему-то создается новый ID 
            
            await context.SaveChangesAsync(cancellationToken);
            await context.Database.CommitTransactionAsync(cancellationToken);

            return Result<Post>.Success(entity);
        }
        catch (Exception exception)
        {
            await context.Database.RollbackTransactionAsync(cancellationToken);
            throw;
        }
    }

private static IServiceCollection AddDatabase(this IServiceCollection serviceCollection, string connectionString)
    {
        serviceCollection.AddNpgsql<ApplicationDbContext>(connectionString, builder =>
        {
            builder.EnableRetryOnFailure(DatabaseConfig.RetryOnFailure,
                TimeSpan.FromSeconds(30), null).ExecutionStrategy(dependencies => new NpgsqlExecutionStrategy(dependencies));
        } );

        return serviceCollection;
    }

public class CategoryConfiguration : IEntityTypeConfiguration<Category>
{
    public void Configure(EntityTypeBuilder<Category> builder)
    {
        builder.ToTable("Category");
        builder.HasKey(x => x.Id);
        builder.Property(x => x.Id).IsRequired();
        builder.Property(x => x.Name).IsRequired();
    }
}

public class PostCategoryConfiguration : IEntityTypeConfiguration<PostCategory>
{
    public void Configure(EntityTypeBuilder<PostCategory> builder)
    {
        builder.ToTable("PostCategoryMap");
        builder.HasKey(x => new { x.CategoryId, x.PostId });
        builder.Property(x => x.CategoryId);

        builder.HasOne(x => x.Post)
            .WithMany(x => x.PostCategories)
            .HasForeignKey(x => x.PostId);

        builder.HasOne(x => x.Category)
            .WithMany(x => x.PostCategories)
            .HasForeignKey(x => x.CategoryId);
    }
}

public class PostConfiguration : IEntityTypeConfiguration<Post>
{
    public void Configure(EntityTypeBuilder<Post> builder)
    {
        builder.ToTable("Post");
        builder.HasKey(x=> x.Id);
        builder.HasIndex(x => x.Id).IsUnique();
        builder.Property(x => x.Id).IsRequired();
        builder.Property(x => x.Title).IsRequired();
        builder.Property(x => x.Content).IsRequired();
        builder.Property(x => x.CreatedOn).IsRequired();
    }
}

Вот я добавляю данные

Данные добавления

По итогу в БД добавляются данные в таком виде Результат добавления


Ответы (1 шт):

Автор решения: Uranus

UPD: Добавлю что ID категории который я передаю в запросе существует в БД.

А откуда это известно?

Если вы ответите что сделали запрос в базу данных и убедились что в соответствующей таблице существует запись для каждого идентификатора категории, переданного в HTTP запросе - это неверный ответ. Ведь вы не контролируете, какие именно данные клиенты передадут в запросе на сервер. То, что в конкретном случае все идентификаторы были корректными - скорее совпадение, чем гарантия правильности. В другой раз, клиент замешкается, не обновит своевременно данные, и пришлет идентификаторы записей, которые только что были удалены.

Вот и Entity Framework, в свою очередь, "не доверяет" данным, которые пришли извне. Если EF не "отслеживает" категорию с указанным CategoryId, он думает: "Хмм, такой категории я не знаю, видимо ее надо вставить" - и создает новую Category.

Отслеживание (tracking) - это процесс, при котором DbContext следит за изменениями объекта после того, как он был добавлен или загружен. Контекст имеет свое представление о том, какие сущности новые, какие уже есть в базе, какие были изменены, а какие надо удалить.

Если ты указываешь только Id навигационного объекта, а саму сущность не прикрепляешь к контексту, EF считает, что ты создаешь новую сущность. Он не делает автоматическую проверку в базе.

Именно поэтому новые PostCategory корректно привязываются к новому Post только по идентификатору, а к существующим Category нет.

Разработчик должен самостоятельно озаботиться тем, чтобы подгрузить категории из базы данных, убедиться что все идентификаторы категорий верные, и только после этого создавать новый пост.

Например вот так (здесь для соответствия стилю кода в вопросе предполагается что существует ICategoryRepository с методом GetByIdsAsync):

private static async Task<TResult> AddPost(AddPostRequest request,
  IPostRepository repository,
  ICategoryRepository categoryRepository,
  CancellationToken cancellationToken)
{
  var existingCategories = await categoryRepository.GetByIdsAsync(request.Categories, cancellationToken);
  if (existingCategories.Count != request.Categories.Count)
    return Results.BadRequest(new Response(StatusCodes.Status400BadRequest, "Некоторые категории не найдены"));

  var post = request.ToModel(existingCategories);
  await repository.Create(post, cancellationToken);

  return Results.Ok(new Response(StatusCodes.Status200OK, string.Empty));
}

public static Post ToModel(this AddPostRequest request, List<Category> categories)
{
  var postId = Guid.NewGuid();

  return new Post
  {
    Id = postId,
    Title = request.Title,
    UserId = request.UserId,
    Content = request.Content,
    PostCategories = categories.Select(cat => new PostCategory
    {
      PostId = postId,
      Category = cat
    }).ToList(),
    CreatedOn = DateTime.UtcNow,
    ModifiedOn = DateTime.UtcNow
  };
}
→ Ссылка