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
| Benefit | Explanation |
|---|---|
| Separation of concerns | Business logic and database logic are in separate classes |
| Testability | In unit tests, the repository can be replaced with a fake (mock) |
| Single database provider | Switching from SQL Server to PostgreSQL only changes the repository |
| Clean service layer | Services 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
DbContextor 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).
