The Repository Pattern separates data access logic from business logic behind a simple interface. This article covers the core pattern in ASP.NET Core: defining the interface and implementation, wiring it into the built-in DI container, and testing it without touching a real database. A generic base repository and the Unit of Work pattern build on these same ideas and get their own articles.

Το Repository Pattern διαχωρίζει τη λογική πρόσβασης δεδομένων από την επιχειρησιακή λογική πίσω από ένα απλό interface. Αυτό το άρθρο καλύπτει το βασικό pattern σε ASP.NET Core: ορισμός του interface και της υλοποίησης, σύνδεση με το ενσωματωμένο DI container, και testing χωρίς πραγματική βάση δεδομένων. Το generic base repository και το μοτίβο Unit of Work χτίζουν πάνω σε αυτές τις ίδιες ιδέες και θα καλυφθούν σε ξεχωριστά άρθρα.

Diagram: client app calls ProductsController, which calls ProductService, which depends on the IProductRepository interface, which can be swapped between SqlProductRepository in production and MockProductRepository in tests
Swap the implementation, keep the interface: the service only ever depends on IProductRepository, so production can use SqlProductRepository while tests use a mock, wired up with a single AddScoped call.
Αλλάζεις την υλοποίηση, κρατάς το interface: το service εξαρτάται μόνο από το IProductRepository, οπότε στην παραγωγή χρησιμοποιείται το SqlProductRepository ενώ στα tests ένα mock, με σύνδεση μέσω μιας μόνο κλήσης AddScoped.

1. What is the Repository Pattern?

1. Τι είναι το Repository Pattern;

An interface (IOrderRepository) describes the operations available for a given entity, and a concrete class (OrderRepository) implements them against EF Core. Everything else in the app, controllers, services, talks to the interface and never touches AppDbContext directly.

Ένα interface (IOrderRepository) περιγράφει τις διαθέσιμες λειτουργίες για μια οντότητα, και μια συγκεκριμένη κλάση (OrderRepository) τις υλοποιεί μέσω EF Core. Ό,τι άλλο υπάρχει στην εφαρμογή, controllers, services, επικοινωνεί με το interface και ποτέ απευθείας με το AppDbContext.

public interface IOrderRepository
{
    IEnumerable<Order> GetAll();
    Order? GetById(int id);
    void Add(Order order);
    void Update(Order order);
    void Delete(Order order);
}

public class OrderRepository : IOrderRepository
{
    private readonly AppDbContext _context;

    public OrderRepository(AppDbContext context)
    {
        _context = context;
    }

    public IEnumerable<Order> GetAll() => _context.Orders.ToList();
    public Order? GetById(int id) => _context.Orders.Find(id);
    public void Add(Order order) { _context.Orders.Add(order); _context.SaveChanges(); }
    public void Update(Order order) { _context.Orders.Update(order); _context.SaveChanges(); }
    public void Delete(Order order) { _context.Orders.Remove(order); _context.SaveChanges(); }
}

The playground below has no real database, so SqlOrderRepository here keeps orders in memory instead of calling EF Core, but the shape is the same. Try swapping it for InMemoryOrderRepository on the last line and hit Run: OrderService never has to change.

Το playground παρακάτω δεν έχει πραγματική βάση δεδομένων, οπότε το SqlOrderRepository εδώ κρατά τις παραγγελίες στη μνήμη αντί να καλεί το EF Core, αλλά το σχήμα είναι το ίδιο. Δοκίμασε να το αντικαταστήσεις με InMemoryOrderRepository στην τελευταία γραμμή και πάτησε Run: το OrderService δεν χρειάζεται ποτέ να αλλάξει.

Live example, editable, runs on .NET Fiddle
Ζωντανό παράδειγμα, επεξεργάσιμο, τρέχει στο .NET Fiddle

2. Wiring it up with .NET's built-in DI container

2. Σύνδεση με το ενσωματωμένο DI container του .NET

.NET has dependency injection built in. Once you have IOrderRepository and OrderRepository, you register them in Program.cs so the framework creates and injects them automatically wherever they're needed (controllers, services, minimal API endpoints).

Το .NET διαθέτει ενσωματωμένο dependency injection. Μόλις έχετε το IOrderRepository και το OrderRepository, τα καταχωρείτε στο Program.cs ώστε το framework να τα δημιουργεί και να τα "εγχέει" αυτόματα όπου χρειάζονται (controllers, services, minimal API endpoints).

