מה זה?

בשיעור הקודם למדנו איך Request ו-Response בנויים, מה כל Method אומר, ומה המשמעות של כל Status Code — אבל עדיין לא כתבנו שורת קוד אחת שבאמת מגיבה לבקשה. השיעור הזה סוגר את הפער: לוקחים בדיוק את מה שנלמד (GET/POST/PUT/DELETE, 200/201/404) והופכים אותו לקוד עובד — CRUD (Create, Read, Update, Delete) בסיסי על משאב אחד, ישירות ב-Program.cs.

לאורך השיעור נבנה את זה שלב אחר שלב, כשכל שלב מוסיף בדיוק דבר אחד חדש. בסוף תדעו לא רק “להעתיק דוגמה”, אלא להחליט בעצמכם — לכל שדה מידע חדש שאתם צריכים לקבל מהלקוח — איפה הוא צריך להופיע בבקשה: בתוך הכתובת עצמה (Route), אחרי סימן שאלה (Query String), או בתוך הגוף (Body). זו בדיוק השאלה שממשיכה ללוות כל מפתח Backend, בכל endpoint שהוא כותב.

מילות מפתח שחשוב לזכור

• Endpoint — צירוף של Method+URL שממופה לקוד שמטפל בו (למשל app.MapGet("/api/students", ...))

• Route Parameter — חלק מהכתובת עצמה שמזהה משאב ספציפי — {id} בתוך /api/students/{id} אומר “בדיוק התלמיד הזה, לא אחר”

• Query String Parameter — מידע אופציונלי אחרי סימן שאלה בכתובת, לסינון/מיון/הגבלה של רשימה — למשל /api/students?minAge=20 אומר “כל התלמידים, אבל רק מעל גיל 20”

• Body — תוכן הבקשה עצמו, בפורמט JSON — משמש כשצריך לשלוח אובייקט שלם (ליצירה או עדכון), לא רק ערך בודד

• In-Memory Store — רשימה פשוטה בזיכרון (לא DB אמיתי עדיין) לשמירת נתונים בין בקשות, לצורך הדוגמה בשיעור הזה בלבד

הסבר עיקרי — בונים CRUD שלב אחר שלב

שלב 1: GET שמחזיר את כל הרשימה

הפעולה הכי פשוטה שיש: לבקש את כל התלמידים, בלי לציין שום פרט נוסף.

var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

var students = new List<Student> { new(1, "Dana", 25), new(2, "Avi", 19) };

app.MapGet("/api/students", () => students);

app.Run();

record Student(int Id, string Name, int Age);

app.MapGet רושם endpoint: “כשמגיעה בקשת GET לכתובת /api/students, תריץ את הפונקציה הזו ותחזיר את מה שהיא מחזירה”. ASP.NET Core הופך את students אוטומטית ל-JSON.

שלב 2: GET משאב בודד — מציגים Route Parameter

מה אם רוצים רק את התלמיד עם ID מספר 5, לא את כל הרשימה? צריך דרך “לומר” לשרת איזה תלמיד בדיוק מבוקש — וזה בדיוק תפקידו של Route Parameter: חלק משתנה בתוך הכתובת עצמה.

app.MapGet("/api/students/{id}", (int id) =>
    students.FirstOrDefault(s => s.Id == id) is { } student
        ? Results.Ok(student)
        : Results.NotFound());

{id} בכתובת נקשר אוטומטית לפרמטר int id בפונקציה — ASP.NET Core “קורא” את החלק הזה מה-URL וממיר אותו לטיפוס C# בעצמו. זה נקרא Model Binding, ונחזור אליו עוד בשיעור ה-Validation. שימו לב: אם לא נמצא תלמיד כזה, מחזירים 404 — בדיוק כמו שנלמד בשיעור הקודם.

שלב 3: סינון עם Query String — מציגים Query String Parameter

עכשיו תרחיש אחר: רוצים את כל התלמידים מעל גיל מסוים — לא תלמיד אחד ספציפי, אלא סינון על הרשימה כולה. זה לא מתאים ל-Route Parameter (זה לא מזהה משאב יחיד) — בשביל זה יש Query String: מידע אופציונלי שמגיע אחרי סימן שאלה בכתובת.

