Middleware Pipeline
מה זה?
בשיעור הקודם כתבנו כמה 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); // מגיע לכאן רק אחרי ה-Middlewareapp.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 פשוט.
דרישות:
- Middleware לוג — מדפיס Method+Path+זמן לפני, ו-Status Code אחרי (עם
await next()) - Middleware הרשאה — בודק Header ומחזיר
401אם חסר, בליnext() - Middleware טיפול-שגיאות — עוטף את
next()ב-try/catch, ומחזיר500עם הודעה גנרית אם נזרקה חריגה בלתי-מטופלת מהשכבות הפנימיות
בדיקה: בקשה תקינה עם Header עוברת את כל השכבות ומגיעה ל-endpoint; בקשה בלי Header נעצרת ב-401; endpoint שזורק חריגה מכוונת נתפס על ידי Middleware השגיאות ומחזיר 500 מסודר, לא Stack Trace גולמי.
מה בפרק הבא
בפרק הבא — Controllers: איך מארגנים endpoints בצורה מובנית עם [Route]/[HttpGet] וכו’, במקום app.MapGet בודדים ב-Program.cs.