// Program.cs
var builder = WebApplication.CreateBuilder(args);

builder.Services.AddDbContext<AppDbContext>(options =>
    options.UseSqlServer(builder.Configuration.GetConnectionString("Default")));

builder.Services.AddScoped<IOrderRepository, OrderRepository>();
builder.Services.AddScoped<OrderService>();

var app = builder.Build();

AddScoped means one instance per HTTP request, the standard lifetime for anything holding a DbContext. Controllers then just ask for IOrderRepository or OrderService in their constructor and .NET provides the right instance.

Το AddScoped σημαίνει ένα instance ανά HTTP request, ο συνηθισμένος κύκλος ζωής για οτιδήποτε κρατά ένα DbContext. Οι controllers στη συνέχεια απλώς ζητούν IOrderRepository ή OrderService στον constructor τους και το .NET παρέχει το σωστό instance.

[ApiController]
[Route("api/[controller]")]
public class OrdersController : ControllerBase
{
    private readonly OrderService _orderService;

    public OrdersController(OrderService orderService)
    {
        _orderService = orderService;
    }

    [HttpGet("{id}")]
    public ActionResult<Order> Get(int id)
    {
        var order = _orderService.FindOrder(id);
        return order is null ? NotFound() : Ok(order);
    }

    [HttpPost]
    public ActionResult<Order> Post(Order order)
    {
        _orderService.PlaceOrder(order);
        return CreatedAtAction(nameof(Get), new { id = order.Id }, order);
    }
}

3. Testing in a .NET project

3. Testing σε ένα .NET project

With xUnit (or NUnit/MSTest) plus Moq, you mock IOrderRepository directly instead of writing a fake class:

Με xUnit (ή NUnit/MSTest) και Moq, κάνετε mock απευθείας το IOrderRepository αντί να γράψετε μια fake κλάση:

[Fact]
public void PlaceOrder_CallsAddOnRepository()
{
    var mockRepo = new Mock<IOrderRepository>();
    var service = new OrderService(mockRepo.Object);

    var order = new Order { Id = 1, Total = 99.90m };
    service.PlaceOrder(order);

    mockRepo.Verify(r => r.Add(order), Times.Once);
}

No AppDbContext, no SQL Server LocalDB, no test data seeding, just the service's own logic, verified in isolation. For integration tests you can still swap in EF Core's in-memory provider (UseInMemoryDatabase) against the real OrderRepository when you specifically want to test the EF mapping.

Χωρίς AppDbContext, χωρίς SQL Server LocalDB, χωρίς seeding test δεδομένων, μόνο η λογική του service, ελεγμένη μεμονωμένα. Για integration tests μπορείτε ακόμα να χρησιμοποιήσετε τον in-memory provider του EF Core (UseInMemoryDatabase) πάνω στο πραγματικό OrderRepository, όταν θέλετε ειδικά να ελέγξετε το EF mapping.

4. Takeaways

4. Συμπεράσματα

  • Depend on the interface (IOrderRepository), not the concrete implementation, so the data access logic can be swapped or mocked freely.
  • Register repositories as Scoped in the DI container, matching the DbContext lifetime.
  • Moq (or NSubstitute) removes the need to hand-write fake repositories for most unit tests.
  • Reserve EF Core's in-memory or SQLite providers for integration tests that specifically validate query/mapping behavior, not for everyday unit tests.
  • A generic base repository and the Unit of Work pattern build on these same ideas; they get their own articles.
  • Εξαρτηθείτε από το interface (IOrderRepository) και όχι από τη συγκεκριμένη υλοποίηση, ώστε η λογική πρόσβασης δεδομένων να μπορεί να αλλάξει ή να γίνει mock ελεύθερα.
  • Καταχωρείστε τα repositories ως Scoped στο DI container, ώστε να ταιριάζει με τον κύκλο ζωής του DbContext.
  • Το Moq (ή το NSubstitute) εξαλείφει την ανάγκη να γράφετε χειροκίνητα fake repositories για τα περισσότερα unit tests.
  • Κρατήστε τους in-memory ή SQLite providers του EF Core για integration tests που επικυρώνουν συγκεκριμένα query/mapping συμπεριφορά, όχι για καθημερινά unit tests.
  • Το generic base repository και το μοτίβο Unit of Work χτίζουν πάνω σε αυτές τις ίδιες ιδέες· θα καλυφθούν σε ξεχωριστά άρθρα.