מה זה?

ה-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 (מהפרויקט המסכם הקודם).

דרישות:

  1. [Required] על Title, עם [StringLength] (מקסימום 100 תווים)
  2. שדה Priority (int) עם [Range(1, 5)]
  3. [ApiController] מופעל על ה-Controller — ולידציה אוטומטית, בלי if ידני ב-Create

בדיקה: בקשת POST עם Title ריק או Priority מחוץ לטווח מקבלת 400 אוטומטית עם הודעה ברורה; בקשה תקינה לגמרי נוצרת בהצלחה כרגיל.

מה בפרק הבא

בפרק הבא — Dependency Injection: איך ASP.NET Core “מזריק” תלויות (כמו שירותים ו-Repositories) אוטומטית, במקום ליצור אותן ידנית בכל מקום — DIP מהיחידה הקודמת, בתשתית אמיתית.