TGViewer
C# 1001 notes C# 1001 notes @csharp_1001_notes · 6.64K subscribers
Post #883 2.11K
🔥 EF Core migrations - это компилятор схемы базы, а не кнопка «создать таблицу»

Опытный C#-разработчик не оставляет бизнес-инварианты только внутри SaveChangesAsync().

Проверку в приложении можно обойти прямым SQL, импортом данных, фоновым процессом или второй версией сервиса. Поэтому правила, нарушение которых делает данные некорректными, стоит закреплять в самой базе.


public sealed class ProductConfiguration
: IEntityTypeConfiguration<Product>
{
public void Configure(EntityTypeBuilder<Product> builder)
{
builder.ToTable("Products", table =>
{
table.HasCheckConstraint(
"CK_Products_Price_Range",
"[Price] > 0 AND [Price] <= 10000000");
});

builder.Property(x => x.Name)
.HasMaxLength(200)
.IsRequired();

builder.Property(x => x.NormalizedName)
.HasMaxLength(200)
.IsRequired();

builder.Property(x => x.Price)
.HasPrecision(18, 2);

builder.HasIndex(x => x.NormalizedName)
.IsUnique()
.HasDatabaseName("UX_Products_NormalizedName");
}
}


HasPrecision(18, 2) управляет физическим представлением decimal, но не проверяет допустимый диапазон цены. За это отвечает CHECK.

Уникальный индекс решает другую важную проблему - гонку между запросами.


if (!await db.Products.AnyAsync(x => x.Name == name))
{
db.Products.Add(product);
await db.SaveChangesAsync();
}


Два параллельных запроса могут одновременно пройти AnyAsync() и попытаться создать одинаковые записи. Только уникальное ограничение базы гарантированно остановит второй запрос.

Использовать для уникальности исходный Name тоже опасно. Результат будет зависеть от collation базы: Laptop, LAPTOP и Laptop могут считаться одинаковыми или разными. Поэтому часто сохраняют нормализованное значение:


product.NormalizedName = product.Name
.Trim()
.ToUpperInvariant();


После изменения модели создаём миграцию:


dotnet ef migrations add HardenProductSchema


Но миграцию нельзя принимать вслепую. EF Core сравнивает текущую модель с ModelSnapshot и генерирует предполагаемый переход между двумя состояниями.

Например, обычное переименование свойства может быть распознано как удаление старого столбца и создание нового:


migrationBuilder.DropColumn(
name: "Name",
table: "Products");

migrationBuilder.AddColumn<string>(
name: "DisplayName",
table: "Products",
nullable: false);


В production это означает потерю данных. Правильную операцию нужно указать вручную:


migrationBuilder.RenameColumn(
name: "Name",
table: "Products",
newName: "DisplayName");


Для крупных таблиц полезен подход expand -> backfill -> contract.

Сначала добавляется nullable-столбец:


migrationBuilder.AddColumn<string>(
name: "NormalizedName",
table: "Products",
type: "nvarchar(200)",
nullable: true);


Затем данные заполняются порциями отдельной задачей. После обновления всех строк новая версия приложения начинает записывать оба значения. И только в следующем релизе столбец переводится в NOT NULL, добавляется индекс и удаляется устаревшая логика.

Так миграция не держит долгую блокировку и остаётся совместимой сразу с несколькими версиями приложения.

В CI стоит проверять, не забыл ли разработчик создать миграцию:


dotnet ef migrations has-pending-model-changes


Для production лучше генерировать проверяемый SQL:


dotnet ef migrations script \
--idempotent \
--output migrations.sql


Либо собирать отдельный executable:


dotnet ef migrations bundle \
--self-contained \
-r linux-x64


efbundle можно запускать в deployment job без исходников проекта и установленного EF CLI.
More from @csharp_1001_notes
  1. Sep 20, 2026🧩 C# Channels: сообщение попало в очередь. Почему обработчик его не получил? Два воркера…
  2. Sep 16, 2026Эта задача проверяет понимание Memory<T>, pooling, ownership, IAsyncEnumerable, cancellati…
  3. Sep 16, 2026# C# 14: хитрая задача на ArrayPool, IAsyncEnumerable и время жизни памяти Есть поток данн…
  4. Sep 14, 2026# C# 14: конкурентный кеш без race condition Реализуйте потокобезопасный кеш: public seale…
  5. Sep 14, 2026🔍Тестовое собеседование с Senior C# разработчиком уже завтра 15 сентября(уже завтра!) в 1…
  6. Sep 11, 2026🔧 Как передать многострочный PEM через Aspire в ASP.NET Core Сертификаты и ключи в PEM со…
Threads Profile ViewerView any public Threads profile without an account.Open ThreadLook →Writing with AI? Make it sound human.Metric37 rewrites AI drafts so they read naturally. Free AI detector, 1,500 words free.Try Metric37 →