מבנה של Controller

כשרצינו לפנות למודל קונפיגורציה פנינו דרך:

builder.Services.Configure<HeadersRemoveConfig>
    (builder.Configuration.GetSection("HeadersToRemove"));

וקראנו ערכים ב-middleware עם IOptions:

public Task Invoke(HttpContext httpContext, IOptions<HeadersRemoveConfig> options)

מה קורה כשצריכים את הנתונים מהקונפיגורציה לא ב-middleware אלא במקום אחר, כמו בקונטרולר? לא נפנה דרך הפונקציה, אלא דרך ה-constructor.

Constructor של Controller

נייצר constructor ל-controller.

קובץ ProductsController.cs

public ProductsController() {

}

מתי נוצר ה-controller? יש לנו בקובץ program את AddControllers ולמטה יותר יש את MapControllers. ה-MapControllers בונה מבנה dictionary של ה-URL ואם מישהו מבקש כתובת, הוא מייצר instance של ה-controller וקורא לפונקציה המתאימה.

ל-controller נעביר את ה-IOptions ונקבל את הערכים הרלוונטים.

קבלת configuration ל-Controller

קובץ ProductsController.cs

private readonly HeadersRemoveConfig _headerConfig;

public ProductsController(IOptionsSnapshot<HeadersRemoveConfig> options) {
    this._headerConfig = options.Value;
}

לערכים של הקונפיג שנשמרים אפשר לגשת מתוך פונקציות ה-controllers.

בגדול לכל class שנרצה לקבל בו את ערכי הקונפיגורציה, נוכל לקבל את הערכים ל-constructor ולעבוד איתם.

מבנה של controller

Controller אמור לנהל איזה ישות מידע, למשל מוצר בדוגמא שלנו, וכל הפונקציונליות שקשורה למוצר אמורה להחשף החוצה בתוך ה-controller.

Controller הוא רכיב שנחשף לעולם. לא תשב בו הלוגיקה העסקית, הוא אמור להיות רזה ולהכיל רק את הפונקציונליות של להוציא את הדברים החוצה, הוא אמור להכיל מעט פעולות. הוא יבצע קריאה לשירותים אחרים שיבצעו את הלוגיקה.

ה-controller מוביל לפנייה למשאבים חיצוניים, למשל DB, לקבצים וכו' שכל הדברים האלה נמצאים על מקום אחר, ולכן אין עומס על השרת שבו יושב ה-API עצמו. השרת יכול לטפל רק ב-1,000 clients בו זמנית, ולכן צריך לבנות נכון את ה-controllers כדי שנוכל לטפל ביותר מ-1,000 פניות בצורה מקבילית.

הבאת מידע בצורה נכונה

נניח שאנחנו רוצים לכתוב פונקציה שתחזיר לנו רשימה של מוצרים, את המימוש של הפונקציה לא נכתוב ב-controller, נכתוב class שידע לבצע את הפעולות האלה ולהחזיר רשימה של ערכים.

כדי לעשות את זה ניצור class חדש. ניצור תיקיית Services ובתוכה ProductsRepository.cs.

כרגע יש לנו את ה-class של ProductDto שהוא הולך ל-client. נייצר עוד תיקייה בתוך תיקיית Models בשם Dtos שבה נשים את ה-class שיש לנו. וניצור עוד תיקייה של DataEntities שבה יש את הישויות האמיתיות שלי שמכילות את כל המידע. בה ניצור את ה-class של Product.

קובץ Product.cs

public record Product
{
    public required int id { get; init; }
    public string? name { get; init; }
    public string? description { get; init; }
    public decimal price { get; init; }
    public decimal? amount { get; init; }
    public string? producer { get; init; }
}

בתוך ProductsRepository נגדיר פונקציה שמחזירה רשימה של מוצרים. יש כרגע מוצר דמו, את הרשימה הזאת נשלוף מה-DB.

קובץ ProductsRepository.cs

public class ProductsRepository
{
    public List<Product> GetAllProducts() {
        return new List<Product>() {
            new Product {
                id = 1,
                name = "ASUS Computer",
                description = "Best computer",
                price = 2400,
                amount = 4,
                producer = "Asus"
            }
        };
    }
}

את ה-using נוציא מרמת הקובץ ונכניס ל-GlobalUsings ונוסיף global לפני.

לכאורה כדי להביא את רשימת המוצרים, אני צריכה ללכת ל-controller ולקרוא לפונקציה שמביאה את המוצרים.

קובץ ProductsController.cs

[HttpGet]
public ActionResult<List<Product>> Get() {
    ProductsRepository repo = new ProductsRepository();
    var result = repo.GetAllProducts();
    return Ok(result);
}

זה יעבוד, אבל זה לא נכון! יש פה coupling, שזה אומר ש-class אחד מכיר class אחר וזה לא מאפשר גמישות במערכת ולא אמורים לעשות את זה ככה. ProductsController מכיא את ProductsRepository וזה לא תקין.