Lab 17
EF Core: основи
DbContext, migrations, Code-First, seeding
Лаба 17 — Entity Framework Core: основи
Мета
Підключити до проєкту реляційну базу даних через ORM Entity Framework Core: описати таблиці C#-класами, налаштувати відображення через Fluent API, перетворення складних типів (Value Conversion), створити схему БД міграцією і заповнити її початковими даними.
Контекст
Досі клініка існує тільки в оперативній пам'яті: кожен запуск програми починається з нуля (сесія з Лаби 12 зберігає лише пацієнтів у текстовий файл). Реальна система потребує більшого: дані мають зберігатися між запусками, кілька користувачів мають бачити ті самі записи, пошук має масштабуватися до тисяч записів.
Найпоширеніше рішення — реляційна база даних. Працювати з нею напряму через SQL-рядки — це ручне формування запитів і ручне перетворення рядків таблиці в C#-об'єкти. Object-Relational Mapper (ORM) автоматизує цю роботу. Entity Framework Core — офіційний ORM від Microsoft: описує таблиці звичайними класами, сам генерує SQL, відстежує зміни об'єктів і керує схемою БД через міграції.
Структура проєкту на початку лаби
Це результат Лаби 16 — стан main після її злиття:
oop-course/ ← гілка main (після злиття Лаби 16)
├── .gitignore
├── oop-course.slnx
└── ClinicApp/
├── ClinicApp.csproj
├── Program.cs
├── Clinic.cs
├── Enums/ (4 файли)
├── Models/
│ ├── Patient.cs
│ ├── Doctor.cs
│ └── … ще 14 файлів без змін
├── Managers/ (13 файлів)
├── Utils/ (12 файлів)
├── Interfaces/ (4 файли)
├── Comparators/ (4 файли)
├── Attributes/ (3 файли)
├── Events/ (4 файли)
├── Extensions/ (3 файли)
└── UI/ (1 файл)Структуру наприкінці лаби (з позначками, що створюється і змінюється в кожній задачі) наведено в розділі «Структура проєкту наприкінці лаби» перед перевіркою.
Ключові поняття EF Core
DbContext— посередник між кодом і БД: підключається до бази (OnConfiguring), оголошує таблиці (DbSet<T>), описує правила відображення (OnModelCreating) і відстежує зміни — під часSaveChanges()сам формуєINSERT,UPDATE,DELETE.DbSet<T>— «таблиця» в термінах C#. LINQ-запит доDbSetEF перетворює на SQL:context.Patients.Where(p => p.LastName == "Коваль")→SELECT … WHERE LastName = 'Коваль'.- Fluent API — конфігурація відображення в
OnModelCreatingланцюжком викликів. На відміну від атрибутів ([Required]), тримає налаштування БД окремо від моделі: модель лишається чистим доменним класом. У цій лабі використовуємо саме Fluent API. - Value Conversion — правило, як зберегти C#-тип, що не має прямого відповідника в SQL:
enum BloodType→ рядок"APositive",struct WorkSchedule→ рядок"8-17". - Міграція — автоматично згенерований клас зі змінами схеми БД (методи
Up()/Down()). Цикл роботи: змінили модель →dotnet ef migrations add Назва→dotnet ef database update.
Що нового дозволено (і тільки воно)
- пакети EF Core і провайдер SQL Server (LocalDB);
DbContext,DbSet<T>, Fluent API уOnModelCreating,ValueConverter;- міграції (
dotnet ef); private setі конструктор без параметрів для потреб EF.
Крок 1. Гілка
Робочий процес (повністю — Git Воркшоп): лаба = гілка
Lab-XXвідmain, коміт на кожне завдання (LabXX TaskYY), у кінці — злиття вmain.
Проєкт ClinicApp/ уже існує. Тут лише нова гілка від main:
git checkout main
git checkout -b Lab-17Коміт — на кожне завдання (Lab17 TaskNN).
Ваш домен
За замовчуванням виконуйте завдання як написано (домен «клініка»). Для власного домену дивіться таблицю «Адаптація до вашого домену» в кінці кожного завдання.
Як користуватися підказками
Підказки — напрям думки, не готовий код. «Що реалізувати» і «Специфікація» кажуть що; підказки — як міркувати; блок 📖 Документація — де прочитати синтаксис. Спершу документація і власна спроба.
Задача 1. Пакети EF Core і `ClinicDbContext` ⭐⭐
Умова
Підключіть EF Core до проєкту і налаштуйте з'єднання з локальною БД. Базовий пакет Microsoft.EntityFrameworkCore не знає ні про SQL Server, ні про SQLite — конкретний провайдер підключається окремим пакетом.
Що реалізувати:
- Додати в
ClinicAppтри пакети версії8.0.0(підnet8.0):Microsoft.EntityFrameworkCore,Microsoft.EntityFrameworkCore.SqlServer,Microsoft.EntityFrameworkCore.Design. - Один раз на комп'ютері встановити інструмент міграцій
dotnet-ef(команда нижче). - Створити теку
ClinicApp/Data/і класClinicDbContext : DbContextз таблицямиPatientsіDoctors. - У
OnConfiguringпідключитися до LocalDB рядком підключення зі специфікації.
Специфікація
dotnet add ClinicApp package Microsoft.EntityFrameworkCore --version 8.0.0
dotnet add ClinicApp package Microsoft.EntityFrameworkCore.SqlServer --version 8.0.0
dotnet add ClinicApp package Microsoft.EntityFrameworkCore.Design --version 8.0.0
dotnet tool install --global dotnet-ef --version 8.0.0Член ClinicDbContext |
Опис |
|---|---|
DbSet<Patient> Patients |
таблиця пацієнтів |
DbSet<Doctor> Doctors |
таблиця лікарів |
OnConfiguring(...) |
UseSqlServer(рядок підключення) |
Рядок підключення до LocalDB:
Server=(localdb)\mssqllocaldb;Database=ClinicApp;Trusted_Connection=True;TrustServerCertificate=True;Приклад
<ItemGroup>
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="8.0.0" />
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="8.0.0" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="8.0.0" />
</ItemGroup>Підказки
- Версія EF Core має збігатися з версією .NET у
ClinicApp.csproj:net8.0→8.0.x. dotnet-ef— глобальний інструмент командного рядка, а не пакет проєкту: без нього командаdotnet efне знайдеться. Перевірка:dotnet ef --version.- LocalDB — вбудований SQL Server для розробки (ставиться разом із Visual Studio); запускається автоматично при першому підключенні.
UseSqlServer— метод розширення з пакета провайдера: саме тому він з'являється лише після встановлення…SqlServer.
📖 Документація:
Адаптація до вашого домену
| Клініка | Готель | Ресторан | Університет | Прокат авто | Бібліотека | Спортзал |
|---|---|---|---|---|---|---|
ClinicDbContext |
HotelDbContext |
RestaurantDbContext |
UniversityDbContext |
RentalDbContext |
LibraryDbContext |
GymDbContext |
Patients, Doctors |
Guests, Staff |
Customers, Waiters |
Students, Lecturers |
Clients, Managers |
Readers, Librarians |
Members, Trainers |
Коміт
git add ClinicApp/ClinicApp.csproj ClinicApp/Data/ClinicDbContext.cs
git commit -m "Lab17 Task01"Задача 2. Відображення `Patient` через Fluent API ⭐⭐
Умова
Опишіть, як клас Patient зберігається в таблиці Patients. Спершу підготуйте сам клас до роботи з EF, потім налаштуйте відображення в OnModelCreating.
Що реалізувати:
- У
PatientзмінитиIdна{ get; private set; }: EF має записати в нього значення з БД післяINSERT, а ззовні змінитиIdі далі неможливо. - Переконатися, що в
Patientє конструктор без параметрів (з Лаби 03) — EF використовує його, завантажуючи об'єкт із БД. - У
ClinicDbContext.OnModelCreatingналаштуватиPatientза специфікацією.
Специфікація
| Що | Налаштування |
|---|---|
| Таблиця | Patients |
| Ключ | Id, значення генерує БД (IDENTITY); значення з лічильника _nextId при вставці ігнорується |
FirstName, LastName |
обов'язкові, до 100 символів |
Phone |
до 10 символів |
Email |
до 100 символів |
BloodType |
зберігається рядком ("APositive"), а не числом |
| Індекс | за LastName, ім'я IX_Patients_LastName |
Приклад
modelBuilder.Entity<Patient>(entity =>
{
entity.ToTable("Patients");
entity.HasKey(p => p.Id);
entity.Property(p => p.FirstName).HasMaxLength(100).IsRequired();
// … решта за специфікацією
});Підказки
ValueGeneratedOnAdd()означає «значення генерує БД під часINSERT» — це і є IDENTITY в SQL Server.- Пастка з лічильником. Конструктор
Patientуже присвоюєIdз_nextId. Побачивши ненульовийId, EF спробує вставити його явно — і SQL Server відмовить (вставка в IDENTITY-стовпець вимкнена). Скажіть EF ігнорувати клієнтське значення при вставці:Metadata.SetBeforeSaveBehavior(PropertySaveBehavior.Ignore)для властивостіId. - Enum без перетворення зберігається числом (0, 1, 2…): змінили порядок значень — дані в БД стали некоректними.
HasConversion<string>()зберігає назву. HasKeyможна не писати — EF знайдеIdза іменем. Але явна конфігурація читається краще.
📖 Документація:
- Fluent API: конфігурація моделі
- Згенеровані значення
- Явні значення для згенерованих властивостей
- Перетворення значень
Адаптація до вашого домену
| Клініка | Готель | Ресторан | Університет | Прокат авто | Бібліотека | Спортзал |
|---|---|---|---|---|---|---|
Patient → Patients, BloodType рядком |
Guest → Guests, RoomType рядком |
Customer → Customers, DishCategory рядком |
Student → Students, Faculty рядком |
Client → Clients, CarClass рядком |
Reader → Readers, BookGenre рядком |
Member → Members, FitnessLevel рядком |
Коміт
git add ClinicApp/Data/ClinicDbContext.cs ClinicApp/Models/Patient.cs
git commit -m "Lab17 Task02"Задача 3. Відображення `Doctor` і перетворення `WorkSchedule` ⭐⭐⭐
Умова
Опишіть відображення Doctor. Особлива частина — розклад WorkSchedule: зберігати двопольну структуру окремою таблицею надлишково, тому збережіть її одним рядком "8-17" і відновлюйте назад при читанні.
Що реалізувати:
- Підготувати
Doctorтак само, якPatient:Id { get; private set; }, конструктор без параметрів. - Налаштувати
DoctorуOnModelCreatingза специфікацією. - Для
ScheduleстворитиValueConverter<WorkSchedule, string>: розклад →"Start-End"і назад. Зворотний розбір винести в окремий статичний метод.
Специфікація
| Що | Налаштування |
|---|---|
| Таблиця | Doctors |
| Ключ | Id, IDENTITY; значення з _nextId ігнорується (як у Patient) |
FirstName, LastName, LicenseNumber |
обов'язкові, до 100 символів |
Phone |
до 10 символів |
Speciality |
рядком |
Schedule |
один рядок "8-17" через ValueConverter |
Приклад
Doctors
Id | FirstName | LastName | Speciality | Schedule
1 | Олег | Сидоренко | Cardiology | 8-16Підказки
- Чому окремий метод для розбору. Лямбди в
HasConversionкомпілюються в дерева виразів — обмежену підмножину C#. Виклики з необов'язковими параметрами (якint.ParseзIFormatProvider) там можуть не компілюватися (помилкаCS0854). Статичний метод розбору, викликаний із лямбди конвертера, цю проблему знімає. ValueConverter<TModel, TProvider>— окремий об'єкт: перший аргумент перетворює модель у значення для БД, другий — назад. Передайте його вHasConversion(converter).- Розбір: розділити рядок за
-, перетворити дві частини на числа і створитиWorkSchedule— валідація з Лаби 05 спрацює автоматично.
📖 Документація:
Адаптація до вашого домену
| Клініка | Готель | Ресторан | Університет | Прокат авто | Бібліотека | Спортзал |
|---|---|---|---|---|---|---|
WorkSchedule → "8-17" |
BookingPeriod → "14-12" |
ServiceHours → "10-23" |
LectureSlot → "9-11" |
RentalPeriod → "9-18" |
ShiftSchedule → "9-18" |
TrainingSlot → "7-9" |
Коміт
git add ClinicApp/Data/ClinicDbContext.cs ClinicApp/Models/Doctor.cs
git commit -m "Lab17 Task03"Задача 4. Міграція і `DbSeeder` ⭐⭐
Умова
Створіть схему БД міграцією і заповніть її початковими даними. Початкові дані — окремий клас, а не Program.cs: Program.cs керує навігацією, а сідер відповідає за початковий стан БД.
Що реалізувати:
- Створити першу міграцію
InitialCreateі застосувати її до БД (команди нижче). Відкрити згенерований клас і переконатися, щоUp()створює таблиці за вашою конфігурацією. - Створити статичний клас
DbSeederуClinicApp/Data/з методомSeed(ClinicDbContext context): додає 5 пацієнтів і 5 лікарів різних спеціальностей і зберігає зміни. - Сідер ідемпотентний: якщо пацієнти в БД уже є — нічого не додає.
- У
Program.csна старті створити контекст і викликатиDbSeeder.Seed.
Специфікація
dotnet ef migrations add InitialCreate --project ClinicApp
dotnet ef database update --project ClinicAppЧлен DbSeeder |
Опис |
|---|---|
Seed(ClinicDbContext context) |
якщо таблиця Patients не порожня — вихід; інакше 5 пацієнтів + 5 лікарів, SaveChanges() |
Приклад
SELECT Id, FirstName, LastName, BloodType FROM Patients;
1 | Іван | Петренко | APositive
2 | Олена | Коваль | BNegative
…Підказки
- Перевірку «чи є дані» робіть через
Any()— він генеруєSELECT TOP 1, а не завантажує всю таблицю. - Контекст створюйте з
using: він тримає з'єднання з БД і має бути звільнений. - Не використовуйте
Database.EnsureCreated()разом із міграціями: БД, створену так, наступна міграція спробує створити ще раз і впаде. Схему створює лишеdotnet ef database update. - Дані перевірте через SQL Server Object Explorer у Visual Studio.
📖 Документація:
Адаптація до вашого домену
| Клініка | Готель | Ресторан | Університет | Прокат авто | Бібліотека | Спортзал |
|---|---|---|---|---|---|---|
| 5 пацієнтів + 5 лікарів | 5 гостей + 5 працівників | 5 клієнтів + 5 офіціантів | 5 студентів + 5 викладачів | 5 клієнтів + 5 менеджерів | 5 читачів + 5 бібліотекарів | 5 учасників + 5 тренерів |
Коміт
git add ClinicApp/Data/DbSeeder.cs ClinicApp/Migrations/ ClinicApp/Program.cs
git commit -m "Lab17 Task04"Структура проєкту наприкінці лаби
Так має виглядати ClinicApp/, коли всі завдання виконано:
oop-course/ ← гілка Lab-17 (після злиття — main)
├── .gitignore
├── oop-course.slnx
└── ClinicApp/
├── ClinicApp.csproj ✏ Т1
├── Program.cs ✏ Т4
├── Clinic.cs
├── Enums/ (4 файли)
├── Models/
│ ├── Patient.cs ✏ Т2
│ ├── Doctor.cs ✏ Т3
│ └── … ще 14 файлів без змін
├── Managers/ (13 файлів)
├── Utils/ (12 файлів)
├── Interfaces/ (4 файли)
├── Comparators/ (4 файли)
├── Attributes/ (3 файли)
├── Events/ (4 файли)
├── Extensions/ (3 файли)
├── UI/ (1 файл)
├── Data/
│ ├── ClinicDbContext.cs 🆕 Т1 ✏ Т2 Т3
│ └── DbSeeder.cs 🆕 Т4
└── Migrations/ (3 файли — генерує EF) 🆕 Т4Легенда: 🆕 — новий файл · ✏ — змінено вміст · Тn — номер задачі, у якій ви працюєте з файлом. Файли без позначки лишились такими, як були після Лаби 16.
Назви файлів наведено для домену «клініка»; у власному домені назви ваші — важливо, що саме створюється й змінюється.
Перевірка перед здачею
dotnet build ClinicApp
dotnet run --project ClinicAppПереконайтесь, що:
- Структура проєкту збігається зі схемою вище
-
dotnet ef --versionпоказує версію 8.0.x - У БД
ClinicAppє таблиціPatients,Doctorsі__EFMigrationsHistory -
BloodTypeіSpecialityзбережені рядками,Schedule— рядком виду8-17 - Після першого запуску в БД 5 пацієнтів і 5 лікарів; після повторного — так само 5 (сідер ідемпотентний)
- У коді немає
EnsureCreated()
Питання для самоперевірки
- Unit of Work. Усі зміни в одному
DbContextзберігаються разом черезSaveChanges(). Як це пов'язано з транзакціями в БД? - Fluent API чи атрибути. Атрибути (
[Required],[MaxLength]) простіші, але змішують модель із деталями БД. Коли ви обрали б атрибути? - Пошкоджені дані. Конструктор
WorkScheduleперевіряєstart < end. Що станеться, якщо в БД рядок"17-8"? Як від цього захиститись? - Два лічильники.
_nextIdуPatientі IDENTITY у БД рахують незалежно. Чому EF має ігнорувати значення з лічильника при вставці? Що було б без цього? - Міграція як версія схеми. Що буде, якщо один розробник застосував міграцію
AddAppointments, а інший ні — і обидва запускають програму з однією БД? using var context. ЧомуDbContextреалізуєIDisposable? Чи зберігаються незбережені зміни під часDispose()?
Статус гілки
Після всіх 4 завдань (кожне — окремий коміт Lab17 TaskNN на гілці Lab-17):
git push -u origin Lab-17
git checkout main
git merge --no-ff Lab-17 -m "Merge Lab-17: EF Core Basics"
git pushНаступна лаба:
git checkout main→git checkout -b Lab-18.