Будь-який застосунок що працює з реляційною базою даних стоїть перед фундаментальним ArchitectureDecision: хто є джерелом правди про схему?
Це питання не технічне — воно про процеси, команди і відповідальність.
В одних організаціях є окрема команда DBA (Database Administrators), що пишуть схему вручну, оптимізують індекси, контролюють типи стовпців. Розробники подають запити на зміни, DBA оцінюють вплив і реалізують. Схема в базі — первинна. Код лише відображає її.
В інших організаціях є agile-команди де розробники і є архітекторами схеми. Вони думають у термінах доменних об'єктів, складають С# класи і дозволяють EF Core генерувати схему. Бізнес-логіка і структура даних розвиваються разом — в одному pull request.
Обидва підходи мають право на існування. Проблема виникає коли команда не розуміє чим відрізняються ці підходи і застосовує інструменти «не за призначенням».
Code-First — ви пишете C# класи (entity), конфігуруєте їх через Fluent API або Data Annotations, а EF Core генерує DDL-схему на їх основі. Міграції відображають зміни у коді.
C# Entities → EF Core → Migrations → Database Schema
(джерело) (трансляція) (версіонування) (результат)
Де сильний:
Де слабкий:
Database-First — DBA або існуюча система визначає схему SQL. EF Core генерує C# entity і DbContext з існуючої БД через scaffold-dbcontext. Розробник отримує готові класи.
Database Schema → scaffold-dbcontext → C# Entities + DbContext
(джерело) (генерація) (результат)
Де сильний:
Де слабкий:
Model-First — використання візуальних дизайнерів (Entity Data Model Designer у старих версіях Visual Studio) для креслення схеми, з подальшою генерацією і коду і БД. EF Core цей підхід вже не підтримує — він є спадщиною EF 6 і формату .edmx.
*.edmx файли — формат EF 6 і старіших версій. Якщо ви мігруєте з EF 6 на EF Core — ваш .edmx доведеться переписати у Code-First або Database-First підхід.🟢 Новий проєкт, agile команда
🟡 Legacу база, новий застосунок
🔵 DBA-контрольована організація
migrationBuilder.Sql().🔴 Кілька застосунків, одна БД
Перш ніж переходити до scaffold — розберемо фундаментальну різницю між двома підходами до ініціалізації бази. Це одна з найчастіших точок плутанини.
EnsureCreated() перевіряє чи існує база даних і якщо ні — створює її на основі поточної EF Core моделі, без жодних міграцій:
await context.Database.EnsureCreatedAsync();
// Якщо БД не існує → CREATE DATABASE + створює всі таблиці
// Якщо БД існує → нічого не робить (навіть якщо схема застаріла!)
Коли правильно использовувати:
EnsureDeleted() + EnsureCreated() cycleЧому НІКОЛИ не використовувати у production:
// ❌ Антипатерн у production:
await context.Database.EnsureCreatedAsync();
// Проблема 1: Якщо БД існує але застаріла — нічого не відбувається!
// Проблема 2: Таблиця __EFMigrationsHistory НЕ створюється
// Проблема 3: Перехід на Migrate потім неможливий без ручного втручання
EnsureCreated() і Migrate() є несумісними. Якщо ви почали з EnsureCreated(), таблиця __EFMigrationsHistory не існує. Перехід на міграції потребуватиме ручного baseline — навчити EF Core що «всі міграції вже застосовані» без реального їх виконання.MigrateAsync() застосовує всі Pending міграції — саме те що потрібно у production (детально розглянуто у статті 23).
// Ідіома для тестів: кожен тест-запуск — чиста база
public class TestDbFixture : IDisposable
{
public AppDbContext Context { get; }
public TestDbFixture()
{
var options = new DbContextOptionsBuilder<AppDbContext>()
.UseNpgsql("Host=localhost;Database=shop_test;...")
.Options;
Context = new AppDbContext(options);
// Скидаємо і відновлюємо схему
Context.Database.EnsureDeleted();
Context.Database.EnsureCreated();
}
public void Dispose() => Context.Dispose();
}
| EnsureCreated | MigrateAsync | |
|---|---|---|
| Створює БД якщо немає | ✅ | ✅ |
| Застосовує зміни до існуючої | ❌ | ✅ |
Створює __EFMigrationsHistory | ❌ | ✅ |
| Сумісний з Migrations | ❌ | ✅ |
| Для production | ❌ | ✅ |
| Для тестів | ✅ | ⚠️ |
scaffold-dbcontext — команда dotnet ef що підключається до існуючої бази і генерує C# entity класи та DbContext. Це точка входу у Database-First workflow.
Крім стандартних EF Core пакетів потрібні:
# Встановити design-time інструмент
dotnet tool restore # або dotnet tool install --global dotnet-ef
# NuGet: обов'язково наявний Microsoft.EntityFrameworkCore.Design
dotnet ef dbcontext scaffold \
"Server=.;Database=ShopDb;Trusted_Connection=True" \
Microsoft.EntityFrameworkCore.SqlServer \
--output-dir Models \
--context-dir Data \
--context AppDbContext
Параметри цієї команди:
dotnet ef dbcontext scaffold [connection] [provider] [options]
[connection] Connection string або ім'я з appsettings.json (--configuration ...)
[provider] NuGet провайдер: Microsoft.EntityFrameworkCore.SqlServer
Npgsql.EntityFrameworkCore.PostgreSQL
Microsoft.EntityFrameworkCore.Sqlite
Options:
-o, --output-dir Директорія для entity класів (відносно проєкту)
--context-dir Директорія для DbContext файлу
-c, --context Ім'я DbContext класу
--namespace Namespace для entity
--context-namespace Namespace для DbContext
-f, --force Перезаписати наявні файли (для re-scaffolding)
-t, --table Scaffold лише вказані таблиці
--schema Scaffold лише вказану схему
--no-onconfiguring Не генерувати OnConfiguring (connection string не у класі)
--no-pluralize Не плюралізувати імена DbSet
--use-database-names Зберігати оригінальні імена таблиць/стовпців (не PascalCase)
-d, --data-annotations Використовувати Data Annotations замість Fluent API
dotnet ef dbcontext scaffold \
"Name=ConnectionStrings:DefaultConnection" \ # з appsettings.json
Microsoft.EntityFrameworkCore.SqlServer \
--output-dir Infrastructure/Entities \
--context-dir Infrastructure \
--context ShopDbContext \
--namespace YourApp.Infrastructure.Entities \
--context-namespace YourApp.Infrastructure \
--no-onconfiguring \ # connection string не у класі!
--force # перезаписати при re-scaffold
Великі legacy бази мають сотні таблиць. Scaffold всіх — не потрібно. Фільтруйте:
# Scaffold лише конкретних таблиць
dotnet ef dbcontext scaffold [connection] [provider] \
--table Products \
--table Categories \
--table Orders
# Scaffold всіх таблиць конкретної схеми
dotnet ef dbcontext scaffold [connection] [provider] \
--schema sales \
--schema inventory
# Комбінація: схема + конкретні таблиці з іншої схемы
dotnet ef dbcontext scaffold [connection] [provider] \
--schema sales \
--table dbo.Products \
--table dbo.Categories
Розберемо що генерує scaffold-dbcontext для типової таблиці Products.
CREATE TABLE Products (
Id INT NOT NULL IDENTITY(1,1),
Name NVARCHAR(200) NOT NULL,
Price DECIMAL(10,2) NOT NULL,
Description NVARCHAR(MAX) NULL,
IsActive BIT NOT NULL DEFAULT(1),
CategoryId INT NOT NULL,
CreatedAt DATETIME2 NOT NULL DEFAULT(GETUTCDATE()),
CONSTRAINT PK_Products PRIMARY KEY (Id),
CONSTRAINT FK_Products_Categories FOREIGN KEY (CategoryId)
REFERENCES Categories(Id),
CONSTRAINT CK_Products_Price CHECK (Price >= 0)
);
CREATE INDEX IX_Products_CategoryId ON Products(CategoryId);
CREATE UNIQUE INDEX UX_Products_Name_CategoryId ON Products(Name, CategoryId);
// Models/Product.cs — згенерований EF Core scaffold
public partial class Product // partial ← ключове слово!
{
public int Id { get; set; }
public string Name { get; set; } = null!; // NOT NULL → = null!
public decimal Price { get; set; }
public string? Description { get; set; } // NULL → nullable string?
public bool IsActive { get; set; }
public int CategoryId { get; set; }
public DateTime CreatedAt { get; set; }
// Навігаційна властивість (FK до Categories)
public virtual Category Category { get; set; } = null!;
// Зворотна навігація (якщо є зв'язки від Product до інших таблиць)
}
partial classScaffold генерує partial class! Це принципово важливо. partial дозволяє розширити клас в окремому файлі без ризику втрати розширень при re-scaffold. Ми розглянемо це детально у наступному розділі.
// AppDbContext.cs — згенерований
public partial class AppDbContext : DbContext
{
public AppDbContext() { }
public AppDbContext(DbContextOptions<AppDbContext> options)
: base(options) { }
public virtual DbSet<Category> Categories { get; set; }
public virtual DbSet<Product> Products { get; set; }
public virtual DbSet<Order> Orders { get; set; }
// УВАГА: якщо не використовували --no-onconfiguring
// connection string потрапить сюди (витік секретів у код!)
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
if (!optionsBuilder.IsConfigured)
{
// ⚠️ НЕ хочемо щоб connection string був тут у production!
optionsBuilder.UseSqlServer("Server=.;Database=ShopDb;...");
}
}
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Product>(entity =>
{
entity.HasKey(e => e.Id);
entity.HasIndex(e => e.CategoryId, "IX_Products_CategoryId");
entity.HasIndex(e => new { e.Name, e.CategoryId }, "UX_Products_Name_CategoryId")
.IsUnique();
entity.Property(e => e.Name).HasMaxLength(200);
entity.Property(e => e.Price).HasPrecision(10, 2);
entity.Property(e => e.IsActive).HasDefaultValue(true);
entity.Property(e => e.CreatedAt)
.HasDefaultValueSql("getutcdate()");
entity.HasCheckConstraint("CK_Products_Price", "[Price] >= 0");
entity.HasOne(e => e.Category)
.WithMany(e => e.Products)
.HasForeignKey(e => e.CategoryId)
.OnDelete(DeleteBehavior.ClientSetNull)
.HasConstraintName("FK_Products_Categories");
});
// ... інші entity конфігурації
}
}
За замовчуванням scaffold-dbcontext генерує Fluent API конфігурацію у OnModelCreating. Це правильний вибір — Fluent API є виразнішим і підтримує більше опцій.
З прапором --data-annotations — частина конфігурації переміщується до атрибутів entity:
dotnet ef dbcontext scaffold [connection] [provider] --data-annotations
// З --data-annotations: атрибути у entity
[Table("Products")]
public partial class Product
{
[Key]
public int Id { get; set; }
[Required]
[StringLength(200)]
public string Name { get; set; } = null!;
[Precision(10, 2)]
public decimal Price { get; set; }
// Навігаційна властивість залишається у Fluent API
// (деякі конфігурації неможливо виразити через атрибути)
}
--data-annotationsне всі конфігурації переходять у атрибути — складні речі (Check Constraints, Default SQL values, складні індекси) залишаються у OnModelCreating. Тому Fluent API підхід (за замовчуванням) є більш послідовним і передбачуваним.T4 (Text Template Transformation Toolkit) — шаблонний рушій Microsoft що використовується EF Core для генерації коду при scaffold. Ви можете замінити стандартні шаблони своїми, щоб отримати якісніший, більш доменно-орієнтований код.
Уявіть legacy база з 300 таблицями. Стандартний scaffold генерує 300 entity — всі partial class, всі базово правильні. Але:
product_categories), а ми хочемо PascalCase entity (ProductCategory)IEntity<TKey> інтерфейс[JsonIgnore] до певних навігаційних властивостейrecord замість class для деяких entityБез T4 — правити 300 файлів вручну. З T4 — правити один шаблон.
Починаючи з EF Core 7, шаблони можна кастомізувати через dotnet ef dbcontext script або спеціальний пакет:
dotnet add package Microsoft.EntityFrameworkCore.Design
dotnet ef dbcontext scaffold --help # переконатись що tools встановлено
Для T4-шаблонів потрібен окремий пакет:
dotnet add package Microsoft.EntityFrameworkCore.Templates --version 9.0.*
# Генерує .t4 шаблони у поточній директорії для редагування
dotnet ef dbcontext scaffold --t4-templates
# АБО через спеціальну команду (залежить від версії tools):
dotnet ef dbcontext scaffold ... --use-t4
Після виконання у папці CodeTemplates/EFCore/ з'являються:
CodeTemplates/
└── EFCore/
├── DbContext.t4 ← шаблон для DbContext
└── EntityType.t4 ← шаблон для entity class
<#@ template hostSpecific="true" #>
<#@ assembly name="Microsoft.EntityFrameworkCore.Design" #>
<#@ parameter name="Model" type="Microsoft.EntityFrameworkCore.Metadata.IEntityType" #>
<#@ parameter name="Options" type="Microsoft.EntityFrameworkCore.Scaffolding.ModelCodeGenerationOptions" #>
<#@ import namespace="System.Linq" #>
<#@ import namespace="Microsoft.EntityFrameworkCore" #>
<#@ import namespace="Microsoft.EntityFrameworkCore.Design" #>
<#
// Логіка C# тут, у <# ... #> блоках
var className = Model.GetTableName(); // отримати ім'я таблиці
#>
// <auto-generated />
namespace <#= Options.ModelNamespace #>;
/// <summary>
/// Represents the <#= Model.GetTableName() #> table.
/// </summary>
public partial class <#= Model.Name #> : IEntity<int> // ← додаємо інтерфейс!
{
<# foreach (var property in Model.GetProperties()) { #>
public <#= property.ClrType.Name #><#= ... #> <#= property.Name #> { get; set; }
<# } #>
}
// CodeTemplates/EFCore/EntityType.t4 (спрощено)
<#@ template hostSpecific="true" #>
<#@ output extension=".cs" #>
// <auto-generated> This file was auto-generated by EF Core scaffolding. </auto-generated>
// ⚠️ DO NOT EDIT DIRECTLY — use Partial Classes for extensions
using System.ComponentModel.DataAnnotations;
using YourApp.Core.Interfaces;
namespace <#= Options.ModelNamespace #>;
/// <summary>
/// Entity class for table [<#= schema #>].[<#= tableName #>].
/// Generated: <#= DateTime.UtcNow.ToString("yyyy-MM-dd") #>
/// </summary>
[Table("<#= tableName #>", Schema = "<#= schema #>")]
public partial class <#= entityName #>
<# if (hasAuditFields) { #>
: IAuditableEntity
<# } #>
{
// Generated properties...
<# foreach (var prop in properties) { #>
/// <summary><#= prop.Comment ?? prop.Name #></summary>
public <#= prop.TypeName #> <#= prop.Name #> { get; set; }<#= prop.DefaultInit #>
<# } #>
// Navigation properties
<# foreach (var nav in navigations) { #>
public virtual <#= nav.TypeName #> <#= nav.Name #> { get; set; } = null!;
<# } #>
}
Categories (Id, Name, Description), Products (Id, Name, Price, CategoryId FK, IsActive BIT DEFAULT 1), Suppliers (Id, Name, Email UNIQUE)dotnet ef dbcontext scaffold ... --output-dir Models --context ShopDbContext --no-onconfiguringShopDbContext.cs — знайдіть OnModelCreating. Скільки рядків конфігурації?Напишіть два тести:
EnsureCreated → додайте новий стовпець у SQL → EnsureCreated знову → перевірте чи стовпець з'явився (Спойлер: ні)MigrateAsync + Code-First міграція → додайте стовпець у C# → нова міграція → MigrateAsync → перевірте чи стовпець єПоясніть різницю у поведінці.
Ваша legacy база має 50 таблиць: 10 бізнес-таблиць і 40 системних. Scaffold лише бізнес-таблиці через --table. Перевірте що:
Виконайте scaffold двічі:
Models/FluentApi/--data-annotations → збережіть у Models/Annotations/Порівняйте результати:
OnModelCreating?Отримайте scaffold з --no-onconfiguring. Налаштуйте DbContext через:
Program.cs: AddDbContext<ShopDbContext>(options => options.UseSqlServer(...))appsettings.json: "ConnectionStrings": { "DefaultConnection": "..." }IDesignTimeDbContextFactory<ShopDbContext>: для dotnet ef команд без запуску ApiРеалізуйте кастомний EntityType.t4 що:
/// <summary> коментарі (з db_comment якщо є, інакше ім'я таблиці)[Obsolete] до entity чиї таблиці мають суфікс _Legacy або _OldIAuditableEntity якщо entity має поля CreatedAt і UpdatedAt// <auto-generated> заголовокПеревірте: після scaffold --force всі entity відповідають новому шаблону.
EnsureCreated — для тестів і прототипів. MigrateAsync — для productionpartial class entity та DbContext. Параметр --no-onconfiguring запобігає витоку connection stringpartial class entity (властивості + навігаційні), DbContext з OnModelCreating (Fluent API конфігурації)Microsoft.EntityFrameworkCore.Templates, файли CodeTemplates/EFCore/EntityType.t4 і DbContext.t4. Виправдані при 50+ таблицях і частому re-scaffoldуУ другій частині — Partial Classes для розширення згенерованих entity, Re-scaffolding workflow, Database Schema Comparison Tools та Multi-database scenarios.
Міграції — Просунуті Сценарії (Частина 2)
Multiple DbContext — ізоляція міграцій і власні MigrationHistory таблиці. HasDefaultSchema для розподілу за схемами. Handling Breaking Changes — стратегії для production без простою. Database-First workflow і Reverse Engineering.
Управління Схемою та Database-First (Частина 2)
Partial Classes для безпечного розширення згенерованих entity — бізнес-логіка без ризику перезапису. Re-scaffolding workflow при зміні БД. Database schema comparison tools. Multi-database scenarios у одному застосунку.