771 lines
21 KiB
Markdown
771 lines
21 KiB
Markdown
# 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)
|