# תבנית DAL — IRepository ו-Service Layer📖 שיעור

## מה זה?

בשיעור הקודם למדנו איך ASP.NET מזריק תלות — נותנים ל-Controller Interface בקונסטרוקטור, ו-ASP.NET דואג להעביר לו מימוש אמיתי, בלי שה-Controller יצטרך ליצור אותו בעצמו. השיעור הזה משתמש **באותו מנגנון בדיוק**, רק על משהו אחר: לא "מי שולח התראה", אלא "איפה שמורים הנתונים".

נתחיל מהמצב הנוכחי של `StudentsController`, נראה בדיוק למה הוא ייתקע ביחידה הבאה (כשעוברים למסד נתונים אמיתי), ונבנה פתרון שלב-שלב שמכין אותנו מראש למעבר הזה.

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

• Repository Pattern — הפשטה של גישה לנתונים מאחורי Interface, כדי שהלוגיקה העסקית לא תדע (ולא תצטרך לדעת) איך הנתונים נשמרים בפועל

• `IRepository<T>` — Interface גנרי עם פעולות CRUD בסיסיות (`GetAll`, `GetById`, `Add`, `Update`, `Delete`)

• Service Layer — שכבה בין ה-Controller ל-Repository, מחזיקה לוגיקה עסקית (כמו ולידציה עסקית מורכבת) שלא שייכת ל-HTTP ולא לגישה-לנתונים

• Three-Layer Architecture — Controller (HTTP) → Service (לוגיקה עסקית) → Repository (גישה לנתונים), כל שכבה לא יודעת על מה שמעליה

## בונים את התבנית שלב אחר שלב

### שלב 0: המצב הנוכחי — הנתונים חיים בתוך ה-Controller

```csharp
public class StudentsController : ControllerBase
{
    private static List<Student> _students = new();

    [HttpGet]
    public IActionResult GetAll() => Ok(_students);

    [HttpPost]
    public IActionResult Create([FromBody] Student newStudent)
    {
        _students.Add(newStudent);
        return CreatedAtAction(nameof(GetById), new { id = newStudent.Id }, newStudent);
    }
    // ... שאר ה-Actions
}
```

שתי נקודות ששווה לעצור עליהן לפני שממשיכים:

- `private static List<Student> _students` — למה `static` דווקא? ASP.NET יוצר Controller **חדש** לכל בקשת HTTP (זוכרים? זה בדיוק מה שראינו בשיעור ה-Controllers). אם `_students` היה שדה רגיל (בלי `static`), כל בקשה הייתה מקבלת רשימה ריקה חדשה, וכל תלמיד שנוצר היה "נעלם" ברגע שהבקשה מסתיימת. `static` אומר "שדה אחד, משותף לכל המופעים של המחלקה" — כך הרשימה נשארת קיימת בין בקשה לבקשה. זו בדיוק הסיבה ש-`static` כאן היא "טלאי" ולא פתרון אמיתי — היא עובדת, אבל רק כי אנחנו "עוקפים" את זה שה-Controller עצמו נוצר מחדש כל פעם.
- הנתונים והלוגיקה שמטפלת ב-HTTP (`GetAll`, `Create`) יושבים **באותה מחלקה בדיוק**.

זה עובד — אבל ביחידה הבאה עוברים למסד נתונים אמיתי (SQL). מה יקרה אז? **כל** שורת קוד ב-`StudentsController` שנוגעת ב-`_students` תצטרך להשתנות — ב-`GetAll`, ב-`Create`, בכל Action. תבנית **DAL** (Data Access Layer) פותרת את זה מראש.

### שלב 1: מוציאים את הגישה לנתונים ל-Interface

בדיוק כמו ששלב 2 בשיעור הקודם הוציא את "שליחת ההתראה" ל-`INotifier`, עכשיו מוציאים את "הגישה לנתונים" ל-Interface. קודם מגדירים **מה** אפשר לעשות עם הנתונים, בלי לומר **איך**:

```csharp
// IRepository<T> — Generic, בדיוק כמו ביחידת ה-Generics
public interface IRepository<T>
{
    List<T> GetAll();
    T GetById(int id);
    void Add(T item);
    void Update(T item);
    void Delete(int id);
}
```

