Start Debugging

Исправление: The seed entity for entity type 'X' cannot be added because a non-zero value is required for property 'Id'

HasData задаёт начальные данные для сущности с ключом, генерируемым хранилищем, но без явного значения. Дайте каждой строке стабильный Id, отличный от нуля, или используйте UseSeeding.

Исправление: вы вызвали HasData, чтобы задать начальные данные для сущности, чей первичный ключ генерируется хранилищем (столбец IDENTITY), но оставили ключ в значении по умолчанию для CLR (0 для int, Guid.Empty для Guid). HasData строит свой скрипт вставки во время миграции, не обращаясь к базе данных, поэтому он не может полагаться на то, что база данных выдаст ключи. Дайте каждой начальной строке явное, стабильное значение ключа, отличное от нуля (new Country { Id = 1, ... }). Если вы действительно хотите, чтобы ключ генерировала база данных, не используйте HasData вовсе: используйте вместо него UseSeeding/UseAsyncSeeding. Это руководство написано для .NET 11, C# 14 и Microsoft.EntityFrameworkCore 11.0.0, но текст сообщения и поведение не менялись начиная с EF Core 2.1.

System.InvalidOperationException: The seed entity for entity type 'Country' cannot be added
because a non-zero value is required for property 'Id'. Consider providing a negative value
to avoid collisions with non-seed data.
   at Microsoft.EntityFrameworkCore.Metadata.Internal.EntityType.<>c__DisplayClass...
   at Microsoft.EntityFrameworkCore.Infrastructure.ModelValidator.ValidateData(IModel model, ...)
   at Microsoft.EntityFrameworkCore.Infrastructure.ModelValidator.Validate(IModel model, ...)
   at Microsoft.EntityFrameworkCore.Metadata.Conventions.Infrastructure.ModelRuntimeInitializer.Initialize(...)

Это ошибка валидации модели, а не ошибка запроса во время выполнения. Она возникает при первом построении модели EF Core: при первом запросе, первом SaveChanges, первом обращении к context.Model или, чаще всего, когда вы запускаете dotnet ef migrations add. Имя типа в кавычках это сущность, для которой вы задали начальные данные; свойство в кавычках это её первичный ключ. Подсказка “consider providing a negative value” в конце это реальный совет, а не наполнитель, и раздел ниже о коллизиях объясняет почему.

Почему HasData требует, чтобы ключ был указан явно

HasData (документация теперь называет это model managed data, поскольку “seeding” обещал больше, чем делает) принадлежит миграциям. Когда вы добавляете миграцию, EF Core сравнивает начальные строки в вашей текущей модели со строками, которые он записал в последнем снимке модели, и выдаёт вызовы InsertData, UpdateData или DeleteData, чтобы согласовать их. Это сравнение происходит во время разработки, на вашей машине, без подключения к базе данных.

Именно первичный ключ делает сравнение возможным. EF Core использует его, чтобы распознать “это та же строка, что я вставил в прошлой миграции, но Name изменился” в отличие от “это совершенно новая строка”. Без стабильного значения ключа ему не с чем сопоставлять между миграциями. Поэтому валидатор модели применяет жёсткое правило: каждая строка HasData должна нести явное значение для своего первичного ключа, даже когда этот ключ настроен как генерируемый хранилищем.

Когда ключ генерируется хранилищем, а вы оставляете его в значении по умолчанию для CLR, EF Core не может отличить “разработчик забыл задать ключ” от “разработчик хочет ключ 0”. Вместо того чтобы молча вставить строку с Id = 0 (что грубо конфликтует со столбцами identity), он выбрасывает исключение. Список ограничений model managed data ставит это первым: “Значение первичного ключа должно быть указано, даже если обычно его генерирует база данных. Оно будет использовано для обнаружения изменений данных между миграциями.”

Минимальный репро

Одна сущность с обычным Id и одним вызовом HasData, который его опускает.

// .NET 11, C# 14, Microsoft.EntityFrameworkCore 11.0.0
public class Country
{
    public int Id { get; set; }      // conventional PK -> store-generated IDENTITY
    public string Name { get; set; } = "";
}

public class AppDbContext : DbContext
{
    public DbSet<Country> Countries => Set<Country>();

