Validation — Model Binding ו-DataAnnotations
מה זה?
ה-Create Action מהשיעור הקודם מקבל [FromBody] Student newStudent ופשוט מוסיף אותו — בלי לבדוק שום דבר:
[HttpPost]
public IActionResult Create([FromBody] Student newStudent)
{
_students.Add(newStudent); // no check at all — anything gets in
return CreatedAtAction(nameof(GetById), new { id = newStudent.Id }, newStudent);
}מה קורה אם שולחים POST עם Name ריק, או Age = -5? התשובה: זה פשוט נכנס למערכת בלי שום התנגדות — _students עכשיו מכיל תלמיד בלי שם ובגיל שלילי. אין כלום שעוצר את זה. השיעור הזה בונה, שלב אחר שלב, את הדרך הנכונה למנוע את זה.
מילות מפתח שחשוב לזכור
• Model Binding — התהליך שממיר JSON נכנס לאובייקט C# אוטומטית (המשך ישיר ל-[FromBody] מהשיעור הקודם)
• DataAnnotations — attributes על Properties שמגדירים כללי תקינות: [Required], [Range], [StringLength], [EmailAddress]
• ModelState.IsValid — בודק אם כל כללי ה-DataAnnotations עמדו בבקשה הנוכחית
• [ApiController] — attribute על ה-Controller שמפעיל ולידציה אוטומטית — מחזיר 400 בעצמו אם המודל לא תקין, בלי בדיקה ידנית בכל Action
בונים ולידציה שלב אחר שלב
שלב 1: הדרך הידנית והמייגעת — בדיקת if לכל שדה
הפתרון הראשון שעולה לראש: לבדוק כל שדה בעצמנו, בתוך ה-Action.
[HttpPost]
public IActionResult Create([FromBody] Student newStudent)
{
if (string.IsNullOrEmpty(newStudent.Name))
return BadRequest("Name is required");
if (newStudent.Name.Length < 2 || newStudent.Name.Length > 50)
return BadRequest("Name must be between 2 and 50 characters");
if (newStudent.Age < 0 || newStudent.Age > 120)
return BadRequest("Age must be between 0 and 120");
_students.Add(newStudent);
return CreatedAtAction(nameof(GetById), new { id = newStudent.Id }, newStudent);
}זה עובד — אבל תארו לעצמכם Product עם 6 שדות, או Order עם 10. כל שדה חדש מוסיף עוד if, וכל Action (Create, Update) שמקבל אותו טיפוס צריך לחזור על אותן בדיקות בעצמו. כפילות מסיבית, בדיוק כמו הבעיה שכבר פגשנו לפני ה-Middleware.
שלב 2: הצהרה במקום בדיקה — DataAnnotations
במקום לבדוק כל שדה בקוד, מצהירים את הכלל ישירות על השדה במודל עצמו:
class Student
{
[Required(ErrorMessage = "Name is required")]
[StringLength(50, MinimumLength = 2, ErrorMessage = "Name must be between 2 and 50 characters")]
public string Name { get; set; }
[Range(0, 120, ErrorMessage = "Age must be between 0 and 120")]
public int Age { get; set; }
[EmailAddress(ErrorMessage = "Invalid email address")]
public string Email { get; set; }
}[Required], [StringLength], [Range], [EmailAddress] הם בדיוק אותם כללים משלב 1 — רק שהם כתובים פעם אחת, על השדה עצמו, ולא בכל Action שמשתמש בו. זה בדיוק הגישה של enum מיחידת היסודות: הצהרה ברורה במקום בדיקה ידנית שקל לשכוח.
שלב 3: עדיין צריך לבדוק — ModelState.IsValid
הצהרת הכללים לבד לא מספיקה — עדיין צריך מישהו שיבדוק אם הם התקיימו:
[HttpPost]
public IActionResult Create([FromBody] Student newStudent)
{
if (!ModelState.IsValid)
{
return BadRequest(ModelState); // 400 with all the validation errors
}
_students.Add(newStudent);
return CreatedAtAction(nameof(GetById), new { id = newStudent.Id }, newStudent);
}ModelState הוא תוצאה שה-Framework ממלא אוטומטית אחרי ה-Model Binding — הוא כבר בדק את כל ה-DataAnnotations על newStudent, ו-IsValid אומר אם כולם עברו. עדיין if אחד — אבל רק אחד, לא אחד לכל שדה.
שלב 4: גם ה-if האחד הזה מיותר — [ApiController]
[ApiController] // Enables automatic validation for the whole Controller
[Route("api/[controller]")]
public class StudentsController : ControllerBase
{
[HttpPost]
public IActionResult Create([FromBody] Student newStudent)
{
// With [ApiController], if newStudent doesn't meet the DataAnnotations —
// ASP.NET Core already returned 400 automatically, this code doesn't even run!
_students.Add(newStudent);
return CreatedAtAction(nameof(GetById), new { id = newStudent.Id }, newStudent);
}
}[ApiController] על המחלקה כולה הופך את בדיקת ModelState.IsValid לאוטומטית — היא רצה לפני שקוד ה-Action בכלל מתחיל, בדיוק כמו Middleware ברמה אחרת. אם המודל לא תקין, [ApiController] כבר החזיר 400 בעצמו, ואף שורת קוד שלכם לא רצה. זו הסיבה שהקוד הסופי “לא בודק כלום” — הוא לא צריך, ה-Framework כבר בדק.
הסבר עיקרי
מהמסע הזה רואים למה DataAnnotations עדיפות — לא רק “קצר יותר”, אלא שהכלל מוגדר פעם אחת במקום אחד (על השדה), ולא חוזר על עצמו בכל Action שמשתמש בטיפוס. זה בדיוק העיקרון שכבר פגשנו: כפילות קוד היא סימן לבעיה, וההצהרה הופכת את הכללים לגלויים וברורים בלי לקרוא לוגיקה.
חיבור ישיר ל-Zod ביחידת ההוספות (בהמשך הקורס, אם רלוונטי) — DataAnnotations הם ל-C# בדיוק מה ש-Zod (בעולם ה-JavaScript/TypeScript) עושה שם — שכבת ולידציה מוצהרת שרצה לפני הלוגיקה העסקית. הרעיון זהה, גם אם התחביר שונה בין השפות.
יתרונות
DataAnnotations הצהרתיות וקצרות — כלל תקינות אחד = attribute אחד, קריא ישירות מעל השדה; [ApiController] הופך את בדיקת התקינות לאוטומטית, בלי if ידני בכל Action; הודעות שגיאה מותאמות אישית (ErrorMessage) נותנות פידבק ברור ללקוח.
חסרונות
DataAnnotations מוגבלות לכללים פשוטים יחסית — ולידציה מורכבת (למשל “תאריך סיום חייב להיות אחרי תאריך התחלה”, תלוי בשני שדות יחד) דורשת קוד נוסף (IValidatableObject או ולידציה ידנית); יותר מדי attributes על מודל אחד עלולים להעמיס ויזואלית.
נקודות חשובות
• DataAnnotations = attributes על Properties שמגדירים כללי תקינות ([Required], [Range], [StringLength])
• [ApiController] מפעיל ולידציה אוטומטית — מחזיר 400 בעצמו אם ModelState לא תקין
• Model Binding ממיר JSON נכנס לאובייקט C# אוטומטית, לפני שהוולידציה בכלל רצה
• ולידציה מורכבת (תלוית-כמה-שדות) דורשת מעבר ל-IValidatableObject או קוד ידני
טעויות נפוצות
• לשכוח [ApiController] ואז לתהות למה מודל לא-תקין “עובר” בלי שגיאה
• לכתוב בדיקת if ידנית לכל שדה במקום DataAnnotations — כפילות מיותרת ופחות קריא
• לצפות ש-DataAnnotations יטפלו בכללים עסקיים מורכבים (תלות בין שדות) — הן מוגבלות לכללים פשוטים per-field
סיכום
עברנו מסע של ארבעה שלבים: בדיקות if ידניות ומייגעות לכל שדה (עובד, אבל כפילות מסיבית) → הצהרת אותם כללים כ-DataAnnotations על המודל (פעם אחת, לא בכל Action) → ModelState.IsValid כבדיקה מרוכזת (עדיין if אחד) → [ApiController] שמבצע גם את הבדיקה האחת הזו אוטומטית. התוצאה הסופית: שכבת הגנה מוקדמת שמבטיחה שרק נתונים תקינים מגיעים ללוגיקה העסקית, כמעט בלי קוד נראה-לעין.
דוקומנטציה רשמית
Microsoft Learn — Model validation in ASP.NET Core
תרגילים
תרגיל 1 — DataAnnotations בסיסיות
המשימה: הוסיפו ל-Product (מהשיעור הקודם) [Required] על Name ו-[Range(0.01, 100000)] על Price. נסו לשלוח POST עם Price = -5.
בדיקה: הבקשה עם Price שלילי מקבלת 400 אוטומטית, בלי קוד ידני ב-Action.
תרגיל 2 — הודעות שגיאה מותאמות
המשימה: הוסיפו ErrorMessage מותאם אישית לכל אחד מה-attributes בתרגיל 1, ובדקו שהודעת השגיאה שמוחזרת ב-400 מכילה את הטקסט שהגדרתם.
בדיקה: תשובת ה-400 כוללת את הודעות השגיאה המותאמות-אישית שלכם, לא רק טקסט גנרי.
פרויקט מסכם
המשימה: הוסיפו ולידציה מלאה למודל Task מ-TasksController (מהפרויקט המסכם הקודם).
דרישות:
[Required]עלTitle, עם[StringLength](מקסימום 100 תווים)- שדה
Priority(int) עם[Range(1, 5)] [ApiController]מופעל על ה-Controller — ולידציה אוטומטית, בליifידני ב-Create
בדיקה: בקשת POST עם Title ריק או Priority מחוץ לטווח מקבלת 400 אוטומטית עם הודעה ברורה; בקשה תקינה לגמרי נוצרת בהצלחה כרגיל.
מה בפרק הבא
בפרק הבא — Dependency Injection: איך ASP.NET Core “מזריק” תלויות (כמו שירותים ו-Repositories) אוטומטית, במקום ליצור אותן ידנית בכל מקום — DIP מהיחידה הקודמת, בתשתית אמיתית.