Lab 12
File I/O
логування, CSV, збереження стану
Лаба 12 — File I/O (Робота з файлами)
Мета
Навчитись читати та писати файли в C#: від простих File.AppendAllText до StreamWriter з форматуванням, CSV-парсингу з обробкою помилок та збереження стану між запусками програми.
Контекст
Після Лаби 11 система вміє валідувати дані через рефлексію. Але все, що ввів користувач, зникає при закритті програми. Ця лаба додає персистентність: логування дій у файл, експорт звітів, імпорт даних з CSV та збереження сесії між запусками.
Структура проєкту на початку лаби
Це результат Лаби 11 — стан main після її злиття:
oop-course/ ← гілка main (після злиття Лаби 11)
├── .gitignore
├── oop-course.slnx
└── ClinicApp/
├── ClinicApp.csproj
├── Program.cs
├── Clinic.cs
├── Enums/ (4 файли)
├── Models/ (15 файлів)
├── Managers/ (9 файлів)
├── Utils/
│ ├── ClinicFormatter.cs
│ ├── ClinicValidator.cs
│ ├── FormBuilder.cs
│ ├── ModelValidator.cs
│ └── ValidationResult.cs
├── Interfaces/ (4 файли)
├── Comparators/ (4 файли)
└── Attributes/ (3 файли)Структуру наприкінці лаби (з позначками, що створюється і змінюється в кожній задачі) наведено в розділі «Структура проєкту наприкінці лаби» перед перевіркою.
Що нового дозволено (і тільки воно)
- клас
File:AppendAllText,ReadAllLines,Exists,Delete; StreamWriterу блоціusing;Path.CombineіDirectory.CreateDirectory;Encoding.UTF8,DateTime.ParseExact,Enum.Parse.
Досі заборонено: LINQ (Лаба 14), делегати й лямбди (Лаби 13–15).
Крок 1. Гілка
Робочий процес (повністю — Git Воркшоп): лаба = гілка
Lab-XXвідmain, коміт на кожне завдання (LabXX TaskYY), у кінці — злиття вmain.
Проєкт ClinicApp/ уже існує. Тут лише нова гілка від main:
git checkout main
git checkout -b Lab-12Коміт — на кожне завдання (Lab12 TaskNN).
Ваш домен
За замовчуванням виконуйте завдання як написано (домен «клініка»). Для власного домену дивіться таблицю «Адаптація до вашого домену» в кінці кожного завдання.
Як користуватися підказками
Підказки — напрям думки, не готовий код. «Що реалізувати» і «Специфікація» кажуть що; підказки — як міркувати; блок 📖 Документація — де прочитати синтаксис. Спершу документація і власна спроба.
Задача 1. `ClinicLogger`: журнал дій у файлі ⭐⭐
Умова
Клініці потрібен журнал подій — файл clinic.log, куди записуються дії системи з часовою міткою. Треба також мати змогу переглянути останні N рядків, не відкриваючи файл вручну.
Що реалізувати:
- Клас
ClinicLoggerуClinicApp/Utils/з методами зі специфікації. Кожен запис дописується в кінець файлу. - У
Clinic.csдодати властивістьLoggerі створитиClinicLoggerу конструкторі. - У
Program.csзаписувати в лог ключові дії: додавання пацієнта й лікаря, запис на прийом, скасування й завершення запису, оплату (LogInfo); помилки введення вcatch(LogWarning). - У головному меню додати пункт
10— «Файли» з двома пунктами: «Останні рядки лога» (запитує N) і «Очистити лог».
Специфікація
Член ClinicLogger |
Опис |
|---|---|
конструктор (string logPath = "clinic.log") |
шлях до файлу лога |
LogInfo(string message) |
запис рівня INFO |
LogWarning(string message) |
запис рівня WARN |
LogError(string message) |
запис рівня ERROR |
GetLastLines(int n) |
string[] — останні n рядків; порожній масив, якщо файлу немає |
Clear() |
очистити лог |
Exists() |
bool — чи існує файл |
Формат рядка: [yyyy-MM-dd HH:mm:ss] [LEVEL] message.
Приклад
[2026-10-15 10:02:11] [INFO ] Додано пацієнта #6: Марія Ткач
[2026-10-15 10:03:40] [INFO ] Запис #9 створено: пацієнт #6 → лікар #2
[2026-10-15 10:04:05] [WARN ] Помилка введення: Телефон має містити рівно 10 цифр.Підказки
File.AppendAllText(path, text, encoding)— дописує в кінець файлу, не перезаписує. Якщо файл не існує — створює автоматично.File.ReadAllLines(path, encoding)— зчитує всі рядки у масивstring[]. Для великих файлів це дорого, але для лога прийнятно.Encoding.UTF8— обов'язково для кирилиці (using System.Text;).Environment.NewLine— правильний перенос рядка для поточної ОС.- Останні N рядків: визначте, скільки рядків пропустити з початку (не менше нуля), і скопіюйте решту в новий масив.
- Один приватний метод запису, який викликають
LogInfo/LogWarning/LogError, — щоб формат рядка був в одному місці.
📖 Документація:
Адаптація до вашого домену
| Клініка | Готель | Ресторан | Університет | Прокат авто | Бібліотека | Спортзал |
|---|---|---|---|---|---|---|
ClinicLogger → clinic.log |
HotelLogger → hotel.log |
RestaurantLogger → restaurant.log |
UniversityLogger → uni.log |
RentalLogger → rental.log |
LibraryLogger → library.log |
GymLogger → gym.log |
Коміт
git add ClinicApp/Utils/ClinicLogger.cs ClinicApp/Clinic.cs ClinicApp/Program.cs
git commit -m "Lab12 Task01"Задача 2. `ClinicExporter`: звіти у файли через `StreamWriter` ⭐⭐
Умова
Адміністратор хоче отримувати готові текстові звіти у файлах — із заголовком, датою генерації і підсумком. Файли зберігаються в теці з датою: reports/2026-10-15/.
Що реалізувати:
- Клас
ClinicExporterуClinicApp/Utils/: отримуєClinicі базову теку через конструктор. - Чотири методи експорту зі специфікації; кожен пише окремий файл і повертає шлях до нього.
- Метод
ExportAll()— викликає всі чотири експорти. - У
Clinic.csдодати властивістьExporter; у меню «Файли» додати пункт3— «Експортувати всі звіти» (виводить шляхи створених файлів).
Специфікація
Член ClinicExporter |
Файл | Вміст |
|---|---|---|
конструктор (Clinic clinic, string baseDir = "reports") |
||
ExportPatients() |
patients.txt |
усі пацієнти |
ExportAppointments() |
appointments.txt |
усі записи |
ExportBilling() |
billing.txt |
неоплачені записи і загальний борг |
ExportTreatmentPlans() |
treatment_plans.txt |
усі плани лікування |
ExportAll() |
викликає всі чотири, повертає string[] шляхів |
Кожен файл: заголовок з назвою звіту і датою генерації, роздільник, нумеровані рядки даних, підсумок (кількість). Тека — reports/yyyy-MM-dd/, створюється автоматично.
Приклад
=== Пацієнти — звіт ===
Згенеровано: 15.10.2026 10:20
----------------------------------------
1. [1] Іван Петренко | 41 рік | A+ | (050) 123-4567
2. [2] Олена Коваль | 33 роки | B- | (067) 234-5678
----------------------------------------
Всього: 2Підказки
StreamWriter(path, false, encoding)—falseозначає перезаписати файл, якщо він існує.using StreamWriter writer = new StreamWriter(...)автоматично закриває файл при виході з блоку. Безusingфайл може лишитись відкритим.Directory.CreateDirectory(dir)створює теку і всі батьківські теки; не кидає виняток, якщо тека вже існує.Path.Combine("reports", "2026-10-15", "patients.txt")склеює шлях правильно для будь-якої ОС — не збирайте шлях конкатенацією з/.- Створення теки з датою винесіть в один приватний метод — його використовують усі чотири експорти.
{i + 1,3}у рядку з інтерполяцією вирівнює номер по правому краю в 3 символи.
📖 Документація:
Адаптація до вашого домену
| Клініка | Готель | Ресторан | Університет | Прокат авто | Бібліотека | Спортзал |
|---|---|---|---|---|---|---|
ClinicExporter |
HotelExporter |
RestaurantExporter |
UniversityExporter |
RentalExporter |
LibraryExporter |
GymExporter |
ExportPatients/Appointments/Billing |
ExportGuests/Bookings/Invoices |
ExportCustomers/Reservations/Orders |
ExportStudents/Courses/Grades |
ExportClients/Rentals/Invoices |
ExportReaders/Loans/Fines |
ExportMembers/Sessions/Payments |
Коміт
git add ClinicApp/Utils/ClinicExporter.cs ClinicApp/Clinic.cs ClinicApp/Program.cs
git commit -m "Lab12 Task02"Задача 3. `CsvImporter`: імпорт пацієнтів з помилковими рядками ⭐⭐⭐
Умова
Адміністратор отримав список нових пацієнтів у форматі CSV і хоче завантажити їх одним файлом. CSV може містити помилкові рядки — їх треба пропустити і повідомити про кожен окремо, не зупиняючи імпорт.
Що реалізувати:
- Клас
ImportResultуClinicApp/Utils/— підсумок імпорту (специфікація нижче). - Клас
CsvImporterуClinicApp/Utils/з методомImportPatients(Clinic clinic, string filePath): читає файл, створює пацієнтів і додає їх уclinic.Patients. - Перший рядок (заголовок) і порожні рядки пропускати; помилковий рядок записувати в
ImportResultз номером і причиною, імпорт продовжувати. - У меню «Файли» додати пункт
4— «Імпорт пацієнтів з CSV»: запитує шлях до файлу і виводить підсумок.
Специфікація
Формат CSV:
FirstName,LastName,DateOfBirth,BloodType,Phone
Іван,Петренко,15.03.1985,APositive,0501234567
Олена,Коваль,22.07.1992,BNegative,0672345678
НеПравильний рядок
,Ткач,01.01.2000,,Член ImportResult |
Опис |
|---|---|
Imported |
кількість імпортованих (private set) |
Skipped |
кількість пропущених (private set) |
Errors |
список помилок лише для читання |
AddSuccess() |
+1 до Imported |
AddError(int lineNumber, string reason) |
+1 до Skipped, помилка "Рядок N: причина" |
Print() |
підсумок і всі помилки |
Якщо файлу немає — одна помилка з номером рядка 0.
Приклад
Імпортовано: 2 | Пропущено: 2
Рядок 4: Рядок має 1 поле замість 5.
Рядок 5: Ім'я не може бути порожнім.Підказки
try/catch— навколо обробки одного рядка, а не навколо всього циклу: помилка в рядку 3 не повинна зупинити рядки 4, 5, 6.line.Split(',')повертаєstring[]. Перевірте кількість полів перед зверненням доparts[2]— рядок може мати менше полів.DateTime.ParseExact(str, "dd.MM.yyyy", CultureInfo.InvariantCulture)— суворий парсинг дати (using System.Globalization;).Enum.Parse(typeof(BloodType), str)кидає виняток, якщо рядок не відповідає жодному значенню enum —catchйого перехопить.- Валідація пацієнта з Лаби 05 спрацює сама: некоректне ім'я чи телефон кине виняток у конструкторі.
- Номер рядка для людини — індекс у масиві плюс один.
📖 Документація:
Адаптація до вашого домену
| Клініка | Готель | Ресторан | Університет | Прокат авто | Бібліотека | Спортзал |
|---|---|---|---|---|---|---|
ImportPatients |
ImportGuests |
ImportCustomers |
ImportStudents |
ImportClients |
ImportReaders |
ImportMembers |
Коміт
git add ClinicApp/Utils/ImportResult.cs ClinicApp/Utils/CsvImporter.cs ClinicApp/Program.cs
git commit -m "Lab12 Task03"Задача 4. `SessionManager`: збереження пацієнтів між запусками ⭐⭐⭐
Умова
Зараз при кожному запуску програми дані починаються з нуля — пацієнти, яких додав користувач, зникають. Збережіть список пацієнтів у файл session.dat при виході і відновіть його при наступному старті.
Що реалізувати:
- Клас
SessionManagerуClinicApp/Utils/з методами зі специфікації. - У
Clinic.csдодати властивістьSession. - У
Program.csна старті (після початкових даних): якщо сесія існує — запитати «Завантажити? (y/n)» і завантажити. - У
Program.csпри виході (пункт0): запитати «Зберегти сесію? (y/n)» і зберегти.
Специфікація
Формат файлу:
[PATIENTS]
Іван,Петренко,15.03.1985,APositive,0501234567
Олена,Коваль,22.07.1992,BNegative,0672345678
[END]Член SessionManager |
Опис |
|---|---|
конструктор (string sessionPath = "session.dat") |
шлях до файлу сесії |
Exists() |
bool — чи є збережена сесія |
Save(Clinic clinic) |
перезаписує файл: секція [PATIENTS], рядок на пацієнта, [END] |
Load(Clinic clinic) |
int — скільки пацієнтів додано; 0, якщо файлу немає |
Правила Load: рядки до першої секції і порожні — ігнорувати; пошкоджений рядок — пропустити; пацієнта, який уже є в системі (те саме ім'я, прізвище і дата народження), — не додавати вдруге.
Приклад
Знайдено збережену сесію. Завантажити? (y/n): y
Завантажено 2 пацієнтів.
…
Зберегти сесію? (y/n): y
Сесію збережено.Підказки
- Рядок, що починається з
[, — заголовок секції. Запам'ятайте поточну секцію і обробляйте наступні рядки відповідно до неї — так у майбутньому легко додати[APPOINTMENTS]. - Сесія зберігає всіх пацієнтів, включно з початковими. Тому без перевірки «вже є» кожен запуск дублюватиме початкових пацієнтів.
- Пошкоджений рядок пропускайте в
catch— краще завантажити 9 з 10 пацієнтів, ніж впасти. _nextIdуPatient— статичний: завантажені пацієнти отримають новіId. Для цієї лаби це нормально.{p.DateOfBirth:dd.MM.yyyy}— форматування дати в рядку з інтерполяцією.
📖 Документація:
Адаптація до вашого домену
| Клініка | Готель | Ресторан | Університет | Прокат авто | Бібліотека | Спортзал |
|---|---|---|---|---|---|---|
[PATIENTS] |
[GUESTS] |
[CUSTOMERS] |
[STUDENTS] |
[CLIENTS] |
[READERS] |
[MEMBERS] |
Коміт
git add ClinicApp/Utils/SessionManager.cs ClinicApp/Clinic.cs ClinicApp/Program.cs
git commit -m "Lab12 Task04"Структура проєкту наприкінці лаби
Так має виглядати ClinicApp/, коли всі завдання виконано:
oop-course/ ← гілка Lab-12 (після злиття — main)
├── .gitignore
├── oop-course.slnx
└── ClinicApp/
├── ClinicApp.csproj
├── Program.cs ✏ Т1 Т2 Т3 Т4
├── Clinic.cs ✏ Т1 Т2 Т4
├── Enums/ (4 файли)
├── Models/ (15 файлів)
├── Managers/ (9 файлів)
├── Utils/
│ ├── ClinicFormatter.cs
│ ├── ClinicValidator.cs
│ ├── FormBuilder.cs
│ ├── ModelValidator.cs
│ ├── ValidationResult.cs
│ ├── ClinicLogger.cs 🆕 Т1
│ ├── ClinicExporter.cs 🆕 Т2
│ ├── ImportResult.cs 🆕 Т3
│ ├── CsvImporter.cs 🆕 Т3
│ └── SessionManager.cs 🆕 Т4
├── Interfaces/ (4 файли)
├── Comparators/ (4 файли)
└── Attributes/ (3 файли)Легенда: 🆕 — новий файл · ✏ — змінено вміст · Тn — номер задачі, у якій ви працюєте з файлом. Файли без позначки лишились такими, як були після Лаби 11.
Назви файлів наведено для домену «клініка»; у власному домені назви ваші — важливо, що саме створюється й змінюється.
Перевірка перед здачею
dotnet build ClinicApp
dotnet run --project ClinicAppПереконайтесь, що:
- Структура проєкту збігається зі схемою вище
- Після додавання пацієнта в
clinic.logз'являється рядок[yyyy-MM-dd HH:mm:ss] [INFO ] … - «Файли» → «Останні рядки лога» показує N останніх записів
- Папка
reports/yyyy-MM-dd/створюється автоматично; у кожному звіті — заголовок, дата генерації, підсумок - CSV з 1 помилковим рядком із 5:
Імпортовано: 4 | Пропущено: 1+ деталі - Після збереження сесії, перезапуску і «y» — додані раніше пацієнти знову в системі, початкові не дублюються
- Пошкоджений рядок у
session.datне зупиняє завантаження решти
Питання для самоперевірки
- Чим
File.WriteAllTextвідрізняється відFile.AppendAllText? Коли кожен з них доречний? - Чому
using StreamWriter writer = ...важливіший за простоStreamWriter writer = new ...? Що станеться, якщо не закрити потік? - Чому
Path.Combineкраще за конкатенацію рядків"reports/" + date + "/file.txt"? - У
CsvImportertry/catchобгортає обробку одного рядка, а не весь цикл. Яка різниця з точки зору поведінки? SessionManager.Loadповертаєint, а неbool. Чому це краще?File.ReadAllLinesзавантажує весь файл у пам'ять. Коли це стає проблемою і що використовувати натомість?
Статус гілки
Після всіх 4 завдань (кожне — окремий коміт Lab12 TaskNN на гілці Lab-12):
git push -u origin Lab-12
git checkout main
git merge --no-ff Lab-12 -m "Merge Lab-12: File I/O"
git pushНаступна лаба:
git checkout main→git checkout -b Lab-13.