Lab 11
Reflection & Attributes
атрибути, рефлексія, валідатор
Лаба 11 — Reflection & Attributes (Рефлексія та атрибути)
Мета
Навчитись створювати власні атрибути через спадкування від System.Attribute, зчитувати метадані типів та властивостей через рефлексію і будувати узагальнені утиліти (валідатор, конструктор форм), що працюють з будь-яким класом без явного знання про його поля.
Контекст
Після Лаби 10 система має повний аналітичний модуль. Ця лаба додає плани лікування (TreatmentPlan) як новий тип даних та інструментарій рефлексії, що перевіряє валідність об'єктів і генерує форми введення автоматично — зчитуючи атрибути з властивостей класу під час виконання.
Структура проєкту на початку лаби
Це результат Лаби 10 — стан main після її злиття:
oop-course/ ← гілка main (після злиття Лаби 10)
├── .gitignore
├── oop-course.slnx
└── ClinicApp/
├── ClinicApp.csproj
├── Program.cs
├── Clinic.cs
├── Enums/
│ ├── AppointmentStatus.cs
│ ├── BloodType.cs
│ └── Speciality.cs
├── Models/ (14 файлів)
├── Managers/ (8 файлів)
├── Utils/
│ ├── ClinicFormatter.cs
│ └── ClinicValidator.cs
├── Interfaces/ (4 файли)
└── Comparators/ (4 файли)Структуру наприкінці лаби (з позначками, що створюється і змінюється в кожній задачі) наведено в розділі «Структура проєкту наприкінці лаби» перед перевіркою.
Що нового дозволено (і тільки воно)
- власні атрибути (
: Attribute) і[AttributeUsage]; - рефлексія:
GetType(),typeof(...),PropertyInfo(GetProperties,GetValue,SetValue,GetCustomAttribute); - обмеження
where T : new(); Convert.ChangeType.
Досі заборонено: LINQ (Лаба 14), делегати й лямбди (Лаби 13–15).
Крок 1. Гілка
Робочий процес (повністю — Git Воркшоп): лаба = гілка
Lab-XXвідmain, коміт на кожне завдання (LabXX TaskYY), у кінці — злиття вmain.
Проєкт ClinicApp/ уже існує. Тут лише нова гілка від main:
git checkout main
git checkout -b Lab-11Коміт — на кожне завдання (Lab11 TaskNN).
Ваш домен
За замовчуванням виконуйте завдання як написано (домен «клініка»). Для власного домену дивіться таблицю «Адаптація до вашого домену» в кінці кожного завдання.
Як користуватися підказками
Підказки — напрям думки, не готовий код. «Що реалізувати» і «Специфікація» кажуть що; підказки — як міркувати; блок 📖 Документація — де прочитати синтаксис. Спершу документація і власна спроба.
Задача 1. Власні атрибути ⭐
Умова
Правила валідації можна описати безпосередньо в класі — атрибутом над кожною властивістю. Замість готових атрибутів із System.ComponentModel.DataAnnotations напишіть власні з нуля.
Що реалізувати:
- Створити теку
ClinicApp/Attributes/(простір іменClinicApp.Attributes). - Створити три атрибути зі специфікації. Кожен —
sealedклас, що успадковуєAttribute, і може стояти лише на властивостях, не більше одного разу.
Специфікація
| Клас | Властивості (лише читання) | Конструктор |
|---|---|---|
RequiredAttribute |
ErrorMessage |
(string errorMessage = "Field is required.") |
MaxLengthAttribute |
Length, ErrorMessage |
(int length, string errorMessage = "") |
MinValueAttribute |
Min (double), ErrorMessage |
(double min, string errorMessage = "") |
Над кожним класом: [AttributeUsage(AttributeTargets.Property, AllowMultiple = false)].
Приклад
public class Sample
{
[Required("Назва обов'язкова.")]
[MaxLength(50, "Назва — не довше 50 символів.")]
public string Title { get; set; } = "";
[MinValue(1, "Кількість — щонайменше 1.")]
public int Quantity { get; set; }
}Підказки
- Атрибут — це клас, що успадковує
System.Attribute. При застосуванні суфіксAttributeможна опускати:[MaxLength(200)]замість[MaxLengthAttribute(200)]. [AttributeUsage]обмежує, де атрибут можна поставити.AttributeTargets.Property— тільки на властивостях.AllowMultiple = falseозначає: один атрибут одного типу на одну властивість.sealed— атрибут не підлягає подальшому спадкуванню (хороша практика).
📖 Документація:
Адаптація до вашого домену
| Клініка | Готель | Ресторан | Університет | Прокат авто | Бібліотека | Спортзал |
|---|---|---|---|---|---|---|
MaxLength на Diagnosis |
MaxLength на GuestNote |
MaxLength на DishNote |
MaxLength на CourseName |
MaxLength на VehicleModel |
MaxLength на BookTitle |
MaxLength на ExercisePlan |
MinValue на DurationDays |
MinValue на StayNights |
MinValue на GuestCount |
MinValue на Credits |
MinValue на RentalDays |
MinValue на OverdueDays |
MinValue на DurationMinutes |
Коміт
git add ClinicApp/Attributes/
git commit -m "Lab11 Task01"Задача 2. Модель `TreatmentPlan` з атрибутами ⭐⭐
Умова
Додайте нову сутність — план лікування. Правила її полів опишіть атрибутами із Задачі 1, а стан плану змінюйте лише через методи.
Що реалізувати:
- Створити
enum TreatmentStatusуClinicApp/Enums/зі значеннямиPlanned,Active,Completed,Cancelled. - Створити клас
TreatmentPlanуClinicApp/Models/з властивостями й атрибутами зі специфікації. - Додати конструктор без параметрів: присвоює
Idз лічильника. - Додати методи переходу стану
Activate(),Complete(),Cancel()— кожен повертаєbool(чи відбувся перехід).
Специфікація
| Властивість | Тип | Атрибути | Доступ |
|---|---|---|---|
Id |
int |
— | лише get, з лічильника |
PatientId |
int |
[MinValue(1, "Patient ID is required.")] |
get; set; |
Diagnosis |
string |
[Required("Diagnosis cannot be empty.")], [MaxLength(200, "Diagnosis must not exceed 200 characters.")] |
get; set; |
DurationDays |
int |
[MinValue(1, "Duration must be at least 1 day.")] |
get; set; |
Status |
TreatmentStatus |
— | get; private set;, початково Planned |
| Метод | Перехід | Інакше |
|---|---|---|
Activate() |
Planned → Active |
false |
Complete() |
Active → Completed |
false |
Cancel() |
будь-який, крім Completed і Cancelled → Cancelled |
false |
Приклад
TreatmentPlan plan = new TreatmentPlan { PatientId = 1, Diagnosis = "Гіпертонія", DurationDays = 30 };
Console.WriteLine(plan.Activate()); // True
Console.WriteLine(plan.Activate()); // False — уже Active
Console.WriteLine(plan.Complete()); // TrueПідказки
- Атрибути — це лише метадані. Самі по собі вони нічого не перевіряють — перевірку виконає
ModelValidatorу Задачі 3. - На одну властивість можна поставити кілька різних атрибутів:
[Required]і[MaxLength(200)]обидва будуть прочитані рефлексією. [Required]має сенс для рядків і посилальних типів: уintне буваєnull, тому для числових ID використано[MinValue(1)].private setдляStatus— стан змінюється тільки через методи, що зберігає правила переходів.- Ініціалізатор
new TreatmentPlan { ... }можливий, бо є конструктор без параметрів і публічні сеттери; він же знадобитьсяFormBuilderу Задачі 4.
📖 Документація:
Адаптація до вашого домену
| Клініка | Готель | Ресторан | Університет | Прокат авто | Бібліотека | Спортзал |
|---|---|---|---|---|---|---|
TreatmentPlan |
ServiceRequest |
SpecialOrder |
CourseProject |
ServiceOrder |
RestorationRequest |
TrainingProgram |
TreatmentStatus |
ServiceStatus |
OrderStatus |
ProjectStatus |
ServiceStatus |
RestorationStatus |
ProgramStatus |
Activate/Complete/Cancel |
Start/Finish/Cancel |
Confirm/Serve/Cancel |
Start/Submit/Withdraw |
Start/Return/Cancel |
Begin/Finish/Cancel |
Begin/Complete/Cancel |
Коміт
git add ClinicApp/Enums/TreatmentStatus.cs ClinicApp/Models/TreatmentPlan.cs
git commit -m "Lab11 Task02"Задача 3. `ModelValidator` через рефлексію ⭐⭐⭐
Умова
Напишіть статичний валідатор, що автоматично перевіряє будь-який об'єкт: читає атрибути з його властивостей через рефлексію. У коді валідатора не має бути жодної перевірки конкретного поля на кшталт if (plan.Diagnosis == "").
Що реалізувати:
- Клас
ValidationResultуClinicApp/Utils/— контейнер помилок (специфікація нижче). - Статичний клас
ModelValidatorуClinicApp/Utils/з методомValidate(object obj): для кожної властивості об'єкта перевіритиRequired,MaxLength,MinValueі зібрати помилки уValidationResult. - Метод
ModelValidator.PrintInfo(Type type): вивести назву типу і список його властивостей з атрибутами.
Специфікація
Член ValidationResult |
Опис |
|---|---|
IsValid |
true, якщо помилок немає |
Errors |
список помилок лише для читання (IReadOnlyList<string>) |
AddError(string error) |
додати помилку |
Print() |
вивести кожну помилку окремим рядком з позначкою [!] |
| Атрибут | Помилка, якщо… | Текст помилки |
|---|---|---|
Required |
значення null або рядок з пробілів |
"Назва: ErrorMessage" |
MaxLength |
рядок довший за Length |
"Назва: ErrorMessage" |
MinValue |
число менше за Min |
"Назва: ErrorMessage" |
Приклад
ModelValidator.Validate(new TreatmentPlan()).Print();
[!] PatientId: Patient ID is required.
[!] Diagnosis: Diagnosis cannot be empty.
[!] DurationDays: Duration must be at least 1 day.Підказки
obj.GetType()— тип конкретного об'єкта під час виконання. Відрізняється відtypeof(T)— типу, відомого на час компіляції.type.GetProperties()— масивPropertyInfo[]. КоженPropertyInfoописує одну властивість: ім'я, тип, атрибути.prop.GetValue(obj)— зчитує значення властивості з конкретного об'єкта; повертаєobject?.prop.GetCustomAttribute<RequiredAttribute>()— шукає атрибут цього типу; якщо не знайдено —null.- Для
MinValueзначення треба перетворити на число:Convert.ToDouble(value). prop.GetCustomAttributes()— усі атрибути на властивості (дляPrintInfo).
📖 Документація:
Адаптація до вашого домену
| Клініка | Готель | Ресторан | Університет | Прокат авто | Бібліотека | Спортзал |
|---|---|---|---|---|---|---|
Валідує TreatmentPlan |
Валідує ServiceRequest |
Валідує SpecialOrder |
Валідує CourseProject |
Валідує ServiceOrder |
Валідує RestorationRequest |
Валідує TrainingProgram |
Коміт
git add ClinicApp/Utils/ValidationResult.cs ClinicApp/Utils/ModelValidator.cs
git commit -m "Lab11 Task03"Задача 4. `TreatmentPlanManager` і `FormBuilder` ⭐⭐⭐
Умова
Підключіть валідацію до менеджера планів і напишіть generic утиліту, що будує консольну форму введення автоматично — за атрибутами на властивостях.
Що реалізувати:
- Клас
TreatmentPlanManagerуClinicApp/Managers/: зберігає плани вList<TreatmentPlan>;Addспершу перевіряє план черезModelValidatorі не додає невалідний. - Статичний клас
FormBuilderуClinicApp/Utils/з методомBuild<T>() where T : new(): створює об'єкт і для кожної властивості з публічним сеттером питає значення в консолі.
Специфікація
Член TreatmentPlanManager |
Опис |
|---|---|
Add(TreatmentPlan plan) |
Валідує; невалідний — виводить помилки, повертає false; інакше додає, true |
GetById(int id) |
TreatmentPlan? |
GetByPatient(int patientId) |
TreatmentPlan[] |
GetByStatus(TreatmentStatus status) |
TreatmentPlan[] |
GetAll() |
TreatmentPlan[] |
FormBuilder.Build<T>() |
|
|---|---|
| Які властивості питає | лише з публічним сеттером (Id і Status пропускаються) |
| Підказка до поля | з атрибутів: обов'язкове, максимальна довжина, мінімум |
| Некоректне введення | повідомлення і повторний запит цього ж поля |
| Результат | заповнений об'єкт T |
Приклад
PatientId (мін. 1): 1
Diagnosis (обов'язкове, макс. 200): Гіпертонія
DurationDays (мін. 1): abc
Некоректне значення, спробуйте ще раз.
DurationDays (мін. 1): 30Підказки
where T : new()— типTповинен мати конструктор без параметрів. Без цьогоnew T()не компілюється.typeof(T)іobj.GetType()даютьType, алеtypeof(T)— статичний (час компіляції),GetType()— динамічний. УFormBuilderрезультат однаковий, боTвідомий.- Пастка
CanWrite.prop.CanWriteповертаєtrueі для приватного сеттера (як уStatus). Публічний сеттер перевіряйте черезprop.GetSetMethod()— для непублічного він повертаєnull. Convert.ChangeType(input, prop.PropertyType)перетворює рядок у потрібний тип і кидає виняток, якщо не вийшло — обгорніть уtry/catchі повторіть запит.prop.SetValue(obj, converted)записує значення у властивість через рефлексію.- Перебирайте властивості звичайним циклом із перевіркою всередині — LINQ і лямбди в цій лабі ще заборонені.
📖 Документація:
Адаптація до вашого домену
| Клініка | Готель | Ресторан | Університет | Прокат авто | Бібліотека | Спортзал |
|---|---|---|---|---|---|---|
TreatmentPlanManager |
ServiceRequestManager |
SpecialOrderManager |
CourseProjectManager |
ServiceOrderManager |
RestorationManager |
TrainingProgramManager |
FormBuilder.Build<TreatmentPlan>() |
FormBuilder.Build<ServiceRequest>() |
FormBuilder.Build<SpecialOrder>() |
FormBuilder.Build<CourseProject>() |
FormBuilder.Build<ServiceOrder>() |
FormBuilder.Build<RestorationRequest>() |
FormBuilder.Build<TrainingProgram>() |
Коміт
git add ClinicApp/Managers/TreatmentPlanManager.cs ClinicApp/Utils/FormBuilder.cs
git commit -m "Lab11 Task04"Задача 5. Меню «Плани лікування» ⭐⭐
Умова
Підключіть TreatmentPlanManager до клініки і додайте меню, що показує роботу рефлексії.
Що реалізувати:
- У
Clinic.csдодати властивістьTreatmentPlansі створитиTreatmentPlanManagerу конструкторі. - У головному меню додати пункт
9— «Плани лікування — рефлексія, атрибути». - У
Program.csдодати підменю «Плани лікування» з пунктами зі специфікації.
Специфікація
| Пункт | Дія |
|---|---|
1 — Показати всі плани |
GetAll() |
2 — Додати план лікування |
форма через FormBuilder.Build<TreatmentPlan>(), далі Add |
3 — Плани пацієнта |
запитує ID пацієнта, GetByPatient |
4 — Активувати план |
запитує ID плану, Activate() |
5 — Завершити план |
запитує ID плану, Complete() |
6 — Скасувати план |
запитує ID плану, Cancel() |
7 — Інформація про тип TreatmentPlan |
ModelValidator.PrintInfo(typeof(TreatmentPlan)) |
0 — Назад |
Для пунктів 4–6: якщо плану немає — «План не знайдено»; якщо перехід неможливий — повідомлення про це.
Приклад
── Плани лікування ────────────────────
1. Показати всі плани
2. Додати план лікування
3. Плани пацієнта
4. Активувати план
5. Завершити план
6. Скасувати план
7. Інформація про тип TreatmentPlan
0. Назад
Оберіть: 7
TreatmentPlan
Id
PatientId [MinValue]
Diagnosis [Required] [MaxLength]
DurationDays [MinValue]
StatusПідказки
- Пункт 7 отримує
Type, а не об'єкт: рефлексія працює і без екземпляра. - Невалідний план із форми
Addне прийме — у меню просто покажіть його помилки й поверніться в підменю.
📖 Документація:
Адаптація до вашого домену
| Клініка | Готель | Ресторан | Університет | Прокат авто | Бібліотека | Спортзал |
|---|---|---|---|---|---|---|
9. Плани лікування |
9. Запити на сервіс |
9. Спецзамовлення |
9. Курсові проєкти |
9. Сервісні замовлення |
9. Реставрація |
9. Програми тренувань |
Коміт
git add ClinicApp/Clinic.cs ClinicApp/Program.cs
git commit -m "Lab11 Task05"Структура проєкту наприкінці лаби
Так має виглядати ClinicApp/, коли всі завдання виконано:
oop-course/ ← гілка Lab-11 (після злиття — main)
├── .gitignore
├── oop-course.slnx
└── ClinicApp/
├── ClinicApp.csproj
├── Program.cs ✏ Т5
├── Clinic.cs ✏ Т5
├── Enums/
│ ├── AppointmentStatus.cs
│ ├── BloodType.cs
│ ├── Speciality.cs
│ └── TreatmentStatus.cs 🆕 Т2
├── Models/
│ ├── TreatmentPlan.cs 🆕 Т2
│ └── … ще 14 файлів без змін
├── Managers/
│ ├── TreatmentPlanManager.cs 🆕 Т4
│ └── … ще 8 файлів без змін
├── Utils/
│ ├── ClinicFormatter.cs
│ ├── ClinicValidator.cs
│ ├── FormBuilder.cs 🆕 Т4
│ ├── ModelValidator.cs 🆕 Т3
│ └── ValidationResult.cs 🆕 Т3
├── Interfaces/ (4 файли)
├── Comparators/ (4 файли)
└── Attributes/
├── MaxLengthAttribute.cs 🆕 Т1
├── MinValueAttribute.cs 🆕 Т1
└── RequiredAttribute.cs 🆕 Т1Легенда: 🆕 — новий файл · ✏ — змінено вміст · Тn — номер задачі, у якій ви працюєте з файлом. Файли без позначки лишились такими, як були після Лаби 10.
Назви файлів наведено для домену «клініка»; у власному домені назви ваші — важливо, що саме створюється й змінюється.
Перевірка перед здачею
dotnet build ClinicApp
dotnet run --project ClinicAppПереконайтесь, що:
- Структура проєкту збігається зі схемою вище
- Три атрибути — власні класи, що успадковують
Attribute, з[AttributeUsage(AttributeTargets.Property ...)] -
ModelValidator.Validate(new TreatmentPlan())повертає три помилки, а в коді валідатора немає звернень до полівTreatmentPlan - Форма
FormBuilder.Build<TreatmentPlan>()не питаєIdіStatus, підказки беруться з атрибутів - Некоректне число у формі — повторний запит, програма не падає
- Пункт «Інформація про тип» виводить властивості й атрибути через
typeof(TreatmentPlan) - Спроба додати план без обов'язкових полів — повідомлення про помилки, план не додано
- У коді немає LINQ і лямбд
Питання для самоперевірки
- Чим відрізняється
obj.GetType()відtypeof(T)? Коли кожен з них корисний? - Що б змінилось у
FormBuilderбезwhere T : new()? ModelValidator.Validate()приймаєobject, а неTreatmentPlan. Що це дає? Які ще об'єкти можна провалідувати без змін у коді валідатора?- Чому
[Required]наintнічого не перевіряє? Як правильно позначити обов'язковий числовий ID? - Чому
CanWriteнедостатньо, щоб відрізнити публічний сеттер від приватного? Convert.ChangeType(input, prop.PropertyType)кидає виняток, якщоPropertyType— enum або nullable. Як це виправити?
Статус гілки
Після всіх 5 завдань (кожне — окремий коміт Lab11 TaskNN на гілці Lab-11):
git push -u origin Lab-11
git checkout main
git merge --no-ff Lab-11 -m "Merge Lab-11: Reflection & Attributes"
git pushНаступна лаба:
git checkout main→git checkout -b Lab-12.