Files
pos-system/microservices/.agent/skills/repository-pattern/references/REFERENCE.md
Ho Ngoc Hai 76d75c753b Migrate
2026-05-23 18:37:02 +07:00

21 KiB

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
  2. Repository Interface Design
  3. EF Core Implementation
  4. Unit of Work Pattern
  5. Entity Configuration
  6. CQRS with Dapper
  7. Specification Pattern
  8. DI Registration

Aggregate Root Pattern

Domain Entities

/// <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

/// <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

/// <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

/// <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

/// <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

/// <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

/// <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

/// <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

/// <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

/// <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

/// <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

/// <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