# 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
/// <summary>
/// EN: Marker interface for aggregate roots.
/// VI: Interface đánh dấu aggregate roots.
/// </summary>
public interface IAggregateRoot { }

/// <summary>
/// EN: Base entity with common properties.
/// VI: Entity cơ sở với các properties chung.
/// </summary>
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
/// <summary>
/// EN: Order aggregate root.
/// VI: Aggregate root cho Order.
/// </summary>
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<OrderItem> _orderItems = new();
    public IReadOnlyCollection<OrderItem> 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;
    }

    /// <summary>
    /// EN: Add item through aggregate root to maintain invariants.
    /// VI: Thêm item thông qua aggregate root để duy trì invariants.
    /// </summary>
    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();
    }

    /// <summary>
    /// EN: Update item quantity through aggregate root.
    /// VI: Cập nhật số lượng item thông qua aggregate root.
    /// </summary>
    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;
    }
}

/// <summary>
/// EN: Order item - child entity of Order aggregate.
/// VI: Order item - entity con của Order aggregate.
/// </summary>
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;
}

/// <summary>
/// EN: Value object for address.
/// VI: Value object cho địa chỉ.
/// </summary>
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
/// <summary>
/// EN: Generic repository interface for aggregate roots.
/// VI: Interface repository generic cho aggregate roots.
/// </summary>
public interface IRepository<T> where T : class, IAggregateRoot
{
    /// <summary>
    /// EN: Unit of Work for transaction management.
    /// VI: Unit of Work cho quản lý transaction.
    /// </summary>
    IUnitOfWork UnitOfWork { get; }

    Task<T?> GetByIdAsync(Guid id, CancellationToken ct = default);
    Task<T> AddAsync(T entity, CancellationToken ct = default);
    void Update(T entity);
    void Delete(T entity);
}

/// <summary>
/// EN: Unit of Work interface.
/// VI: Interface Unit of Work.
/// </summary>
public interface IUnitOfWork : IDisposable
{
    Task<int> SaveChangesAsync(CancellationToken ct = default);
    Task<bool> SaveEntitiesAsync(CancellationToken ct = default);
}
```

### Specific Repository Interface

```csharp
/// <summary>
/// EN: Order repository interface with domain-specific methods.
/// VI: Interface repository Order với các phương thức domain-specific.
/// </summary>
public interface IOrderRepository : IRepository<Order>
{
    /// <summary>
    /// EN: Get order with all items loaded.
    /// VI: Lấy order với tất cả items được load.
    /// </summary>
    Task<Order?> GetWithItemsAsync(Guid id, CancellationToken ct = default);

    /// <summary>
    /// EN: Get orders by user with pagination.
    /// VI: Lấy orders theo user với phân trang.
    /// </summary>
    Task<IReadOnlyList<Order>> GetByUserIdAsync(
        string userId,
        int skip = 0,
        int take = 20,
        CancellationToken ct = default);

    /// <summary>
    /// EN: Get orders by status.
    /// VI: Lấy orders theo status.
    /// </summary>
    Task<IReadOnlyList<Order>> GetByStatusAsync(
        OrderStatus status,
        CancellationToken ct = default);
}
```

---

## EF Core Implementation

### DbContext as Unit of Work

```csharp
/// <summary>
/// EN: Application DbContext implementing Unit of Work.
/// VI: DbContext ứng dụng triển khai Unit of Work.
/// </summary>
public class ApplicationDbContext : DbContext, IUnitOfWork
{
    public DbSet<Order> Orders => Set<Order>();
    public DbSet<OrderItem> OrderItems => Set<OrderItem>();

    public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options)
        : base(options) { }

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        modelBuilder.ApplyConfigurationsFromAssembly(typeof(ApplicationDbContext).Assembly);
    }

    /// <summary>
    /// EN: Save entities with domain events support.
    /// VI: Lưu entities với hỗ trợ domain events.
    /// </summary>
    public async Task<bool> 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
/// <summary>
/// EN: Order repository implementation with EF Core.
/// VI: Triển khai repository Order với EF Core.
/// </summary>
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<Order?> GetByIdAsync(Guid id, CancellationToken ct = default)
    {
        return await _context.Orders.FindAsync(new object[] { id }, ct);
    }

    public async Task<Order?> GetWithItemsAsync(Guid id, CancellationToken ct = default)
    {
        return await _context.Orders
            .Include(o => o.OrderItems)
            .FirstOrDefaultAsync(o => o.Id == id, ct);
    }

    public async Task<IReadOnlyList<Order>> 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<IReadOnlyList<Order>> GetByStatusAsync(
        OrderStatus status,
        CancellationToken ct = default)
    {
        return await _context.Orders
            .Where(o => o.Status == status)
            .OrderByDescending(o => o.CreatedAt)
            .ToListAsync(ct);
    }

    public async Task<Order> 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