ה-`<T>` הוא Generic — זוכרים מיחידת ה-Generics? במקום לכתוב `IRepository` נפרד לכל סוג נתון (`IStudentRepository`, `IProductRepository`...), כותבים אותו **פעם אחת** עם פרמטר-טיפוס `T`, וכל אחד ממלא אותו לפי הצורך: `IRepository<Student>` הוא הבטחה ל-CRUD על תלמידים, `IRepository<Product>` — על מוצרים, עם אותו קוד Interface בדיוק.

### שלב 2: המימוש הראשון — בדיוק מה שהיה קודם, רק מועבר החוצה

```csharp
public class InMemoryStudentRepository : IRepository<Student>
{
    private List<Student> _students = new List<Student>();

    public List<Student> GetAll() => _students;
    public Student GetById(int id) => _students.FirstOrDefault(s => s.Id == id);
    public void Add(Student item) => _students.Add(item);
    public void Update(Student item) { /* עדכון בתוך הרשימה */ }
    public void Delete(int id) => _students.RemoveAll(s => s.Id == id);
}
```

שימו לב לשלוש נקודות:

- `: IRepository<Student>` — המחלקה הזו **מממשת** את ההבטחה מהשלב הקודם, בדיוק כמו ש-`EmailNotifier` מימש `: INotifier`.
- `_students` כאן הוא שדה **רגיל**, לא `static` — כי עכשיו הוא לא חי בתוך Controller שנוצר מחדש בכל בקשה. בהמשך השיעור נראה שה-Repository עצמו יירשם כ-`Scoped`/`Singleton` ב-DI, ואז ה-Container הוא זה ש"שומר אותו בחיים" בין הקריאות — לא הפתרון המאולתר של `static`.
- `FirstOrDefault` — מיחידת ה-LINQ: מחפש ברשימה לפי התנאי בסוגריים, ומחזיר את האיבר הראשון שמתאים, או `null` אם אף אחד לא מתאים.

חוץ מהעטיפה, זה **בדיוק** מה שהיה קודם בתוך ה-Controller — רק עבר למחלקה נפרדת, שמיישמת את ה-Interface מהשלב הקודם.

### שלב 3: חיבור ה-Controller ל-Repository דרך DI

בדיוק כמו שבשיעור הקודם `OrdersController` קיבל `INotifier` בקונסטרוקטור, עכשיו `StudentsController` מקבל `IRepository<Student>`:

```csharp
public class StudentsController : ControllerBase
{
    private readonly IRepository<Student> _repository;
    public StudentsController(IRepository<Student> repository) => _repository = repository;

    [HttpGet]
    public IActionResult GetAll() => Ok(_repository.GetAll());
}
```

(`public StudentsController(IRepository<Student> repository) => _repository = repository;` הוא בדיוק אותו קונסטרוקטור כמו בשיעור הקודם, רק כתוב בתחביר מקוצר — `=>` במקום `{ _repository = repository; }` — שני הכתיבים זהים לגמרי בפועל.)

וברישום ב-`Program.cs`, בדיוק אותה שורה מהמנגנון שכבר מכירים:

```csharp
// Program.cs
builder.Services.AddScoped<IRepository<Student>, InMemoryStudentRepository>();
```

"כשמישהו מבקש `IRepository<Student>`, תן לו `InMemoryStudentRepository`." ה-Controller כבר לא יודע אם הנתונים ב-`List`, ב-SQL, או בכל מקום אחר — הוא רק יודע שיש לו `IRepository<Student>` שיודע לבצע CRUD.

### שלב 4: מתי Repository לבד לא מספיק

מה קורה אם צריך כלל כמו "גיל מינימלי 16 להרשמה"? זה לא שייך ל-Controller (זו לא לוגיקת HTTP), וגם לא ל-Repository (זו לא גישה-לנתונים — Repository לא אמור "לדעת" כללים עסקיים). לכלל הזה אין כרגע בית, אז מוסיפים שכבה שלישית באמצע:

