מה זה?

בשיעור הקודם כתבנו כמה endpoints. עכשיו דמיינו שרוצים שכל בקשה תירשם ביומן (log), וגם שתיבדק הרשאה לפני שהיא מטופלת. הפתרון ה"נאיבי" — להוסיף את שתי הבדיקות בתוך כל endpoint בנפרד:

app.MapGet("/api/students", (HttpContext ctx) =>
{
    Console.WriteLine($"Incoming: {ctx.Request.Method} {ctx.Request.Path}");   // ①
    if (!ctx.Request.Headers.ContainsKey("Authorization"))                     // ②
        return Results.Unauthorized();
    return Results.Ok(students);
});

app.MapPost("/api/students", (HttpContext ctx, Student s) =>
{
    Console.WriteLine($"Incoming: {ctx.Request.Method} {ctx.Request.Path}");   // ① — same two lines
    if (!ctx.Request.Headers.ContainsKey("Authorization"))                     // ② — copy-pasted again
        return Results.Unauthorized();
    students.Add(s);
    return Results.Created($"/api/students/{s.Id}", s);
});

שתי השורות המסומנות (①②) חוזרות על עצמן בכל endpoint, ואם יש 20 endpoints — הן מועתקות 20 פעמים. זו בדיוק אותה בעיה שכבר פגשנו ביחידת SOLID (הפרת SRP): אם צריך לשנות את פורמט הלוג, צריך לזכור לתקן בכל מקום בנפרד — ובקלות שוכחים אחד.

הרעיון של Middleware: לכתוב את הלוגיקה החוזרת הזו פעם אחת בלבד, ולתת ל-ASP.NET Core להריץ אותה אוטומטית על כל בקשה נכנסת — לפני שהיא בכלל מגיעה לקוד ה-endpoint שלכם. Middleware Pipeline הוא שרשרת שכבות כאלה, שכל בקשה עוברת בסדר קבוע.

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

• Middleware — רכיב שרץ על כל בקשה נכנסת (או חלק ממנה), יכול לבדוק/לשנות אותה ואז להעביר הלאה

• Pipeline — השרשרת המלאה של כל ה-Middleware, בסדר שבו הם נרשמו

• next() — קריאה שמעבירה את הבקשה ל-Middleware הבא בשרשרת; בלעדיה, הבקשה “נעצרת” כאן

• app.Use(...) — הדרך לרשום Middleware בפרויקט ASP.NET Core, בקובץ Program.cs

איך בונים את זה — שלב אחר שלב

שלב 1: הכי בסיסי שיש — Middleware שרק רושם ביומן

מוסיפים ל-Program.cs, מיד אחרי var app = builder.Build(); ולפני כל app.MapX(...):

var app = builder.Build();

app.Use(async (context, next) =>
{
    Console.WriteLine($"Incoming request: {context.Request.Method} {context.Request.Path}");
    await next();   // ממשיכים הלאה לשכבה הבאה (בסוף — ל-endpoint עצמו)
});

app.MapGet("/api/students", () => students);   // מגיע לכאן רק אחרי ה-Middleware

app.Use מקבל פונקציה עם שני פרמטרים: context (כל המידע על הבקשה הנוכחית) ו-next (קריאה שממשיכה הלאה). בלי await next(), הבקשה פשוט “נתקעת” ב-Middleware הזה ואף פעם לא מגיעה ל-endpoint. זה בדיוק ה"מימוש בקוד" של הרעיון: הלוגיקה נכתבה פעם אחת, לא בכל endpoint בנפרד.

שלב 2: Middleware שיכול לעצור את השרשרת

עכשיו מוסיפים בדיקת הרשאה — הפעם, אם היא נכשלת, לא קוראים ל-next() בכלל:

app.Use(async (context, next) =>
{
    if (!context.Request.Headers.ContainsKey("Authorization"))
    {
        context.Response.StatusCode = 401;
        return;   // אין next() — הבקשה נעצרת כאן, לא ממשיכה הלאה
    }
    await next();
});

דילוג על next() הוא הדרך לעצור בקשה באמצע השרשרת — בדיוק כמו מאבטח שלא נותן למישהו בלי תג להיכנס בכלל, לא משנה מה הוא רצה לעשות בפנים.

שלב 3: הסדר קובע הכול

שני ה-Middleware מהשלבים הקודמים, ביחד, בסדר הנכון:

var app = builder.Build();

app.Use(async (context, next) =>                 // ① רץ ראשון על כל בקשה
{
    Console.WriteLine($"Incoming: {context.Request.Method} {context.Request.Path}");
    await next();
    Console.WriteLine($"Response sent: {context.Response.StatusCode}");   // רץ אחרי שהתשובה חזרה
});

app.Use(async (context, next) =>                 // ② רץ שני — רק אם ① קרא ל-next()
{
    if (!context.Request.Headers.ContainsKey("Authorization"))
    {
        context.Response.StatusCode = 401;
        return;
    }
    await next();
});

app.MapGet("/api/students", () => "Student list");   // ③ מגיע לכאן רק אם גם ① וגם ② קראו ל-next()

Middleware שנרשם ראשון (app.Use הראשון) רץ ראשון על כל בקשה נכנסת. אם היינו רושמים את בדיקת ההרשאה לפני הלוג, בקשה בלי הרשאה הייתה נעצרת ב-401 לפני שהלוג בכלל ירוץ — לפעמים רצוי, לפעמים לא. הסדר הוא החלטת עיצוב מכוונת, לא מקרית.

  flowchart RL
    REQ["Incoming request"] --> LOG["Middleware: Log"]
    LOG --> AUTH{"Middleware: Authorization?"}
    AUTH -->|"missing"| E401["401 — stops here"]
    AUTH -->|"present"| MAP["Endpoint"]
    MAP --> RES["Outgoing response"]
    RES -.->|"await next() returns"| LOG

