מה זה?

בשיעור ה-CRUD היו לנו 6 endpoints על משאב אחד (Student). זה כבר מרגיש עמוס — עכשיו דמיינו שמוסיפים גם Product, גם Order, וכל אחד עם 5-6 endpoints משלו:

// Program.cs — every resource's endpoints, all crammed into one file
app.MapGet("/api/students", () => students);
app.MapGet("/api/students/{id}", (int id) => /* ... */ );
app.MapPost("/api/students", (Student s) => /* ... */ );
// ... 3 more student endpoints ...

app.MapGet("/api/products", () => products);
app.MapGet("/api/products/{id}", (int id) => /* ... */ );
app.MapPost("/api/products", (Product p) => /* ... */ );
// ... 3 more product endpoints ...

app.MapGet("/api/orders", () => orders);
// ... and it keeps growing ...

Program.cs הופך לקובץ ענק ובלתי-קריא, עם endpoints של משאבים שונים לגמרי מעורבבים זה בזה. Controllers פותרים את זה: מארגנים את כל ה-endpoints של משאב אחד (כל מה שקשור ל"תלמידים", למשל) יחד, במחלקה נפרדת משלו — SRP במימוש: מחלקה אחת, אחריות אחת.

מאיפה זה רץ — החיבור בין הקבצים

עד עכשיו כל שיעור ביחידה חי בקובץ אחד: Program.cs. ברגע שעוברים ל-Controllers הפרויקט מתפצל לכמה קבצים, וזו בדיוק הנקודה שצריך להבין לפני שרואים את קוד ה-Controller עצמו — אחרת לא ברור מאיפה הוא רץ ומי מפעיל אותו.

MyFirstApi/
├─ Program.cs                    # entry point — starts the server, wires everything together
├─ Models/
│  └─ Student.cs                 # the "record Student" — moved here out of Program.cs
└─ Controllers/
   └─ StudentsController.cs      # every "students" endpoint — the new file this lesson builds

מה משתנה ב-Program.cs

בשיעור ה-CRUD Program.cs הכיל את כל שורות ה-app.MapGet(...). עכשיו הוא מתקצר לשלד שרק מפעיל את מנגנון ה-Controllers — שתי שורות זה כל ההבדל:

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddControllers();   // 1. turn on the Controllers system

var app = builder.Build();

app.MapControllers();   // 2. connect the discovered Controllers to the request pipeline

app.Run();

• AddControllers() — אומר ל-ASP.NET Core “המערכת הזו משתמשת ב-Controllers”. בהפעלת השרת זה גורם לו לסרוק את הפרויקט, למצוא כל מחלקת Controller, ולקרוא את ה-attributes שמעליה ([Route], [HttpGet]…).

• MapControllers() — לוקח את טבלת ה-routing שנבנתה מהסריקה הזו ומחבר אותה ל-Middleware Pipeline מהשיעור הקודם. בלי השורה הזו אף בקשה לא תגיע לאף Action — גם אם ה-Controller מתקמפל בלי שגיאה, כל כתובת תחזיר 404.

שימו לב: Program.cs לא מזכיר את StudentsController בשמו אף פעם. הוא לא צריך — הגילוי אוטומטי.

מי “קורא” ל-Controller?

אף אחד לא קורא לו ישירות — לא אתם, ולא Program.cs. הזרימה:

  1. בהפעלת השרת (dotnet run): AddControllers() גורם ל-ASP.NET Core לסרוק את הקוד, למצוא את StudentsController, ולקרוא [Route("api/[controller]")] + [HttpGet] מעל כל Action. נבנית טבלה: GET api/students → StudentsController.GetAll(), GET api/students/{id} → GetById(), וכן הלאה.

  2. כשמגיעה בקשה GET /api/students: היא עוברת דרך ה-Middleware Pipeline, ואז ה-routing מסתכל בטבלה, מוצא התאמה, יוצר instance של StudentsController, וקורא ל-GetAll(). מה ש-Ok(...) מחזיר הופך ל-Response.

זו בדיוק אותה זרימה של app.MapGet("/api/students", () => ...) מהשיעור הקודם — רק שבמקום שכתבתם את המיפוי בעצמכם בשורה אחת, ה-Framework מגלה אותו מה-attributes. Controller הוא לא “תוכנית שרצה”, הוא ארגז של פונקציות שה-Framework מפעיל בתגובה לבקשות.

איך StudentsController מכיר את Student?

שני הקבצים באותו פרויקט = אותו assembly. אם Student יושב תחת namespace MyFirstApi.Models, מוסיפים בראש StudentsController.cs שורת using MyFirstApi.Models; והטיפוס זמין. בפרויקט קטן שהכל בו תחת namespace אחד — אין צורך אפילו ב-using.

מריצים ובודקים

dotnet run