```csharp
public class StudentService
{
    private readonly IRepository<Student> _repository;   // תלות בהבטחה, לא במימוש — בדיוק כמו בשיעור הקודם

    public StudentService(IRepository<Student> repository) => _repository = repository;

    public Student RegisterStudent(string name, int age)
    {
        if (age < 16) throw new ArgumentException("Minimum registration age is 16");   // הכלל העסקי חי כאן
        var student = new Student(name, age);
        _repository.Add(student);
        return student;
    }
}
```

`StudentService` היא מחלקה רגילה שאנחנו כותבים, לא Controller ולא Repository. היא מקבלת `IRepository<Student>` בקונסטרוקטור — בדיוק אותו Constructor Injection — ומוסיפה עליו את הכלל העסקי לפני שהיא בכלל פונה ל-Repository.

### שלב 5: ה-Controller הופך לדק — HTTP בלבד

```csharp
public class StudentsController : ControllerBase
{
    private readonly StudentService _service;
    public StudentsController(StudentService service) => _service = service;

    [HttpPost]
    public IActionResult Create([FromBody] Student newStudent)
    {
        var student = _service.RegisterStudent(newStudent.Name, newStudent.Age);
        return CreatedAtAction(nameof(GetById), new { id = student.Id }, student);
    }
}
```

ה-Controller כבר לא מקבל `IRepository<Student>` בכלל — הוא מקבל `StudentService`, וזה כל מה שהוא צריך לדעת. הוא לא נוגע בכלל הגיל המינימלי, ולא נוגע ב-Repository — הוא רק מתרגם בין HTTP (`Create`, גוף הבקשה) לקריאה למתודה `RegisterStudent`.

לרישום ב-`Program.cs` מצטרפת שורה נוספת, בשביל השכבה החדשה:

```csharp
// Program.cs — רושמים את כל שלוש השכבות
builder.Services.AddScoped<IRepository<Student>, InMemoryStudentRepository>();
builder.Services.AddScoped<StudentService>();
```

השורה השנייה רושמת את `StudentService` עצמה (בלי Interface — כי אין כרגע צורך להחליף מימוש ל-Service, רק ל-Repository). ASP.NET יודע לבנות אותה: כשהוא רואה שה-`StudentService` מבקש `IRepository<Student>` בקונסטרוקטור שלה, הוא כבר יודע (מהשורה הראשונה) מה להעביר לה.

**התוצאה:** שלוש שכבות ברורות, כל אחת מוזרקת לזו שמעליה:

```
Client
  │  POST /api/students
  ▼
StudentsController   (HTTP בלבד: קורא ל-body, מחזיר Status Code)
  ▼
StudentService       (לוגיקה עסקית: בדיקת גיל מינימלי)
  ▼
IRepository<Student>  (גישה לנתונים: הוספה, קריאה, עדכון, מחיקה)
```

## הסבר עיקרי

שלוש השכבות, שלוש אחריות (SRP במימוש ארכיטקטוני) — Controller יודע **רק** HTTP. Service יודע **רק** לוגיקה עסקית — הוא לא יודע אם הנתונים ב-List, SQL, או MongoDB. Repository יודע **רק** גישה-לנתונים — הוא לא יודע כלום על HTTP או על כללים עסקיים. כל שכבה יכולה להשתנות בנפרד בלי לגעת באחרות.

למה זה מכין בדיוק ליחידה הבאה — כש-`InMemoryStudentRepository` יוחלף ב-`SqlStudentRepository` (עם EF Core, ביחידה הבאה), שניהם ימשיכו לממש את אותו `IRepository<Student>` — `StudentService` ו-`StudentsController` לא ישתנו **בכלל**. שינוי אחד, בשכבה אחת, בזכות ה-Interface.

## יתרונות

הפרדת שכבות מאפשרת להחליף מימוש נתונים (InMemory→SQL) בלי לגעת בלוגיקה עסקית או ב-HTTP; כל שכבה נבדקת (Unit Testing) בנפרד — אפשר לבדוק `StudentService` עם `IRepository` מדומה, בלי מסד נתונים אמיתי; מבנה ברור ועקבי בין כל ה-Controllers בפרויקט.

## חסרונות

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

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

• Controller = HTTP בלבד; Service = לוגיקה עסקית; Repository = גישה לנתונים

