При добавлении записи у связывающей сущности генерируется рандомный 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 шт):
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
};
}