השרת מדפיס כתובת (למשל http://localhost:5123). בודקים endpoint:

curl http://localhost:5123/api/students

או פותחים http://localhost:5123/swagger בדפדפן — טמפלט webapi (זה שהוזכר בשיעור ה-HTTP, dotnet new webapi) מגיע עם Swagger UI שמציג את כל ה-Actions ומאפשר לשלוח אליהם בקשות ישירות מהדפדפן, בלי curl.

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

• Controller — מחלקה שמאגדת endpoints קשורים, בדרך כלל יורשת מ-ControllerBase, ויושבת בקובץ נפרד משלה תחת Controllers/

• AddControllers() — קריאה ב-Program.cs שמפעילה את סריקת ה-Controllers בהפעלת השרת

• MapControllers() — קריאה ב-Program.cs שמחברת את ה-Controllers שהתגלו ל-Middleware Pipeline; בלעדיה כל בקשה מחזירה 404

• [Route] — attribute שקובע את ה-URL הבסיסי של כל ה-Controller (למשל api/[controller])

• Action — מתודה בתוך Controller שמטפלת ב-endpoint בודד (המקבילה ל-app.MapGet(...) הבודד מהשיעור הקודם)

• [HttpGet]/[HttpPost]/[HttpPut]/[HttpDelete] — attributes שמתייגים כל Action עם ה-HTTP Method שלה

• IActionResult — טיפוס החזרה שמאפשר להחזיר Status Code + Body יחד (Ok(...), NotFound(), BadRequest(...))

בונים Controller שלב אחר שלב

שלב 1: מחלקה ריקה — רק השלד

כל הקוד מכאן ואילך נכתב בקובץ חדש, Controllers/StudentsController.cs — לא ב-Program.cs (ש-AddControllers()/MapControllers() שבו כבר במקום, ראו למעלה).

[ApiController]
[Route("api/[controller]")]   // "[controller]" is replaced automatically with "students" (from the class name)
public class StudentsController : ControllerBase
{
    // no Actions yet — just the shell
}

ControllerBase נותן למחלקה את כל היכולות של Controller (כמו Ok()/NotFound() שנראה בקרוב). [Route("api/[controller]")] בונה את כתובת הבסיס אוטומטית — [controller] הוא placeholder שמוחלף בשם המחלקה בלי הסיומת “Controller” (StudentsController → students, אז כל ה-Actions בפנים יהיו תחת api/students).

שלב 2: Action ראשון — GET לכל הרשימה

private static List<Student> _students = new() { new(1, "Dana", 25) };

[HttpGet]   // GET api/students
public IActionResult GetAll()
{
    return Ok(_students);   // 200 + the list as JSON
}

[HttpGet] בלי פרמטר מתייג את המתודה כ-GET api/students — בדיוק כתובת הבסיס מה-[Route]. IActionResult נותן שליטה על Status Code + Body יחד: Ok(data) = 200 עם data כ-JSON.

שלב 3: Action שני — GET למשאב בודד

[HttpGet("{id}")]   // GET api/students/5
public IActionResult GetById(int id)
{
    var student = _students.FirstOrDefault(s => s.Id == id);
    if (student == null) return NotFound();   // 404
    return Ok(student);   // 200
}

[HttpGet("{id}")] מוסיף {id} אחרי כתובת הבסיס — בדיוק כמו ה-Route Parameter מהשיעור הקודם, רק שעכשיו הוא חלק מ-attribute במקום פרמטר ל-MapGet. NotFound() מחזיר 404 — אותה מוסכמה בדיוק, קוד מאורגן אחרת.

שלב 4: Action שלישי — יצירה עם Body

[HttpPost]   // POST api/students
public IActionResult Create([FromBody] Student newStudent)
{
    _students.Add(newStudent);
    return CreatedAtAction(nameof(GetById), new { id = newStudent.Id }, newStudent);   // 201
}

[FromBody] אומר ל-ASP.NET Core “קח את ה-JSON מגוף הבקשה, בצע Deserialize אוטומטי ל-Student”. CreatedAtAction הוא הגרסה-ל-Controllers של Results.Created — בונה אוטומטית את כתובת ה-Location (מצביעה ל-Action בשם GetById, עם ה-id החדש), בלי לכתוב את הכתובת ידנית.

שלב 5: Action רביעי — מחיקה

[HttpDelete("{id}")]   // DELETE api/students/5
public IActionResult Delete(int id)
{
    var student = _students.FirstOrDefault(s => s.Id == id);
    if (student == null) return NotFound();
    _students.Remove(student);
    return NoContent();   // 204 — succeeded, no content to return
}

אותה מוסכמה שוב: מוצאים לפי id, 404 אם לא נמצא, אחרת מבצעים ומחזירים 204.

התוצאה: כל 4 ה-Actions יחד יוצרים StudentsController שלם, מאורגן במחלקה אחת נפרדת — לא עוד שורות ב-Program.cs.

הסבר עיקרי

[Route("api/[controller]")] עקבי אוטומטית בין Controllers — אם ה-Controller נקרא ProductsController, ה-URL הבסיס יהיה api/products בלי לכתוב אותו ידנית שוב.

IActionResult נותן שליטה על Status Code + Body יחד, בדיוק המוסכמה מ-REST שלמדנו בשיעור ה-HTTP — כל Action בוחר את התשובה המתאימה במפורש, לא רק מחזיר ערך גולמי.

[FromBody] מחבר ל-JSON מהיחידה הקודמת — כשלקוח שולח POST עם JSON ב-Body, ASP.NET Core מבצע Deserialize אוטומטי, בדיוק JsonSerializer.Deserialize<Student> שכבר תרגלנו, רק שה-Framework עושה את זה מאחורי הקלעים.

יתרונות

Controllers מארגנים endpoints קשורים תחת מחלקה אחת ברורה (SRP); attributes ([HttpGet] וכו’) הופכים את המיפוי Method↔URL↔קוד לגלוי וקריא ישירות מעל כל מתודה; IActionResult מאפשר תשובה מפורשת ומתאימה לכל תרחיש.

חסרונות

Controller “שמן” מדי (יותר מדי Actions, יותר מדי לוגיקה עסקית ישירות בתוכו) חוזר לבעיית SRP — לוגיקה עסקית כבדה צריכה לעבור לשכבת Service נפרדת (יילמד בשיעור ה-DAL); attributes רבים על כל מתודה עלולים להעמיס ויזואלית על קוד פשוט.

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

• Controller יושב בקובץ נפרד תחת Controllers/; Program.cs מפעיל אותו עם AddControllers() + MapControllers() ולעולם לא מזכיר אותו בשמו

• אף אחד לא “קורא” ל-Controller — ASP.NET Core סורק אותו בהפעלה, בונה טבלת routing מה-attributes, ומפעיל את ה-Action המתאים לכל בקשה נכנסת (אותה זרימה כמו app.MapGet, רק מגולה אוטומטית)

• [Route("api/[controller]")] בונה URL בסיס אוטומטית משם המחלקה

• [HttpGet]/[HttpPost] וכו’ מתייגים כל Action עם ה-Method שלה

• IActionResult (Ok/NotFound/CreatedAtAction) שולט על Status Code + Body יחד

• [FromBody] מבצע Deserialize אוטומטי מ-JSON לאובייקט C#

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

• לשכוח [HttpGet("{id}")] ולכתוב שני [HttpGet] ללא הבחנה — קונפליקט routing

• להחזיר ערך גולמי (return student;) במקום IActionResult עם Status Code מפורש כשצריך שליטה על תגובת השגיאה

• לשים לוגיקה עסקית כבדה ישירות ב-Controller במקום להעביר לשכבת Service — הפרת SRP

• לשכוח AddControllers() או MapControllers() ב-Program.cs — ה-Controller מתקמפל בלי שגיאה, אבל כל בקשה מחזירה 404; קל לבזבז זמן על חיפוש הבאג ב-Controller עצמו כשהוא בכלל ב-Program.cs

סיכום

Controllers מארגנים endpoints קשורים תחת מחלקה אחת, עם [Route] שבונה URL בסיס אוטומטית ו-[HttpGet]/[HttpPost]/וכו’ שמתייגים כל Action עם ה-Method שלה. בנינו StudentsController שלם שלב אחר שלב — משלד ריק, דרך GET-הכול, GET-בודד, POST ו-DELETE — כשכל Action הוא בדיוק אותה לוגיקה שכתבנו בשיעור הקודם כ-app.MapGet בודד, רק מאורגנת במקום ברור ומסודר.

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

Microsoft Learn — Controller-based APIs


תרגילים

תרגיל 1 — Controller בסיסי

המשימה: צרו ProductsController עם [HttpGet] שמחזיר List<Product> קבועה מראש (בזיכרון), ו-[HttpGet("{id}")] שמחזיר מוצר בודד או 404.

בדיקה: GET api/products מחזיר את כל המוצרים; GET api/products/99 (לא קיים) מחזיר 404.

תרגיל 2 — Create עם IActionResult

המשימה: הוסיפו [HttpPost] ל-ProductsController שמקבל [FromBody] Product, מוסיף אותו לרשימה, ומחזיר CreatedAtAction.

בדיקה: בקשת POST עם JSON תקין מחזירה 201 עם המוצר החדש ב-Body.


פרויקט מסכם

המשימה: בנו TasksController מלא לניהול משימות (Task Manager API) — הפרויקט שילווה את שאר יחידות ה-Backend בהמשך הקורס.

דרישות:

  1. CRUD מלא: GET (רשימה + בודד), POST, PUT, DELETE — כל אחד עם [Http...] מתאים
  2. כל Action מחזיר IActionResult עם Status Code הגיוני (200/201/204/404)
  3. PUT/DELETE על משימה לא-קיימת מחזירים 404, לא קורסים

בדיקה: רצף מלא של יצירה→קריאה→עדכון→מחיקה על אותה משימה עובד מקצה לקצה, עם Status Codes נכונים בכל שלב; ניסיון על ID לא-קיים בכל אחת מהפעולות מחזיר 404 מסודר.

מה בפרק הבא

בפרק הבא — Validation: איך מוודאים שהנתונים שמגיעים בבקשה תקינים לפני שהם מגיעים ללוגיקה העסקית, עם Model Binding ו-DataAnnotations.