הסבר עיקרי

next() הוא “המשך לשכבה הבאה” — כל Middleware מקבל את הבקשה, יכול לעשות משהו לפניה (כמו הלוג), לקרוא ל-next() כדי להעביר אותה הלאה, ואז (אופציונלי) לעשות עוד משהו אחרי שהבקשה חזרה מהשכבות הפנימיות (כמו הדפסת ה-Status Code בדוגמה בשלב 3). דילוג על next() עוצר את השרשרת שם — בדיוק כמו ש-Middleware בדיקת ה-JWT שיופיע ביחידת NoSQL/Auth יעצור בקשות לא-מאומתות.

זה בדיוק SRP+OCP במימוש חדש — כל Middleware אחראי על דבר אחד (SRP) — הוספת Middleware חדש (למשל Rate Limiting) היא הוספת שכבה נוספת, בלי לגעת בשכבות הקיימות (OCP). זו לא תיאוריה מופשטת — זה בדיוק אותו עיקרון שכבר תרגלתם, במימוש קונקרטי בתשתית אמיתית של ASP.NET Core.

יתרונות

Middleware מרכז לוגיקה חוצת-בקשות (לוג, הרשאות, טיפול-שגיאות) במקום אחד, לא חוזרת בכל endpoint; סדר מפורש וברור קובע בדיוק מה קורה מתי; קל להוסיף שכבה חדשה בלי לגעת בקיימות.

חסרונות

יותר מדי שכבות Middleware מאטות כל בקשה (כל שכבה מוסיפה עיבוד); סדר שגוי (למשל הרשאות אחרי לוגיקה עסקית) עלול ליצור פרצות אבטחה או באגים קשים לאבחון.

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

• Middleware רץ על כל בקשה, בסדר הרישום המדויק שלו ב-Program.cs — מיד אחרי builder.Build(), לפני ה-endpoints

• next() מעביר לשכבה הבאה; דילוג עליו עוצר את השרשרת שם

• סדר Middleware הוא החלטה מכוונת — לוג/הרשאות/טיפול-שגיאות בדרך כלל מוקדם בשרשרת

• Middleware הוא יישום מעשי של SRP+OCP על תשתית שרת אמיתית

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

• לשכוח await next() ב-Middleware שאמור להמשיך לשכבה הבאה — הבקשה “נתקעת” שם בטעות

• לרשום Middleware בסדר שגוי (למשל הרשאות אחרי שהלוגיקה העסקית כבר רצה) — פוגע במטרה כולה

• לחשוב ש-Middleware הוא רק “לוג” — הוא כלי כללי לכל דבר שצריך לרוץ על כל בקשה

סיכום

Middleware Pipeline הוא שרשרת שכבות שכל בקשה נכנסת עוברת, בסדר קבוע לפי סדר הרישום ב-Program.cs. כל Middleware יכול לבדוק/לשנות את הבקשה, לקרוא ל-next() כדי להמשיך הלאה, או לעצור את השרשרת (כמו החזרת 401 בלי next()). זהו מימוש מעשי של SRP (כל שכבה אחריות אחת) ו-OCP (הוספת שכבה בלי לגעת בקיימות) על תשתית שרת אמיתית — ובנינו אותו מאפס, שלב אחר שלב: קודם שכבה בודדת שרק ממשיכה הלאה, אחר כך שכבה שיודעת לעצור, ולבסוף השילוב של שתיהן יחד לפי סדר מכוון.

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

Microsoft Learn — ASP.NET Core Middleware


תרגילים

תרגיל 1 — Middleware לוג ראשון

המשימה: כתבו Middleware שמדפיס את ה-Method וה-Path של כל בקשה נכנסת, ורשמו אותו ראשון בשרשרת. ודאו שהוא רץ על כל endpoint שתגדירו.

בדיקה: כל בקשה (לכל endpoint) מדפיסה שורת לוג לפני שהתשובה חוזרת.

תרגיל 2 — Middleware עוצר

המשימה: כתבו Middleware שבודק אם קיים Header בשם X-Api-Key, ואם לא — מחזיר 401 בלי לקרוא ל-next().

בדיקה: בקשה בלי ה-Header מקבלת 401 ולא מגיעה ל-endpoint; בקשה עם ה-Header מגיעה כרגיל.


פרויקט מסכם

המשימה: בנו שרשרת Middleware בת 3 שכבות לפרויקט ASP.NET Core פשוט.

דרישות:

  1. Middleware לוג — מדפיס Method+Path+זמן לפני, ו-Status Code אחרי (עם await next())
  2. Middleware הרשאה — בודק Header ומחזיר 401 אם חסר, בלי next()
  3. Middleware טיפול-שגיאות — עוטף את next() ב-try/catch, ומחזיר 500 עם הודעה גנרית אם נזרקה חריגה בלתי-מטופלת מהשכבות הפנימיות

בדיקה: בקשה תקינה עם Header עוברת את כל השכבות ומגיעה ל-endpoint; בקשה בלי Header נעצרת ב-401; endpoint שזורק חריגה מכוונת נתפס על ידי Middleware השגיאות ומחזיר 500 מסודר, לא Stack Trace גולמי.

מה בפרק הבא

בפרק הבא — Controllers: איך מארגנים endpoints בצורה מובנית עם [Route]/[HttpGet] וכו’, במקום app.MapGet בודדים ב-Program.cs.