• `IRepository<T>` הוא ה-Interface שמאפשר להחליף InMemory ב-SQL בלי לגעת בשכבות מעליו

• Service Layer מחזיק לוגיקה עסקית שלא שייכת ל-HTTP ולא לגישה-ישירה-לנתונים

• זהו יישום ארכיטקטוני מלא של DIP — כל שכבה תלויה בהפשטה של השכבה שמתחתיה

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

• לשים לוגיקה עסקית (כמו בדיקת גיל מינימלי) ישירות ב-Controller במקום ב-Service

• לגשת ל-Repository ישירות מה-Controller, מדלגים על ה-Service Layer — מאבד את מקום הריכוז ללוגיקה עסקית

• ליצור `new InMemoryStudentRepository()` בתוך `StudentService` במקום להזריק דרך `IRepository<T>` — חוזר אחורה מ-DI/DIP, ומאבד את היכולת להחליף מימוש בלי לגעת ב-`StudentService`

## סיכום

בנינו את תבנית ה-DAL שלב אחר שלב: הוצאת הגישה לנתונים ל-`IRepository<T>`, מימוש InMemory שהוא בדיוק מה שהיה קודם בתוך ה-Controller, חיבור דרך DI, הוספת `StudentService` ללוגיקה העסקית, וסיום עם Controller דק שיודע רק HTTP. התוצאה: `Controller (HTTP בלבד) → Service (לוגיקה עסקית) → Repository (גישה לנתונים)`, כל שכבה לא יודעת על פרטי המימוש של מה שמתחתיה. מעבר ממימוש InMemory למסד נתונים אמיתי (ביחידה הבאה) יהיה שינוי מבודד לגמרי בשכבת ה-Repository בלבד.

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

[Microsoft Learn — Repository pattern](https://learn.microsoft.com/dotnet/architecture/microservices/microservice-ddd-cqrs-patterns/infrastructure-persistence-layer-design)

---

## תרגילים

### תרגיל 1 — IRepository ומימוש InMemory

**המשימה:** צרו `IRepository<Product>` עם CRUD בסיסי, ומימוש `InMemoryProductRepository`. השתמשו בו ישירות (עדיין בלי Service Layer) מ-Controller קיים, דרך DI.

**בדיקה:** כל פעולות ה-CRUD עובדות דרך ה-Repository, לא דרך `List` ישירות בתוך ה-Controller.

### תרגיל 2 — הוספת Service Layer

**המשימה:** הוסיפו `ProductService` בין ה-Controller ל-Repository מתרגיל 1, עם כלל עסקי אחד (למשל: מוצר עם `Price <= 0` נדחה עם `ArgumentException`).

**בדיקה:** ה-Controller קורא רק ל-`ProductService`, לא ל-Repository ישירות; ניסיון להוסיף מוצר עם מחיר לא-חוקי נדחה בשכבת ה-Service.

---

## פרויקט מסכם

**המשימה:** ארגנו מחדש את כל `TasksController` (מהפרויקטים המסכמים הקודמים) לפי תבנית DAL מלאה.

**דרישות:**
1. `IRepository<TaskItem>` עם CRUD מלא, ומימוש `InMemoryTaskRepository`
2. `TaskService` עם לפחות כלל עסקי אחד (למשל: לא ניתן ליצור משימה עם `Title` ריק, גם אם עברה DataAnnotations — בדיקה נוספת ברמה עסקית)
3. `TasksController` מוזרק עם `TaskService` בלבד — לא נוגע ב-`IRepository` ישירות בכלל

**בדיקה:** כל זרימת ה-CRUD הקיימת ממשיכה לעבוד זהה מבחוץ (אותם endpoints, אותם Status Codes); בדיקה שה-Controller לא מכיל אף שורת קוד שנוגעת ישירות ב-`IRepository`.

## מה בפרק הבא

בפרק הבא נעבור ליחידה חדשה — **SQL ו-EF Core**: נחליף את `InMemoryStudentRepository` במסד נתונים אמיתי שרץ בקונטיינר Docker (בדיוק כמו שלמדנו ביחידת ה-Docker), בלי לגעת ב-`StudentService` או ב-`StudentsController`.
