# Repository Pattern - Detailed Reference
Detailed code examples for Repository pattern in ASP.NET Core with Entity Framework Core.
## Table of Contents
1. [Aggregate Root Pattern](#aggregate-root-pattern)
2. [Repository Interface Design](#repository-interface-design)
3. [EF Core Implementation](#ef-core-implementation)
4. [Unit of Work Pattern](#unit-of-work-pattern)
5. [Entity Configuration](#entity-configuration)
6. [CQRS with Dapper](#cqrs-with-dapper)
7. [Specification Pattern](#specification-pattern)
8. [DI Registration](#di-registration)
---
## Aggregate Root Pattern
### Domain Entities
```csharp
///
/// EN: Marker interface for aggregate roots.
/// VI: Interface đánh dấu aggregate roots.
///
public interface IAggregateRoot { }
///
/// EN: Base entity with common properties.
/// VI: Entity cơ sở với các properties chung.
///
public abstract class Entity
{
public Guid Id { get; protected set; }
public DateTime CreatedAt { get; protected set; }
public DateTime? UpdatedAt { get; protected set; }
protected Entity()
{
Id = Guid.NewGuid();
CreatedAt = DateTime.UtcNow;
}
public override bool Equals(object? obj)
{
if (obj is not Entity other)
return false;
if (ReferenceEquals(this, other))
return true;
if (GetType() != other.GetType())
return false;
return Id == other.Id;
}
public override int GetHashCode() => Id.GetHashCode();
}
```
### Order Aggregate Example
```csharp
///
/// EN: Order aggregate root.
/// VI: Aggregate root cho Order.
///
public class Order : Entity, IAggregateRoot
{
public string UserId { get; private set; }
public OrderStatus Status { get; private set; }
public Address ShippingAddress { get; private set; }
public decimal TotalAmount { get; private set; }
private readonly List _orderItems = new();
public IReadOnlyCollection OrderItems => _orderItems.AsReadOnly();
// EN: Private constructor for EF Core
// VI: Constructor private cho EF Core
private Order() { }
public Order(string userId, Address shippingAddress)
{
UserId = userId ?? throw new ArgumentNullException(nameof(userId));
ShippingAddress = shippingAddress ?? throw new ArgumentNullException(nameof(shippingAddress));
Status = OrderStatus.Draft;
}
///
/// EN: Add item through aggregate root to maintain invariants.
/// VI: Thêm item thông qua aggregate root để duy trì invariants.
///
public void AddItem(Guid productId, int quantity, decimal unitPrice)
{
if (Status != OrderStatus.Draft)
throw new DomainException("Cannot add items to non-draft order");
var existingItem = _orderItems.FirstOrDefault(x => x.ProductId == productId);
if (existingItem != null)
{
existingItem.AddQuantity(quantity);
}
else
{
_orderItems.Add(new OrderItem(Id, productId, quantity, unitPrice));
}
RecalculateTotal();
}
///
/// EN: Update item quantity through aggregate root.
/// VI: Cập nhật số lượng item thông qua aggregate root.
///
public void UpdateItemQuantity(Guid itemId, int newQuantity)
{
var item = _orderItems.FirstOrDefault(x => x.Id == itemId)
?? throw new DomainException($"Item {itemId} not found");
if (newQuantity <= 0)
{
_orderItems.Remove(item);
}
else
{
item.SetQuantity(newQuantity);
}
RecalculateTotal();
}
public void Submit()
{
if (Status != OrderStatus.Draft)
throw new DomainException("Only draft orders can be submitted");
if (!_orderItems.Any())
throw new DomainException("Cannot submit empty order");
Status = OrderStatus.Submitted;
UpdatedAt = DateTime.UtcNow;
}
private void RecalculateTotal()
{
TotalAmount = _orderItems.Sum(x => x.GetSubtotal());
UpdatedAt = DateTime.UtcNow;
}
}
///
/// EN: Order item - child entity of Order aggregate.
/// VI: Order item - entity con của Order aggregate.
///
public class OrderItem : Entity
{
public Guid OrderId { get; private set; }
public Guid ProductId { get; private set; }
public int Quantity { get; private set; }
public decimal UnitPrice { get; private set; }
private OrderItem() { }
internal OrderItem(Guid orderId, Guid productId, int quantity, decimal unitPrice)
{
OrderId = orderId;
ProductId = productId;
Quantity = quantity > 0 ? quantity : throw new ArgumentException("Quantity must be positive");
UnitPrice = unitPrice >= 0 ? unitPrice : throw new ArgumentException("Price cannot be negative");
}
internal void AddQuantity(int quantity)
{
Quantity += quantity;
}
internal void SetQuantity(int quantity)
{
Quantity = quantity > 0 ? quantity : throw new ArgumentException("Quantity must be positive");
}
public decimal GetSubtotal() => Quantity * UnitPrice;
}
///
/// EN: Value object for address.
/// VI: Value object cho địa chỉ.
///
public record Address(
string Street,
string City,
string State,
string PostalCode,
string Country);
public enum OrderStatus
{
Draft,
Submitted,
Processing,
Shipped,
Delivered,
Cancelled
}
```
---
## Repository Interface Design
### Base Repository Interface
```csharp
///
/// EN: Generic repository interface for aggregate roots.
/// VI: Interface repository generic cho aggregate roots.
///
public interface IRepository where T : class, IAggregateRoot
{
///
/// EN: Unit of Work for transaction management.
/// VI: Unit of Work cho quản lý transaction.
///
IUnitOfWork UnitOfWork { get; }
Task GetByIdAsync(Guid id, CancellationToken ct = default);
Task AddAsync(T entity, CancellationToken ct = default);
void Update(T entity);
void Delete(T entity);
}
///
/// EN: Unit of Work interface.
/// VI: Interface Unit of Work.
///
public interface IUnitOfWork : IDisposable
{
Task SaveChangesAsync(CancellationToken ct = default);
Task SaveEntitiesAsync(CancellationToken ct = default);
}
```
### Specific Repository Interface
```csharp
///
/// EN: Order repository interface with domain-specific methods.
/// VI: Interface repository Order với các phương thức domain-specific.
///
public interface IOrderRepository : IRepository
{
///
/// EN: Get order with all items loaded.
/// VI: Lấy order với tất cả items được load.
///
Task GetWithItemsAsync(Guid id, CancellationToken ct = default);
///
/// EN: Get orders by user with pagination.
/// VI: Lấy orders theo user với phân trang.
///
Task> GetByUserIdAsync(
string userId,
int skip = 0,
int take = 20,
CancellationToken ct = default);
///
/// EN: Get orders by status.
/// VI: Lấy orders theo status.
///
Task> GetByStatusAsync(
OrderStatus status,
CancellationToken ct = default);
}
```
---
## EF Core Implementation
### DbContext as Unit of Work
```csharp
///
/// EN: Application DbContext implementing Unit of Work.
/// VI: DbContext ứng dụng triển khai Unit of Work.
///
public class ApplicationDbContext : DbContext, IUnitOfWork
{
public DbSet Orders => Set();
public DbSet OrderItems => Set();
public ApplicationDbContext(DbContextOptions options)
: base(options) { }
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.ApplyConfigurationsFromAssembly(typeof(ApplicationDbContext).Assembly);
}
///
/// EN: Save entities with domain events support.
/// VI: Lưu entities với hỗ trợ domain events.
///
public async Task SaveEntitiesAsync(CancellationToken ct = default)
{
// EN: Dispatch domain events before saving
// VI: Dispatch domain events trước khi lưu
// await _mediator.DispatchDomainEventsAsync(this);
await SaveChangesAsync(ct);
return true;
}
}
```
### Repository Implementation
```csharp
///
/// EN: Order repository implementation with EF Core.
/// VI: Triển khai repository Order với EF Core.
///
public class OrderRepository : IOrderRepository
{
private readonly ApplicationDbContext _context;
public IUnitOfWork UnitOfWork => _context;
public OrderRepository(ApplicationDbContext context)
{
_context = context ?? throw new ArgumentNullException(nameof(context));
}
public async Task GetByIdAsync(Guid id, CancellationToken ct = default)
{
return await _context.Orders.FindAsync(new object[] { id }, ct);
}
public async Task GetWithItemsAsync(Guid id, CancellationToken ct = default)
{
return await _context.Orders
.Include(o => o.OrderItems)
.FirstOrDefaultAsync(o => o.Id == id, ct);
}
public async Task> GetByUserIdAsync(
string userId,
int skip = 0,
int take = 20,
CancellationToken ct = default)
{
return await _context.Orders
.Where(o => o.UserId == userId)
.OrderByDescending(o => o.CreatedAt)
.Skip(skip)
.Take(take)
.ToListAsync(ct);
}
public async Task> GetByStatusAsync(
OrderStatus status,
CancellationToken ct = default)
{
return await _context.Orders
.Where(o => o.Status == status)
.OrderByDescending(o => o.CreatedAt)
.ToListAsync(ct);
}
public async Task AddAsync(Order order, CancellationToken ct = default)
{
var entry = await _context.Orders.AddAsync(order, ct);
return entry.Entity;
}
public void Update(Order order)
{
_context.Entry(order).State = EntityState.Modified;
}
public void Delete(Order order)
{
_context.Orders.Remove(order);
}
}
```
---
## Entity Configuration
### Fluent API Configuration
```csharp
///
/// EN: EF Core configuration for Order entity.
/// VI: Cấu hình EF Core cho entity Order.
///
public class OrderConfiguration : IEntityTypeConfiguration
{
public void Configure(EntityTypeBuilder builder)
{
builder.ToTable("Orders");
builder.HasKey(o => o.Id);
builder.Property(o => o.UserId)
.IsRequired()
.HasMaxLength(36);
builder.Property(o => o.Status)
.IsRequired()
.HasConversion();
builder.Property(o => o.TotalAmount)
.HasPrecision(18, 2);
// EN: Configure owned type for Address
// VI: Cấu hình owned type cho Address
builder.OwnsOne(o => o.ShippingAddress, a =>
{
a.Property(x => x.Street).HasMaxLength(200);
a.Property(x => x.City).HasMaxLength(100);
a.Property(x => x.State).HasMaxLength(100);
a.Property(x => x.PostalCode).HasMaxLength(20);
a.Property(x => x.Country).HasMaxLength(100);
});
// EN: Configure navigation to OrderItems
// VI: Cấu hình navigation đến OrderItems
var navigation = builder.Metadata.FindNavigation(nameof(Order.OrderItems));
navigation?.SetPropertyAccessMode(PropertyAccessMode.Field);
builder.HasMany(o => o.OrderItems)
.WithOne()
.HasForeignKey(oi => oi.OrderId)
.OnDelete(DeleteBehavior.Cascade);
}
}
///
/// EN: EF Core configuration for OrderItem entity.
/// VI: Cấu hình EF Core cho entity OrderItem.
///
public class OrderItemConfiguration : IEntityTypeConfiguration
{
public void Configure(EntityTypeBuilder builder)
{
builder.ToTable("OrderItems");
builder.HasKey(oi => oi.Id);
builder.Property(oi => oi.UnitPrice)
.HasPrecision(18, 2);
builder.HasIndex(oi => oi.OrderId);
}
}
```
---
## CQRS with Dapper
### Query DTOs
```csharp
///
/// EN: Lightweight DTO for order queries.
/// VI: DTO nhẹ cho order queries.
///
public record OrderSummaryDto(
Guid Id,
string Status,
decimal TotalAmount,
DateTime CreatedAt,
int ItemCount);
public record OrderDetailDto(
Guid Id,
string UserId,
string Status,
decimal TotalAmount,
DateTime CreatedAt,
AddressDto ShippingAddress,
List Items);
public record OrderItemDto(
Guid Id,
Guid ProductId,
string ProductName,
int Quantity,
decimal UnitPrice,
decimal Subtotal);
public record AddressDto(
string Street,
string City,
string State,
string PostalCode,
string Country);
```
### Dapper Query Handler
```csharp
///
/// EN: Query handler using Dapper for optimized reads.
/// VI: Query handler dùng Dapper cho đọc tối ưu.
///
public class GetUserOrdersQueryHandler
: IRequestHandler>
{
private readonly IDbConnection _connection;
public GetUserOrdersQueryHandler(IDbConnection connection)
{
_connection = connection;
}
public async Task> Handle(
GetUserOrdersQuery request,
CancellationToken ct)
{
// EN: Count total records
// VI: Đếm tổng số records
const string countSql = @"
SELECT COUNT(*) FROM Orders WHERE UserId = @UserId";
var total = await _connection.ExecuteScalarAsync(countSql, new { request.UserId });
// EN: Get paginated results
// VI: Lấy kết quả phân trang
const string dataSql = @"
SELECT
o.Id,
o.Status,
o.TotalAmount,
o.CreatedAt,
COUNT(oi.Id) as ItemCount
FROM Orders o
LEFT JOIN OrderItems oi ON o.Id = oi.OrderId
WHERE o.UserId = @UserId
GROUP BY o.Id, o.Status, o.TotalAmount, o.CreatedAt
ORDER BY o.CreatedAt DESC
OFFSET @Skip ROWS FETCH NEXT @Take ROWS ONLY";
var orders = await _connection.QueryAsync(dataSql, new
{
request.UserId,
request.Skip,
request.Take
});
return new PagedResult(
orders.ToList(),
total,
request.Skip / request.Take + 1,
request.Take);
}
}
///
/// EN: Get order detail with items using Dapper multi-mapping.
/// VI: Lấy chi tiết order với items dùng Dapper multi-mapping.
///
public class GetOrderDetailQueryHandler
: IRequestHandler
{
private readonly IDbConnection _connection;
public async Task Handle(
GetOrderDetailQuery request,
CancellationToken ct)
{
const string sql = @"
SELECT
o.Id, o.UserId, o.Status, o.TotalAmount, o.CreatedAt,
o.ShippingAddress_Street as Street,
o.ShippingAddress_City as City,
o.ShippingAddress_State as State,
o.ShippingAddress_PostalCode as PostalCode,
o.ShippingAddress_Country as Country
FROM Orders o
WHERE o.Id = @OrderId AND o.UserId = @UserId;
SELECT
oi.Id, oi.ProductId, p.Name as ProductName,
oi.Quantity, oi.UnitPrice,
(oi.Quantity * oi.UnitPrice) as Subtotal
FROM OrderItems oi
JOIN Products p ON oi.ProductId = p.Id
WHERE oi.OrderId = @OrderId";
using var multi = await _connection.QueryMultipleAsync(sql, new
{
request.OrderId,
request.UserId
});
var order = await multi.ReadFirstOrDefaultAsync();
if (order == null) return null;
var items = (await multi.ReadAsync()).ToList();
return new OrderDetailDto(
order.Id,
order.UserId,
order.Status,
order.TotalAmount,
order.CreatedAt,
new AddressDto(order.Street, order.City, order.State, order.PostalCode, order.Country),
items);
}
}
```
---
## Specification Pattern
### Generic Specification
```csharp
///
/// EN: Specification pattern for complex queries.
/// VI: Specification pattern cho queries phức tạp.
///
public abstract class Specification
{
public abstract Expression> ToExpression();
public bool IsSatisfiedBy(T entity)
{
return ToExpression().Compile()(entity);
}
public Specification And(Specification other)
=> new AndSpecification(this, other);
public Specification Or(Specification other)
=> new OrSpecification(this, other);
public Specification Not()
=> new NotSpecification(this);
}
public class AndSpecification : Specification
{
private readonly Specification _left;
private readonly Specification _right;
public AndSpecification(Specification left, Specification right)
{
_left = left;
_right = right;
}
public override Expression> ToExpression()
{
var leftExpr = _left.ToExpression();
var rightExpr = _right.ToExpression();
var param = Expression.Parameter(typeof(T));
var body = Expression.AndAlso(
Expression.Invoke(leftExpr, param),
Expression.Invoke(rightExpr, param));
return Expression.Lambda>(body, param);
}
}
```
### Order Specifications
```csharp
///
/// EN: Specification for orders by user.
/// VI: Specification cho orders theo user.
///
public class OrderByUserSpecification : Specification
{
private readonly string _userId;
public OrderByUserSpecification(string userId) => _userId = userId;
public override Expression> ToExpression()
=> order => order.UserId == _userId;
}
///
/// EN: Specification for orders by status.
/// VI: Specification cho orders theo status.
///
public class OrderByStatusSpecification : Specification
{
private readonly OrderStatus _status;
public OrderByStatusSpecification(OrderStatus status) => _status = status;
public override Expression> ToExpression()
=> order => order.Status == _status;
}
// EN: Usage / VI: Cách dùng
var spec = new OrderByUserSpecification(userId)
.And(new OrderByStatusSpecification(OrderStatus.Submitted));
var orders = await _context.Orders
.Where(spec.ToExpression())
.ToListAsync();
```
---
## DI Registration
### Program.cs Configuration
```csharp
///
/// EN: Register repositories and DbContext.
/// VI: Đăng ký repositories và DbContext.
///
// EN: DbContext registration
// VI: Đăng ký DbContext
builder.Services.AddDbContext(options =>
{
options.UseNpgsql(
builder.Configuration.GetConnectionString("DefaultConnection"),
npgsqlOptions =>
{
npgsqlOptions.MigrationsAssembly(typeof(ApplicationDbContext).Assembly.FullName);
npgsqlOptions.EnableRetryOnFailure(
maxRetryCount: 3,
maxRetryDelay: TimeSpan.FromSeconds(30),
errorCodesToAdd: null);
});
if (builder.Environment.IsDevelopment())
{
options.EnableSensitiveDataLogging();
options.EnableDetailedErrors();
}
});
// EN: Repository registrations
// VI: Đăng ký repositories
builder.Services.AddScoped();
builder.Services.AddScoped();
builder.Services.AddScoped();
// EN: Dapper connection for queries
// VI: Dapper connection cho queries
builder.Services.AddScoped(_ =>
new NpgsqlConnection(builder.Configuration.GetConnectionString("DefaultConnection")));
// EN: Register MediatR
// VI: Đăng ký MediatR
builder.Services.AddMediatR(cfg =>
cfg.RegisterServicesFromAssembly(typeof(Program).Assembly));
```
---
## Resources / Tài Nguyên
- [Microsoft: Implementing Repository Pattern](https://docs.microsoft.com/en-us/dotnet/architecture/microservices/microservice-ddd-cqrs-patterns/infrastructure-persistence-layer-implementation-entity-framework-core)
- [DDD Aggregate Pattern](https://martinfowler.com/bliki/DDD_Aggregate.html)
- [CQRS Pattern](https://docs.microsoft.com/en-us/azure/architecture/patterns/cqrs)
- [Dapper Documentation](https://github.com/DapperLib/Dapper)