DEV Community

Νίκος Σταυρόπουλος
Νίκος Σταυρόπουλος

Posted on • Edited on

Repository Pattern στη C# με SOLID αρχές

Image description

Το Repository Pattern είναι ένα από τα πιο δημοφιλή (design patterns) για τον διαχωρισμό της επιχειρηματικής λογικής από την πρόσβαση στη βάση δεδομένων. Επιτρέπει στον κώδικα να είναι καθαρός, ευέλικτος και επεκτάσιμος.

- Γιατί να χρησιμοποιούμε το Repository Pattern;

Πλεονεκτήματα

Διαχωρισμός Επιπέδων – Το Repository απομονώνει τη βάση δεδομένων από το business logic.
Εύκολη αντικατάσταση – Αν θέλουμε να αλλάξουμε από Entity Framework σε Dapper ή άλλη ORM, μπορούμε να το κάνουμε χωρίς να αλλάξουμε όλη την εφαρμογή.
Mocking για Unit Tests – Μπορούμε εύκολα να κάνουμε mock τα repositories, βελτιώνοντας τη δυνατότητα δοκιμών.
Καλύτερη διαχείριση δεδομένων – Παρέχει μία κεντρική προσέγγιση για πρόσβαση στα δεδομένα.

- SOLID Αρχές και Repository Pattern

Όταν εφαρμόζουμε το Repository Pattern, είναι σημαντικό να ακολουθούμε τις αρχές SOLID για να διατηρούμε τον κώδικα καθαρό και επεκτάσιμο.

1️⃣ Single Responsibility Principle (SRP) – Κάθε repository πρέπει να έχει μία ευθύνη: την πρόσβαση στα δεδομένα ενός συγκεκριμένου entity.
2️⃣ Open-Closed Principle (OCP) – Πρέπει να μπορούμε να προσθέτουμε νέα repositories ή queries χωρίς να αλλάζουμε τον υπάρχοντα κώδικα.
3️⃣ Liskov Substitution Principle (LSP) – Κάθε repository μπορεί να αντικατασταθεί από ένα άλλο χωρίς να προκαλέσει προβλήματα.
4️⃣ Interface Segregation Principle (ISP) – Αντί για ένα repository με πολλές μεθόδους, είναι καλύτερο να έχουμε εξειδικευμένα interfaces.
5️⃣ Dependency Inversion Principle (DIP) – Το repository δεν πρέπει να εξαρτάται άμεσα από την ORM (π.χ. Entity Framework), αλλά από ένα abstraction.

Παράδειγμα Repository στη C# με SOLID Αρχές

1️⃣ Οντότητα (Entity)

Ας υποθέσουμε ότι έχουμε μια εφαρμογή με Πελάτες (Customers).

public class Customer
{
    public int Id { get; set; }
    public string Name { get; set; }
    public string Email { get; set; }
}

Enter fullscreen mode Exit fullscreen mode

2️⃣ Δημιουργία του Γενικού Repository (Generic Repository)

Ένα γενικό repository (Generic Repository) μας επιτρέπει να έχουμε επανχρησιμοποιήσιμο κώδικα.

public interface IRepository<T> where T : class
{
    Task<T?> GetByIdAsync(int id);
    Task<IEnumerable<T>> GetAllAsync();
    Task AddAsync(T entity);
    Task UpdateAsync(T entity);
    Task DeleteAsync(int id);
}
Enter fullscreen mode Exit fullscreen mode

3️⃣ Υλοποίηση του Generic Repository με το Entity Framework

Το EF Core θα χρησιμοποιηθεί για την πρόσβαση στη βάση δεδομένων.

public class Repository<T> : IRepository<T> where T : class
{
    protected readonly AppDbContext _context;
    protected readonly DbSet<T> _dbSet;

    public Repository(AppDbContext context)
    {
        _context = context;
        _dbSet = context.Set<T>();
    }

    public async Task<T?> GetByIdAsync(int id) => await _dbSet.FindAsync(id);

    public async Task<IEnumerable<T>> GetAllAsync() => await _dbSet.ToListAsync();