    protected override void OnConfiguring(DbContextOptionsBuilder options)
        => options.UseSqlServer("Server=(localdb)\\mssqllocaldb;Database=SeedRepro;Trusted_Connection=True");

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        modelBuilder.Entity<Country>().HasData(
            new Country { Name = "USA" },      // no Id -> Id stays 0 -> throws
            new Country { Name = "Canada" });
    }
}

Запустите против этого dotnet ef migrations add Seed, и вы получите исключение. Свойства Id равны 0, EF Core трактует 0 как “нет значения” для генерируемого хранилищем ключа int, и валидация падает до того, как будет записан какой-либо код миграции.

Исправление 1: дайте каждой начальной строке явный, стабильный ключ

Это правильное исправление для настоящих справочных данных: небольших фиксированных таблиц-справочников, которые меняются только через миграции (страны, валюты, роли, статусы). Назначьте каждой строке значение ключа вручную и никогда не используйте его повторно и не перенумеровывайте.

// .NET 11, C# 14, EF Core 11.0.0
modelBuilder.Entity<Country>().HasData(
    new Country { Id = 1, Name = "USA" },
    new Country { Id = 2, Name = "Canada" },
    new Country { Id = 3, Name = "Mexico" });

Два правила поддерживают это в рабочем состоянии со временем:

  1. Значения ключей теперь часть истории вашей схемы. Относитесь к ним как к неизменяемым. Если вы измените строку 2 с Id = 2 на Id = 22, следующая миграция выдаст DeleteData для 2 и InsertData для 22, что выбросит старую строку и всё, что ссылалось на неё по внешнему ключу.
  2. Выбирайте значения явно, не давайте им смещаться. Жёстко заданные константы подходят. Чего делать нельзя, так это вычислять их из чего-то недетерминированного (счётчика цикла, зависящего от порядка коллекции, DateTime.Now, вызова Guid.NewGuid()), потому что сравнение миграции должно давать одни и те же значения на каждой машине.

Если ключ это Guid, применяется то же правило: укажите фиксированный, жёстко заданный Guid, а не Guid.NewGuid(). Свежесгенерированный GUID при каждой сборке заставляет каждую миграцию думать, что строка изменилась.

// .NET 11, C# 14, EF Core 11.0.0 - fixed GUIDs, not Guid.NewGuid()
modelBuilder.Entity<Role>().HasData(
    new Role { Id = new Guid("3f2504e0-4f89-41d3-9a0c-0305e82c3301"), Name = "Admin" },
    new Role { Id = new Guid("3f2504e0-4f89-41d3-9a0c-0305e82c3302"), Name = "User" });

Исправление 2: используйте отрицательные ключи, чтобы обойти коллизии identity

Собственная подсказка исключения, “consider providing a negative value to avoid collisions with non-seed data”, решает проблему, с которой вы столкнётесь позже, а не ту, что перед вами. Когда вы задаёте начальные Id = 1, 2, 3 в столбце IDENTITY SQL Server, счётчик identity ничего не знает о ваших начальных строках. Первый реальный INSERT после задания начальных данных запускает счётчик с 1, пытается использовать ключ, который ваши начальные данные уже заняли, и падает с нарушением первичного ключа, либо в дело вступает механика IDENTITY_INSERT, и всё запутывается.

Отрицательные ключи для начальных данных обходят это. Реальные строки, созданные пользователем, отсчитываются вверх от 1; ваши начальные справочные строки живут ниже 0 и никогда не пересекаются.

// .NET 11, C# 14, EF Core 11.0.0
modelBuilder.Entity<Country>().HasData(
    new Country { Id = -1, Name = "USA" },
    new Country { Id = -2, Name = "Canada" },
    new Country { Id = -3, Name = "Mexico" });

Это давняя рекомендация команды EF Core для таблиц-справочников на основе IDENTITY, которые также получают вставки во время выполнения. Это излишне для таблицы, для которой только задаются начальные данные (чистый справочник, в который приложение никогда не вставляет), где положительные ключи читаются естественнее.

Исправление 3: перестаньте использовать HasData для этих данных

