# 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)