היכרות עם Minimal API
Minimal API מדבר על איך כותבים Web API בצורה נכונה ויעילה יותר.
Minimal API עתיד להחליף לגמרי את ה-controllers.
למה להחליף את ה-controllers?
- איפה ה-controllers יושבים? הם שייכים לספריית MVC של Microsoft. זה לא המבנה שאנחנו מדברים עליו ב-Web API. הספרייה כבר לא שייכת למה שאנחנו עושים ורוצים להפטר ממנה.
- יש לנו clas עם הרבה פונקציות. בקשה מגיעה מה-client ל-server. ה-server מזהה שהוא אמור להגיע לאיזה פונקציה שיושבת ב-controller. הוא מייצר instance של ה-controller ומפעיל את הפונקציה האחת הזאת. כלומר בכל הפעמים שנייצר instance של ה-class הזה, נשתמש בפונקציה אחת. זה מיותר מבחינת זיכרון.
- לפעמים כל פונקציה צריכה לעשות שימוש בשירות אחר, וכל המצבים האלה מייצרים controller שיש בו המון depedencies, אבל כל פונקציה משתמשת בחלק קטן מהם.
- מודרניזציה – אנחנו רוצים שהקוד יהיה דומה לטכנולוגיות פשוטות אחרות וזמן לימוד אפסי למתכנתים.
Minimal API יפתור את הבעיות ויעזור לביצועים. כמובן שיהיו בעיות אחרות.
יצירת Minimal API
Minimal API צריך שני דברים כדי להתחיל לעבוד איתו, מורידים את ה-controllers, כלומר את AddControllers ואחת MapCOntrollers מה-program.
נכתוב Minimal API ראשון פשוט. אנחנו ניגשים ל-app ומשתמשים באופרטור הרלוונטי.
app.MapGet();
app.MapPost();
app.MapDelete();
app.MapPut();
נתחיל עם MapGet. כשמישהו רוצה את ה-time, זאת הפונקציה שאני רוצה לספק לו.
app.MapGet("time", () => DateTime.Now);
אם נריץ את התוכנית נראה שנפתח ה-swagger עם הנתיב שלנו ועם הפונקציה שעובדת. לא יצרנו class.
אם נרצה נוכל להעביר פרמטר בצורה הזאת:
app.MapGet("time/{id}", (int id) => $"The value id {id}");
אפשר להעביר כמה פרמטרים שחלקם גם יהיו אופציונלים:
app.MapGet("time/{name}/{id}",
(string name, int id, string? description) =>
$"The value {id} {description} {name}");
עד עכשיו היו מקרים פשוטים, אבל מה עם DI, async וכו'? נניח שאני רוצה לקבל את רשימת המוצרים. מה שעשינו קודם ב-controller זה הזרקנו ל-constructor את השירות והשתמשנו בו.
פה אנחנו מבקשים את השירות ישירות לפונקציה.
app.MapGet("products", async (IProductsRepository repo) => {
var result = await repo.GetAllProducts();
return result;
});
לסיכום:
- Minimal API מייתר את הצורך לייצר instance של class נוסף שזה ה-controller. מביאים pointer לפונקציה עבור url.
- כאשר יש שירותים נוספים עובדים עם DI ברמת הפונקציה.
Use TypeResults
כאשר החזרנו את רשימת המוצרים ב-controller הגדרנו את זה בחתימת הפונקציה וגם החזרנו Ok. הבעיה היא שה-Ok מגיע מה-controllerBase שירשנו ממנו את ה-controller, שאנחנו רוצים להמנע מייצור ה-instance שלו. בשביל בכל זאת להחזיר result יש לנו את ה-TypedResults שנוכל להשתמש בו.
app.MapGet("products", async (IProductsRepository repo) => {
var result = await repo.GetAllProducts();
return TypedResults.Ok(result);
});
עכשיו נשאר לנו לאכוף את ה-tyoe שאנחנו רוצים להחזיר, אחרת יהיו לנו שגיאות בהחזרת type לא נכון. נשנה את החתימה של הפונקציה שלנו כדי שנגדיר בדיוק את מה שהיא מחזירה.
app.MapGet("products", async Task<Ok<List<Product>>> (IProductsRepository repo) => {
var result = await repo.GetAllProducts();
return TypedResults.Ok(result);
});
עכשיו אנחנו רוצים לחקות את הפונקציה שהיתה לנו ב-controller שהחזירה גם notFound. נזכר איך היתה כתובה הפונקציה ב-controller.
קובץ ProductsController.cs
[HttpGet("{id}")]
[ProducesResponseType(200, Type = typeof(Product))]
[ProducesResponseType(404)]
public async Task<ActionResult<Product>> GetById(int id) {
var result = await _productRepository.GetProductById(id);
if(result == null) {
return NotFound();
} else {
return Ok(result);
}
}
בפונקציה הקודמת של כל המוצרים הגדרנו שהיא מחזירה Ok ואם ננסה להחזיר NotFound תהיה לנו בעיית קומפילציה, אנחנו רוצים להחזיר פה כמה אפשרויות. את זה נצטרך להגיד לפונקציה.
app.MapGet("products/{id}", async Task<Results<NoFound, Ok<Product>>> (int id, IProductsRepository repo) => {
var result = await repo.GetProductById(id);
if(result == null) {
return TypedResults.NotFound();
}
return TypedResults.Ok(result);
});
מה שחסר לנו עדיין זה לקבץ את הפונקציות תחת משפחה אחת, שאת זה גם עשה ה-controller וגם הקוד נמצא לנו בתוך קובץ ה-program וזה יוצר בלאגן.