Если вашей причиной обратиться к HasData было “мне просто нужно несколько начальных строк в базе данных”, то вам, вероятно, вообще не нужны model managed data. HasData создан для статических, детерминированных данных, принадлежащих миграциям, и больше ни для чего. Для данных, которые должны использовать генерируемые базой данных ключи, зависят от других строк или просто являются удобными стартовыми данными, команда EF Core теперь рекомендует UseSeeding и UseAsyncSeeding, введённые в EF Core 9. Они выполняют реальный SaveChanges против живой базы данных, поэтому ключи генерирует база данных, и правило “не ноль” никогда не применяется.

// .NET 11, C# 14, EF Core 11.0.0
protected override void OnConfiguring(DbContextOptionsBuilder options)
    => options
        .UseSqlServer(connectionString)
        .UseSeeding((context, _) =>
        {
            if (!context.Set<Country>().Any())
            {
                // No Id set: the database generates it on SaveChanges. No error.
                context.Set<Country>().AddRange(
                    new Country { Name = "USA" },
                    new Country { Name = "Canada" });
                context.SaveChanges();
            }
        })
        .UseAsyncSeeding(async (context, _, ct) =>
        {
            if (!await context.Set<Country>().AnyAsync(ct))
            {
                context.Set<Country>().AddRange(
                    new Country { Name = "USA" },
                    new Country { Name = "Canada" });
                await context.SaveChangesAsync(ct);
            }
        });

UseSeeding вызывается из EnsureCreated и Migrate, а также из dotnet ef database update, даже когда нет ожидающих миграций. Реализуйте обе перегрузки, синхронную и асинхронную: инструменты EF Core вызывают синхронную, и она молча пропустит задание начальных данных, если существует только асинхронная версия. Поскольку тело задания начальных данных выполняется как код приложения без требования к ключу во время разработки, опускать Id здесь как раз правильно: его назначает база данных.

Варианты, которые дают ту же ошибку

Формулировка сообщения идентична во всех этих случаях, поэтому поисковый трафик попадает сюда для всех них. Причина всегда одна и та же (строке HasData не хватает явного значения ключа, генерируемого хранилищем), но поверхность различается.

Другая, но легко путаемая ошибка это The seed entity for entity type 'X' cannot be added because there was no value provided for the required property 'Y', где Y это обязательное свойство, не являющееся ключом, например Name. Эта означает, что столбец [Required] или IsRequired() оставлен null в начальной строке, а не то, что ключ отсутствует. Исправление там это заполнить отсутствующее свойство, не являющееся ключом.

Если модель никогда не строится достаточно далеко, чтобы валидировать начальные данные, потому что EF Core даже не может найти ключ для типа, перед вами другая проблема: см. исправление для the entity type requires a primary key to be defined.

Почему “просто задайте Id = 0 явно” не работает

Заманчивое неисправление это написать new Country { Id = 0, Name = "USA" } и предположить, что явность удовлетворит валидатор. Она не удовлетворяет. Для генерируемого хранилищем ключа EF Core сравнивает значение со значением по умолчанию для CLR, а 0 это и есть значение по умолчанию для int. Валидатор не может отличить ваш намеренный 0 от незаданного поля, поэтому он всё равно выбрасывает исключение. Единственные значения, которые проходят, это значения, отличные от значения по умолчанию: любой int, отличный от нуля, любой непустой Guid, любой составной, отличный от значения по умолчанию. Если вам действительно нужна строка с ключом 0, это признак того, что ключ не должен генерироваться хранилищем; пометьте его ValueGeneratedNever(), и валидатор перестаёт применять правило “не ноль”, потому что ключ теперь целиком ваша ответственность.

// .NET 11, C# 14, EF Core 11.0.0 - opt out of store generation, then 0 is allowed
modelBuilder.Entity<Country>(b =>
{
    b.Property(c => c.Id).ValueGeneratedNever();
    b.HasData(new Country { Id = 0, Name = "Unknown" });   // now valid
});

Это реальный паттерн для строки-сигнала “неизвестно”, но делайте это осознанно: как только ключ становится ValueGeneratedNever, каждая будущая вставка в эту таблицу должна предоставлять собственный ключ, включая вставки во время выполнения из вашего приложения.

Связанное

Источники

Comments

Sign in with GitHub to comment. Reactions and replies thread back to the comments repo.

< Назад