Core API Repository Pattern

The BookStore API currently has database logic inside the BookService. As the application grows, this creates a problem — business logic and data access logic are mixed together, making the code harder to test and maintain. The Repository Pattern solves this by creating a dedicated layer for database operations.

What Is the Repository Pattern?

A repository acts as a collection-like interface for accessing data. It hides all database-specific code (EF Core, SQL) behind a clean interface. The service layer calls repository methods and never touches DbContext directly.

Before Repository Pattern:
  Controller → Service (has DbContext + business logic mixed)

After Repository Pattern:
  Controller → Service (business logic only)
                  |
                  ▼
              Repository (database access only)
                  |
                  ▼
              DbContext → SQL Server

Benefits of the Repository Pattern

BenefitExplanation
Separation of concernsBusiness logic and database logic are in separate classes
TestabilityIn unit tests, the repository can be replaced with a fake (mock)
Single database providerSwitching from SQL Server to PostgreSQL only changes the repository
Clean service layerServices focus only on business rules, not SQL or EF Core details

Step 1 – Create the Generic Repository Interface

A generic repository works with any entity type, reducing code duplication. Create a Repositories folder:

// Repositories/IRepository.cs
namespace BookStoreAPI.Repositories
{
    public interface IRepository<T> where T : class
    {
        Task<List<T>> GetAllAsync();
        Task<T?> GetByIdAsync(int id);
        Task<T> AddAsync(T entity);
        Task UpdateAsync(T entity);
        Task DeleteAsync(T entity);
        Task SaveAsync();
    }
}

Step 2 – Create a Specific Book Repository Interface

The book repository extends the generic one and adds book-specific queries:

// Repositories/IBookRepository.cs
using BookStoreAPI.Models;

namespace BookStoreAPI.Repositories
{
    public interface IBookRepository : IRepository<Book>
    {
        Task<List<Book>> GetByCategoryAsync(string category);
        Task<List<Book>> GetAvailableBooksAsync();
    }
}

Step 3 – Create the Book Repository Implementation

// Repositories/BookRepository.cs
using BookStoreAPI.Data;
using BookStoreAPI.Models;
using Microsoft.EntityFrameworkCore;

namespace BookStoreAPI.Repositories
{
    public class BookRepository : IBookRepository
    {
        private readonly BookStoreDbContext _context;

        public BookRepository(BookStoreDbContext context)
        {
            _context = context;
        }

        public async Task<List<Book>> GetAllAsync()
        {
            return await _context.Books.ToListAsync();
        }

        public async Task<Book?> GetByIdAsync(int id)
        {
            return await _context.Books.FindAsync(id);
        }

        public async Task<Book> AddAsync(Book book)
        {
            await _context.Books.AddAsync(book);
            return book;
        }

        public Task UpdateAsync(Book book)
        {
            _context.Books.Update(book);
            return Task.CompletedTask;
        }

        public Task DeleteAsync(Book book)
        {
            _context.Books.Remove(book);
            return Task.CompletedTask;
        }

        public async Task SaveAsync()
        {
            await _context.SaveChangesAsync();
        }

        // Book-specific queries
        public async Task<List<Book>> GetByCategoryAsync(string category)
        {
            return await _context.Books
                .Where(b => b.Category == category)
                .ToListAsync();
        }

        public async Task<List<Book>> GetAvailableBooksAsync()
        {
            return await _context.Books
                .Where(b => b.IsAvailable)
                .ToListAsync();
        }
    }
}

Step 4 – Update BookService to Use the Repository

The service no longer touches DbContext. It only calls repository methods:

// Services/BookService.cs
using BookStoreAPI.Models;
using BookStoreAPI.Repositories;

namespace BookStoreAPI.Services
{
    public class BookService : IBookService
    {
        private readonly IBookRepository _bookRepository;

        public BookService(IBookRepository bookRepository)
        {
            _bookRepository = bookRepository;
        }

        public async Task<List<Book>> GetAllAsync()
        {
            return await _bookRepository.GetAllAsync();
        }

        public async Task<Book?> GetByIdAsync(int id)
        {
            return await _bookRepository.GetByIdAsync(id);
        }

        public async Task<Book> CreateAsync(Book book)
        {
            book.CreatedDate = DateTime.UtcNow;
            await _bookRepository.AddAsync(book);
            await _bookRepository.SaveAsync();
            return book;
        }

        public async Task<bool> UpdateAsync(int id, Book updatedBook)
        {
            var book = await _bookRepository.GetByIdAsync(id);
            if (book == null) return false;

            book.Title = updatedBook.Title;
            book.Author = updatedBook.Author;
            book.Price = updatedBook.Price;
            book.Category = updatedBook.Category;
            book.IsAvailable = updatedBook.IsAvailable;

            await _bookRepository.UpdateAsync(book);
            await _bookRepository.SaveAsync();
            return true;
        }

        public async Task<bool> DeleteAsync(int id)
        {
            var book = await _bookRepository.GetByIdAsync(id);
            if (book == null) return false;

            await _bookRepository.DeleteAsync(book);
            await _bookRepository.SaveAsync();
            return true;
        }

        public async Task<List<Book>> GetByCategoryAsync(string category)
        {
            return await _bookRepository.GetByCategoryAsync(category);
        }
    }
}

Step 5 – Register the Repository in Program.cs

// Program.cs
builder.Services.AddScoped<IBookRepository, BookRepository>();
builder.Services.AddScoped<IBookService, BookService>();

The Full Dependency Chain

HTTP Request
     |
     v
BooksController
  needs → IBookService
               |
               v
          BookService
            needs → IBookRepository
                          |
                          v
                     BookRepository
                       needs → BookStoreDbContext
                                     |
                                     v
                                SQL Server → Books Table

ASP.NET Core's DI container automatically resolves this entire chain. When a request arrives, the framework creates BookStoreDbContext, injects it into BookRepository, injects the repository into BookService, and injects the service into BooksController — all in one step.

Updated Project Structure

BookStoreAPI/
├── Controllers/
│   └── BooksController.cs
├── Data/
│   └── BookStoreDbContext.cs
├── Models/
│   └── Book.cs
├── Repositories/                 ← NEW
│   ├── IRepository.cs            ← NEW
│   ├── IBookRepository.cs        ← NEW
│   └── BookRepository.cs         ← NEW
├── Services/
│   ├── IBookService.cs
│   └── BookService.cs            ← Updated (no DbContext)
└── Program.cs                    ← Updated (repository registered)

Key Points

  • The Repository Pattern creates a dedicated layer for database access, keeping it separate from business logic.
  • A generic repository interface reduces code duplication across multiple entities.
  • The service layer only calls repository methods — it no longer references DbContext or EF Core directly.
  • Repositories are registered as Scoped services in Program.cs, matching the DbContext lifetime.
  • The pattern makes unit testing easier because repositories can be replaced with simple fakes (mocks).

Leave a Comment