Controllers — Route, Action, IActionResult
מה זה?
בשיעור ה-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. הזרימה:
בהפעלת השרת (
dotnet run):AddControllers()גורם ל-ASP.NET Core לסרוק את הקוד, למצוא אתStudentsController, ולקרוא[Route("api/[controller]")]+[HttpGet]מעל כל Action. נבנית טבלה:GET api/students→StudentsController.GetAll(),GET api/students/{id}→GetById(), וכן הלאה.כשמגיעה בקשה
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 בהמשך הקורס.
דרישות:
- CRUD מלא:
GET(רשימה + בודד),POST,PUT,DELETE— כל אחד עם[Http...]מתאים - כל Action מחזיר
IActionResultעם Status Code הגיוני (200/201/204/404) PUT/DELETEעל משימה לא-קיימת מחזירים404, לא קורסים
בדיקה: רצף מלא של יצירה→קריאה→עדכון→מחיקה על אותה משימה עובד מקצה לקצה, עם Status Codes נכונים בכל שלב; ניסיון על ID לא-קיים בכל אחת מהפעולות מחזיר 404 מסודר.
מה בפרק הבא
בפרק הבא — Validation: איך מוודאים שהנתונים שמגיעים בבקשה תקינים לפני שהם מגיעים ללוגיקה העסקית, עם Model Binding ו-DataAnnotations.