app.MapGet("/api/students/search", (int? minAge) =>
{
    var result = minAge.HasValue
        ? students.Where(s => s.Age >= minAge.Value)
        : students;
    return Results.Ok(result);
});

קריאה ל-GET /api/students/search?minAge=20 תחזיר רק תלמידים מגיל 20 ומעלה. שימו לב ל-int? (Nullable) — Query String תמיד אופציונלי, אז אם לא סופק minAge בכלל, minAge יהיה null ומחזירים את כל הרשימה. אפשר לשלב כמה פרמטרים עם &, למשל ?minAge=20&maxAge=25.

הכלל לזכור: אם המידע מזהה בדיוק איזה משאב ספציפי מבקשים — Route Parameter, חלק מהכתובת. אם המידע הוא הגדרה איך לסנן/למיין/להגביל רשימה — Query String, אחרי סימן השאלה.

שלב 4: יצירת משאב חדש — מציגים Body

ליצור תלמיד חדש צריך לשלוח הרבה מידע ביחד (שם, גיל…) — יותר מדי בשביל למקם בכתובת עצמה. בשביל זה יש Body: תוכן הבקשה עצמו, בפורמט JSON.

app.MapPost("/api/students", (Student student) =>
{
    students.Add(student);
    return Results.Created($"/api/students/{student.Id}", student);
});

ASP.NET Core רואה שהפרמטר student הוא טיפוס מורכב (לא int/string בודד) ומבין אוטומטית: “זה מגיע מה-Body כ-JSON, לא מה-URL”. Results.Created מחזיר 201, בדיוק כמו שנלמד — עם כתובת המשאב החדש.

שלב 5: עדכון — Route Parameter ו-Body יחד

עדכון משאב קיים צריך את שניהם: איזה משאב לעדכן (Route Parameter) וגם מה הנתונים החדשים (Body).

app.MapPut("/api/students/{id}", (int id, Student updated) =>
{
    var index = students.FindIndex(s => s.Id == id);
    if (index == -1) return Results.NotFound();
    students[index] = updated;
    return Results.NoContent();
});

id מגיע מה-Route (מ-{id} בכתובת), updated מגיע מה-Body — ASP.NET Core מבין את שניהם בו-זמנית, כל אחד מהמקום הנכון לו, בלי שצריך לציין את זה בעצמכם.

שלב 6: מחיקה — Route Parameter בלבד

מחיקה צריכה רק לדעת מה למחוק — אין נתונים נוספים לשלוח, אז אין Body בכלל.

app.MapDelete("/api/students/{id}", (int id) =>
{
    students.RemoveAll(s => s.Id == id);
    return Results.NoContent();
});

מה עדיין חסר

כל ה-endpoints כאן חיים באותו Program.cs, ו-students הוא רשימה בזיכרון שנעלמת בכל הפעלה מחדש של השרת. שני החוסרים האלה בדיוק — ארגון endpoints וקביעות הנתונים — הם מה ששני השיעורים הקרובים (Controllers, ואחר כך DAL Pattern) פותרים, אחד בכל פעם.

יתרונות

קוד מינימלי — כל endpoint שורה עד כמה שורות, בלי טקס מיותר; קשר ישיר וברור בין המוסכמות של HTTP (Methods/Status Codes) לביטוי שלהן בקוד; אפשר לבדוק מיד עם curl/Postman/Swagger, בלי תשתית נוספת.

חסרונות

כל ה-endpoints ב-Program.cs אחד — עובד מצוין לדוגמה קטנה, אבל API עם עשרות endpoints הופך את הקובץ לענק ובלתי-קריא (בדיוק הבעיה ש-Controllers פותרים, בשיעור הקרוב); ה-In-Memory Store נמחק בכל הפעלה מחדש של השרת — לא מתאים לנתונים אמיתיים עד שנגיע לתבנית DAL בהמשך היחידה.

נקודות חשובות

• Route Parameter ({id}) = מזהה משאב ספציפי, חלק מהכתובת עצמה