    public async Task AddAsync(T entity)
    {
        await _dbSet.AddAsync(entity);
        await _context.SaveChangesAsync();
    }

    public async Task UpdateAsync(T entity)
    {
        _dbSet.Update(entity);
        await _context.SaveChangesAsync();
    }

    public async Task DeleteAsync(int id)
    {
        var entity = await _dbSet.FindAsync(id);
        if (entity != null)
        {
            _dbSet.Remove(entity);
            await _context.SaveChangesAsync();
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

4️⃣ Δημιουργία Εξειδικευμένου Repository για Πελάτες (Customer Repository)

Αντί να προσθέτουμε όλες τις μεθόδους σε ένα repository, δημιουργούμε εξειδικευμένα repositories.

4.1 Ορίζουμε ένα εξειδικευμένο interface

public interface ICustomerRepository : IRepository<Customer>
{
    Task<Customer?> GetByEmailAsync(string email);
}
Enter fullscreen mode Exit fullscreen mode

4.2 Υλοποίηση του Customer Repository

public class CustomerRepository : Repository<Customer>, ICustomerRepository
{
    public CustomerRepository(AppDbContext context) : base(context) { }

    public async Task<Customer?> GetByEmailAsync(string email)
    {
        return await _context.Customers.FirstOrDefaultAsync(c => c.Email == email);
    }
}
Enter fullscreen mode Exit fullscreen mode

Είναι καλό να έχουμε πολλά Repositories ή ένα μεγάλο με πολλές μεθόδους;

👉 Η καλύτερη πρακτική είναι να έχουμε μικρά, εξειδικευμένα repositories.

🔹 Πρόβλημα με ένα τεράστιο repository:

  • Γίνεται δύσκολο στη συντήρηση.
  • Παραβιάζει το Interface Segregation Principle (ISP).
  • Αν η εφαρμογή μεγαλώσει, το repository γίνεται πολυπλοκό.

🔹 Λύση:

  • Ένα Generic Repository για τις βασικές λειτουργίες (CRUD).
  • Εξειδικευμένα Repositories για σύνθετες queries ή business logic (π.χ. ICustomerRepository).

🔹 Πώς μπορούμε να κληρονομήσουμε ένα interface χωρίς να χρειαστεί να το υλοποιήσουμε;

Στη C#, αν μια κλάση κληρονομεί ένα interface, πρέπει να υλοποιήσει όλες τις μεθόδους του. Όμως υπάρχει τρόπος να αποφύγουμε την υποχρεωτική υλοποίηση!

👉 Λύση: Χρησιμοποιούμε μια ενδιάμεση κλάση (abstract class ή base class).

Παράδειγμα

Έστω ότι έχουμε ένα interface ICustomerRepository και θέλουμε να δημιουργήσουμε ένα BaseRepository που να μην χρειάζεται να υλοποιήσει όλες τις μεθόδους.

public abstract class BaseRepository<T> : IRepository<T> where T : class
{
    public virtual Task<T?> GetByIdAsync(int id) => throw new NotImplementedException();
    public virtual Task<IEnumerable<T>> GetAllAsync() => throw new NotImplementedException();
    public virtual Task AddAsync(T entity) => throw new NotImplementedException();
    public virtual Task UpdateAsync(T entity) => throw new NotImplementedException();
    public virtual Task DeleteAsync(int id) => throw new NotImplementedException();
}
Enter fullscreen mode Exit fullscreen mode

Τώρα, όποιο repository κληρονομήσει το BaseRepository, δεν χρειάζεται να υλοποιήσει όλα τα CRUD methods αν δεν τα χρειάζεται!

📌 Συμπέρασμα
✔ Το Repository Pattern βελτιώνει τη δομή του κώδικα.
✔ Είναι προτιμότερο να έχουμε εξειδικευμένα repositories αντί για ένα τεράστιο.
✔ Αν θέλουμε να κληρονομήσουμε ένα interface χωρίς υλοποίηση, χρησιμοποιούμε abstract classes.

Δείτε ακόμα : Κατηγορίες των Durable Functions και πότε να τις χρησιμοποιήσω

Top comments (0)