/// <summary>
/// EN: EF Core configuration for Order entity.
/// VI: Cấu hình EF Core cho entity Order.
/// </summary>
public class OrderConfiguration : IEntityTypeConfiguration<Order>
{
    public void Configure(EntityTypeBuilder<Order> builder)
    {
        builder.ToTable("Orders");
        
        builder.HasKey(o => o.Id);
        
        builder.Property(o => o.UserId)
            .IsRequired()
            .HasMaxLength(36);

        builder.Property(o => o.Status)
            .IsRequired()
            .HasConversion<string>();

        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);
    }
}

/// <summary>
/// EN: EF Core configuration for OrderItem entity.
/// VI: Cấu hình EF Core cho entity OrderItem.
/// </summary>
public class OrderItemConfiguration : IEntityTypeConfiguration<OrderItem>
{
    public void Configure(EntityTypeBuilder<OrderItem> 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
/// <summary>
/// EN: Lightweight DTO for order queries.
/// VI: DTO nhẹ cho order queries.
/// </summary>
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<OrderItemDto> 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
/// <summary>
/// EN: Query handler using Dapper for optimized reads.
/// VI: Query handler dùng Dapper cho đọc tối ưu.
/// </summary>
public class GetUserOrdersQueryHandler 
    : IRequestHandler<GetUserOrdersQuery, PagedResult<OrderSummaryDto>>
{
    private readonly IDbConnection _connection;

    public GetUserOrdersQueryHandler(IDbConnection connection)
    {
        _connection = connection;
    }

    public async Task<PagedResult<OrderSummaryDto>> 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<int>(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<OrderSummaryDto>(dataSql, new
        {
            request.UserId,
            request.Skip,
            request.Take
        });

        return new PagedResult<OrderSummaryDto>(
            orders.ToList(),
            total,
            request.Skip / request.Take + 1,
            request.Take);
    }
}

/// <summary>
/// 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.
/// </summary>
public class GetOrderDetailQueryHandler 
    : IRequestHandler<GetOrderDetailQuery, OrderDetailDto?>
{
    private readonly IDbConnection _connection;

    public async Task<OrderDetailDto?> 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<dynamic>();
        if (order == null) return null;

        var items = (await multi.ReadAsync<OrderItemDto>()).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
/// <summary>
/// EN: Specification pattern for complex queries.
/// VI: Specification pattern cho queries phức tạp.
/// </summary>
public abstract class Specification<T>
{
    public abstract Expression<Func<T, bool>> ToExpression();

    public bool IsSatisfiedBy(T entity)
    {
        return ToExpression().Compile()(entity);
    }

    public Specification<T> And(Specification<T> other)
        => new AndSpecification<T>(this, other);

    public Specification<T> Or(Specification<T> other)
        => new OrSpecification<T>(this, other);

    public Specification<T> Not()
        => new NotSpecification<T>(this);
}

public class AndSpecification<T> : Specification<T>
{
    private readonly Specification<T> _left;
    private readonly Specification<T> _right;

    public AndSpecification(Specification<T> left, Specification<T> right)
    {
        _left = left;
        _right = right;
    }

    public override Expression<Func<T, bool>> 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<Func<T, bool>>(body, param);
    }
}
```

### Order Specifications

```csharp
/// <summary>
/// EN: Specification for orders by user.
/// VI: Specification cho orders theo user.
/// </summary>
public class OrderByUserSpecification : Specification<Order>
{
    private readonly string _userId;

    public OrderByUserSpecification(string userId) => _userId = userId;

    public override Expression<Func<Order, bool>> ToExpression()
        => order => order.UserId == _userId;
}

/// <summary>
/// EN: Specification for orders by status.
/// VI: Specification cho orders theo status.
/// </summary>
public class OrderByStatusSpecification : Specification<Order>
{
    private readonly OrderStatus _status;

    public OrderByStatusSpecification(OrderStatus status) => _status = status;

    public override Expression<Func<Order, bool>> 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
/// <summary>
/// EN: Register repositories and DbContext.
/// VI: Đăng ký repositories và DbContext.
/// </summary>

// EN: DbContext registration
// VI: Đăng ký DbContext
builder.Services.AddDbContext<ApplicationDbContext>(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<IOrderRepository, OrderRepository>();
builder.Services.AddScoped<IUserRepository, UserRepository>();
builder.Services.AddScoped<IProductRepository, ProductRepository>();

// EN: Dapper connection for queries
// VI: Dapper connection cho queries
builder.Services.AddScoped<IDbConnection>(_ =>
    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)