• Query String (?minAge=20) = סינון/מיון/הגבלה אופציונליים על רשימה, אחרי סימן שאלה

• Body = תוכן מלא בפורמט JSON, לשליחת אובייקט שלם (יצירה/עדכון)

• Results.Ok/NotFound/Created/NoContent ממפים ישירות ל-Status Codes המוכרים — 200/404/201/204

טעויות נפוצות

• להשתמש ב-Route Parameter כשמתכוונים לסינון — למשל /api/students/{minAge} במקום /api/students/search?minAge=20; זה מבלבל כי {minAge} נראה כאילו הוא מזהה משאב ספציפי, אבל הוא לא

• לשכוח ש-Query String תמיד אופציונלי, ולא להשתמש בטיפוס Nullable (int?) — התוכנית תיכשל אם הלקוח לא שלח את הפרמטר

• להחזיר תמיד Results.Ok() גם כשהמשאב לא נמצא, במקום Results.NotFound()

• לשכוח את סוג הפרמטר (לכתוב id בלי int) ואז ה-Route Parameter לא נקשר כמו שצריך

סיכום

בשיעור הזה הפכנו את התיאוריה של HTTP לקוד עובד, שלב אחר שלב: GET לכל הרשימה, GET למשאב בודד עם Route Parameter, סינון עם Query String, ויצירה/עדכון/מחיקה עם Body ו-Route Parameter יחד. שלוש הדרכים שמידע מגיע לשרת — Route, Query, Body — הן לא שרירותיות: לכל אחת יש תפקיד ברור ושונה, וההחלטה איפה למקם כל פיסת מידע היא חלק בסיסי מתכנון כל endpoint.

דוקומנטציה רשמית

Microsoft Learn — Minimal APIs overview

Microsoft Learn — Parameter binding in Minimal APIs


תרגילים

תרגיל 1 — סינון עם Query String

המשימה: הוסיפו למשאב Student שדה Grade (ציון). הוסיפו endpoint GET /api/students/search שמקבל Query String אופציונלי minGrade, ומחזיר רק תלמידים עם ציון גבוה או שווה לו (אם לא סופק — מחזיר את כולם).

בדיקה: GET /api/students/search?minGrade=60 מחזיר רק תלמידים עוברים; GET /api/students/search (בלי פרמטר) מחזיר את כל התלמידים.

תרגיל 2 — קודי סטטוס נכונים

המשימה: בדקו עם curl/Postman את כל 4 ה-endpoints הבסיסיים, כולל תרחישי כישלון (ID שלא קיים ל-GET/PUT/DELETE).

בדיקה: GET/PUT/DELETE על ID לא קיים מחזירים 404; POST תקין מחזיר 201 עם Location header תקין.


פרויקט מסכם

המשימה: ממשו בקוד את ה-API שתכננתם על נייר בשיעור הקודם — TasksController בתור endpoints בסיסיים (עדיין לא Controller class — זה בשיעור הבא), עם In-Memory Store.

דרישות:

  1. משאב TaskItem עם Id, Title, IsDone
  2. חמשת ה-endpoints הבסיסיים: GET /api/tasks, GET /api/tasks/{id}, POST /api/tasks, PUT /api/tasks/{id}, DELETE /api/tasks/{id}
  3. endpoint נוסף GET /api/tasks/search שמקבל Query String אופציונלי isDone (true/false) ומחזיר רק משימות שתואמות
  4. כל endpoint מחזיר את ה-Status Code הנכון לפי התכנון מהשיעור הקודם

בדיקה: כל ה-endpoints עובדים דרך curl/Postman; יצירה ואז קריאה של אותה משימה מחזירה את הנתונים הנכונים; GET /api/tasks/search?isDone=false מחזיר רק משימות פתוחות; מחיקה ואז קריאה מחזירה 404.

מה בפרק הבא

בפרק הבא — Middleware Pipeline: עכשיו שיש קוד אמיתי שמטפל בבקשות, נראה איך ASP.NET Core מעבד כל בקשה נכנסת דרך שרשרת שכבות מסודרת (לוג, הרשאות, טיפול בשגיאות) לפני שהיא מגיעה בכלל לקוד שכתבנו כאן.