Repository pattern: separate data access
A service asks an interface for data. The repository hides where that data comes from, so the service does not depend on a database or a list.
Watch it animatedStep through it, with the running line highlighted. Free with an account.The idea
The service depends on IProductRepository, a contract for the data it needs. ListProductRepository is one implementation; a database-backed repository can replace it without changing ProductService. The visual follows one request through the service, interface, and implementation.
Code
struct Product { int id; string name; };
class ProductRepository {
public:
virtual optional<Product> getById(int id) = 0;
virtual ~ProductRepository() = default;
};
class ListProductRepository : public ProductRepository {
vector<Product> products;
public:
ListProductRepository(vector<Product> products) : products(move(products)) {}
optional<Product> getById(int id) override {
for (auto& p : products) if (p.id == id) return p;
return nullopt;
}
};
class ProductService {
ProductRepository& repository;
public:
ProductService(ProductRepository& repository) : repository(repository) {}
string nameOf(int id) { auto p = repository.getById(id); return p ? p->name : "missing"; }
};
int main() {
ListProductRepository repository({{7, "Book"}, {8, "Pen"}});
ProductService service(repository);
cout << service.nameOf(7) << "\n";
}from dataclasses import dataclass
from typing import Protocol
@dataclass(frozen=True)
class Product:
id: int
name: str
class ProductRepository(Protocol):
def get_by_id(self, id: int) -> Product | None: ...
class ListProductRepository:
def __init__(self, products: list[Product]):
self.products = products
def get_by_id(self, id: int) -> Product | None:
return next((p for p in self.products if p.id == id), None)
class ProductService:
def __init__(self, repository: ProductRepository):
self.repository = repository
def name_of(self, id: int) -> str:
p = self.repository.get_by_id(id); return p.name if p else "missing"
repository = ListProductRepository([Product(7, "Book"), Product(8, "Pen")])
service = ProductService(repository)
print(service.name_of(7))record Product(int Id, string Name);
interface IProductRepository {
Product? GetById(int id);
}
class ListProductRepository : IProductRepository {
private readonly List<Product> products;
public ListProductRepository(List<Product> products) { this.products = products; }
public Product? GetById(int id) => products.FirstOrDefault(p => p.Id == id);
}
class ProductService {
private readonly IProductRepository repository;
public ProductService(IProductRepository repository) { this.repository = repository; }
public string NameOf(int id) => repository.GetById(id)?.Name ?? "missing";
}
var repository = new ListProductRepository(
new List<Product> { new(7, "Book"), new(8, "Pen") });
var service = new ProductService(repository);
Console.WriteLine(service.NameOf(7));Tips
- Use a repository when it gives the application a useful boundary around persistence or makes storage easy to replace in tests.
- Avoid adding a generic repository automatically around every EF Core DbSet. EF Core already provides query and persistence abstractions, and another layer can hide useful capabilities.
In the .NET library
In dependency injection, register IProductRepository with the implementation to use. Tests can provide a small fake repository with predictable data.