chore: Refactor skill documentation for clarity and consistency

- Revised multiple skill documentation files to improve clarity and ensure consistency across descriptions and formatting.
- Enhanced the organization of content to facilitate easier navigation and understanding for developers.
- This update aims to streamline the documentation process and support better skill development practices.
This commit is contained in:
Ho Ngoc Hai
2026-01-14 11:49:05 +07:00
parent df007cafde
commit 6ca9027782
9 changed files with 3625 additions and 40 deletions

View File

@@ -0,0 +1,441 @@
---
name: deployment-kubernetes
description: Kubernetes deployment patterns. Use for Pods, Services, Ingress, Helm Charts, ConfigMaps, Secrets, và health probes.
compatibility: "Kubernetes 1.28+, Helm 3+"
metadata:
author: Velik Ho
version: "1.0"
---
# Kubernetes Deployment Patterns / Mẫu Triển Khai Kubernetes
Kubernetes deployment patterns cho GoodGo microservices production.
## When to Use This Skill / Khi Nào Sử Dụng
Use this skill when:
- Deploying services to Kubernetes / Triển khai services lên Kubernetes
- Creating Helm charts / Tạo Helm charts
- Configuring Ingress routing / Cấu hình Ingress routing
- Managing secrets and configs / Quản lý secrets và configs
- Setting up health probes / Cài đặt health probes
- Scaling applications / Scale ứng dụng
## Core Concepts / Khái Niệm Cốt Lõi
### Kubernetes Architecture / Kiến Trúc Kubernetes
```
┌─────────────────────────────────────────────────────────────┐
│ KUBERNETES CLUSTER │
├─────────────────────────────────────────────────────────────┤
│ ┌─────────────────────────────────────────────────────┐ │
│ │ INGRESS │ │
│ │ (NGINX / Traefik Controller) │ │
│ └──────────────────────┬──────────────────────────────┘ │
│ │ │
│ ┌──────────────────────▼──────────────────────────────┐ │
│ │ SERVICES │ │
│ │ ┌───────────┐ ┌───────────┐ ┌───────────┐ │ │
│ │ │ iam-svc │ │ order-svc │ │storage-svc│ │ │
│ │ └─────┬─────┘ └─────┬─────┘ └─────┬─────┘ │ │
│ └──────────┼──────────────┼──────────────┼────────────┘ │
│ │ │ │ │
│ ┌──────────▼──────────────▼──────────────▼────────────┐ │
│ │ PODS │ │
│ │ ┌─────────┐ ┌─────────┐ ┌─────────┐ │ │
│ │ │ Pod 1 │ │ Pod 2 │ │ Pod 3 │ │ │
│ │ │ replica │ │ replica │ │ replica │ │ │
│ │ └─────────┘ └─────────┘ └─────────┘ │ │
│ └─────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────┘
```
### Key Resources / Các Tài Nguyên Chính
| Resource | Purpose | Example |
|----------|---------|---------|
| **Pod** | Smallest deployable unit | Container(s) + volumes |
| **Deployment** | Manages ReplicaSets | Rolling updates |
| **Service** | Stable network endpoint | Load balancing |
| **Ingress** | HTTP routing | Host/path rules |
| **ConfigMap** | Non-sensitive config | App settings |
| **Secret** | Sensitive data | Passwords, keys |
### Health Probes / Các Loại Probe
| Probe | Purpose | Failure Action |
|-------|---------|----------------|
| **Liveness** | Is container alive? | Restart container |
| **Readiness** | Can accept traffic? | Remove from LB |
| **Startup** | Has started? | Block other probes |
## Key Patterns / Mẫu Chính
### Deployment Manifest
```yaml
# k8s/base/iam-service/deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: iam-service
labels:
app: iam-service
tier: backend
spec:
replicas: 3
selector:
matchLabels:
app: iam-service
template:
metadata:
labels:
app: iam-service
spec:
containers:
- name: iam-service
image: goodgo/iam-service:latest
ports:
- containerPort: 8080
env:
- name: ASPNETCORE_ENVIRONMENT
value: "Production"
- name: ConnectionStrings__DefaultConnection
valueFrom:
secretKeyRef:
name: iam-secrets
key: database-url
resources:
requests:
memory: "256Mi"
cpu: "100m"
limits:
memory: "512Mi"
cpu: "500m"
livenessProbe:
httpGet:
path: /health/live
port: 8080
initialDelaySeconds: 10
periodSeconds: 10
failureThreshold: 3
readinessProbe:
httpGet:
path: /health/ready
port: 8080
initialDelaySeconds: 5
periodSeconds: 5
failureThreshold: 3
startupProbe:
httpGet:
path: /health/startup
port: 8080
initialDelaySeconds: 5
periodSeconds: 5
failureThreshold: 30
```
### Service Manifest
```yaml
# k8s/base/iam-service/service.yaml
apiVersion: v1
kind: Service
metadata:
name: iam-service
labels:
app: iam-service
spec:
type: ClusterIP
ports:
- port: 80
targetPort: 8080
protocol: TCP
name: http
selector:
app: iam-service
```
### Ingress Configuration
```yaml
# k8s/base/ingress.yaml
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: goodgo-ingress
annotations:
nginx.ingress.kubernetes.io/rewrite-target: /
nginx.ingress.kubernetes.io/ssl-redirect: "true"
cert-manager.io/cluster-issuer: letsencrypt-prod
spec:
ingressClassName: nginx
tls:
- hosts:
- api.goodgo.vn
secretName: goodgo-tls
rules:
- host: api.goodgo.vn
http:
paths:
- path: /api/v1/iam
pathType: Prefix
backend:
service:
name: iam-service
port:
number: 80
- path: /api/v1/orders
pathType: Prefix
backend:
service:
name: order-service
port:
number: 80
- path: /api/v1/storage
pathType: Prefix
backend:
service:
name: storage-service
port:
number: 80
```
### ConfigMap & Secret
```yaml
# k8s/base/configmap.yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: app-config
data:
ASPNETCORE_ENVIRONMENT: "Production"
Logging__LogLevel__Default: "Information"
Redis__InstanceName: "GoodGo:"
---
# k8s/base/secret.yaml
apiVersion: v1
kind: Secret
metadata:
name: iam-secrets
type: Opaque
stringData:
database-url: "Host=postgres;Database=iam_db;Username=postgres;Password=secret"
jwt-secret: "your-super-secret-key-here"
```
### Helm Chart Structure
```
charts/
└── goodgo-service/
├── Chart.yaml
├── values.yaml
├── templates/
│ ├── deployment.yaml
│ ├── service.yaml
│ ├── ingress.yaml
│ ├── configmap.yaml
│ ├── secret.yaml
│ ├── hpa.yaml
│ └── _helpers.tpl
└── values/
├── development.yaml
├── staging.yaml
└── production.yaml
```
### Helm Values File
```yaml
# charts/goodgo-service/values.yaml
replicaCount: 3
image:
repository: goodgo/iam-service
tag: "latest"
pullPolicy: IfNotPresent
service:
type: ClusterIP
port: 80
targetPort: 8080
ingress:
enabled: true
className: nginx
annotations:
cert-manager.io/cluster-issuer: letsencrypt-prod
hosts:
- host: api.goodgo.vn
paths:
- path: /api/v1/iam
pathType: Prefix
tls:
- secretName: goodgo-tls
hosts:
- api.goodgo.vn
resources:
requests:
memory: "256Mi"
cpu: "100m"
limits:
memory: "512Mi"
cpu: "500m"
autoscaling:
enabled: true
minReplicas: 2
maxReplicas: 10
targetCPUUtilizationPercentage: 70
env:
- name: ASPNETCORE_ENVIRONMENT
value: "Production"
envFromSecret:
- name: ConnectionStrings__DefaultConnection
secretName: iam-secrets
secretKey: database-url
```
### HorizontalPodAutoscaler
```yaml
# k8s/base/hpa.yaml
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: iam-service-hpa
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: iam-service
minReplicas: 2
maxReplicas: 10
metrics:
- type: Resource
resource:
name: cpu
target:
type: Utilization
averageUtilization: 70
- type: Resource
resource:
name: memory
target:
type: Utilization
averageUtilization: 80
```
## Common Mistakes / Lỗi Thường Gặp
### 1. No Resource Limits
```yaml
# ❌ BAD: No limits
containers:
- name: app
image: myapp
# ✅ GOOD: With limits
containers:
- name: app
image: myapp
resources:
requests:
memory: "256Mi"
cpu: "100m"
limits:
memory: "512Mi"
cpu: "500m"
```
### 2. Missing Health Probes
```yaml
# ❌ BAD: No probes
containers:
- name: app
# ✅ GOOD: All probes configured
containers:
- name: app
livenessProbe:
httpGet:
path: /health/live
port: 8080
readinessProbe:
httpGet:
path: /health/ready
port: 8080
```
### 3. Hardcoded Secrets
```yaml
# ❌ BAD: Secret in env
env:
- name: DB_PASSWORD
value: "mysecretpassword"
# ✅ GOOD: From Secret
env:
- name: DB_PASSWORD
valueFrom:
secretKeyRef:
name: db-secrets
key: password
```
## Quick Reference / Tham Chiếu Nhanh
### kubectl Commands
```bash
# EN: Apply manifests / VI: Áp dụng manifests
kubectl apply -f k8s/base/
# EN: Check pod status / VI: Kiểm tra trạng thái pods
kubectl get pods -l app=iam-service
# EN: View logs / VI: Xem logs
kubectl logs -f deployment/iam-service
# EN: Scale deployment / VI: Scale deployment
kubectl scale deployment iam-service --replicas=5
# EN: Rollout status / VI: Trạng thái rollout
kubectl rollout status deployment/iam-service
# EN: Rollback / VI: Rollback
kubectl rollout undo deployment/iam-service
```
### Helm Commands
```bash
# EN: Install chart / VI: Cài đặt chart
helm install iam-service ./charts/goodgo-service -f values/production.yaml
# EN: Upgrade / VI: Nâng cấp
helm upgrade iam-service ./charts/goodgo-service -f values/production.yaml
# EN: Rollback / VI: Rollback
helm rollback iam-service 1
# EN: List releases / VI: Liệt kê releases
helm list -A
```
## Resources / Tài Nguyên
- [Detailed Examples](./references/REFERENCE.md) - Full configurations
- [Docker Traefik](../docker-traefik/SKILL.md) - Container basics
- [Observability](../observability/SKILL.md) - Health checks
- [Error Handling](../error-handling-patterns/SKILL.md) - Probes setup

View File

@@ -0,0 +1,446 @@
# Kubernetes Deployment - Detailed Reference
Detailed configurations và examples cho Kubernetes deployment trong GoodGo.
## Table of Contents
1. [Complete Deployment Example](#complete-deployment-example)
2. [Helm Chart Templates](#helm-chart-templates)
3. [Kustomize Configuration](#kustomize-configuration)
4. [CI/CD Integration](#cicd-integration)
5. [Production Configurations](#production-configurations)
---
## Complete Deployment Example
### Namespace
```yaml
# k8s/base/namespace.yaml
apiVersion: v1
kind: Namespace
metadata:
name: goodgo
labels:
name: goodgo
environment: production
```
### Complete Service Deployment
```yaml
# k8s/base/iam-service/deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: iam-service
namespace: goodgo
labels:
app.kubernetes.io/name: iam-service
app.kubernetes.io/part-of: goodgo
app.kubernetes.io/version: "1.0.0"
spec:
replicas: 3
revisionHistoryLimit: 5
strategy:
type: RollingUpdate
rollingUpdate:
maxSurge: 1
maxUnavailable: 0
selector:
matchLabels:
app.kubernetes.io/name: iam-service
template:
metadata:
labels:
app.kubernetes.io/name: iam-service
annotations:
prometheus.io/scrape: "true"
prometheus.io/port: "8080"
prometheus.io/path: "/metrics"
spec:
serviceAccountName: iam-service
securityContext:
runAsNonRoot: true
runAsUser: 1000
fsGroup: 1000
containers:
- name: iam-service
image: goodgo/iam-service:1.0.0
imagePullPolicy: IfNotPresent
securityContext:
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
capabilities:
drop:
- ALL
ports:
- name: http
containerPort: 8080
protocol: TCP
envFrom:
- configMapRef:
name: iam-config
env:
- name: ConnectionStrings__DefaultConnection
valueFrom:
secretKeyRef:
name: iam-secrets
key: database-url
- name: Jwt__SecretKey
valueFrom:
secretKeyRef:
name: iam-secrets
key: jwt-secret
resources:
requests:
memory: "256Mi"
cpu: "100m"
limits:
memory: "512Mi"
cpu: "500m"
livenessProbe:
httpGet:
path: /health/live
port: http
initialDelaySeconds: 10
periodSeconds: 10
timeoutSeconds: 5
failureThreshold: 3
readinessProbe:
httpGet:
path: /health/ready
port: http
initialDelaySeconds: 5
periodSeconds: 5
timeoutSeconds: 3
failureThreshold: 3
startupProbe:
httpGet:
path: /health/startup
port: http
initialDelaySeconds: 5
periodSeconds: 5
failureThreshold: 30
volumeMounts:
- name: tmp
mountPath: /tmp
volumes:
- name: tmp
emptyDir: {}
affinity:
podAntiAffinity:
preferredDuringSchedulingIgnoredDuringExecution:
- weight: 100
podAffinityTerm:
labelSelector:
matchLabels:
app.kubernetes.io/name: iam-service
topologyKey: kubernetes.io/hostname
topologySpreadConstraints:
- maxSkew: 1
topologyKey: topology.kubernetes.io/zone
whenUnsatisfiable: ScheduleAnyway
labelSelector:
matchLabels:
app.kubernetes.io/name: iam-service
```
### Service Account
```yaml
# k8s/base/iam-service/serviceaccount.yaml
apiVersion: v1
kind: ServiceAccount
metadata:
name: iam-service
namespace: goodgo
---
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
name: iam-service-role
namespace: goodgo
rules:
- apiGroups: [""]
resources: ["configmaps", "secrets"]
verbs: ["get", "list", "watch"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: iam-service-rolebinding
namespace: goodgo
subjects:
- kind: ServiceAccount
name: iam-service
roleRef:
kind: Role
name: iam-service-role
apiGroup: rbac.authorization.k8s.io
```
### PodDisruptionBudget
```yaml
# k8s/base/iam-service/pdb.yaml
apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
name: iam-service-pdb
namespace: goodgo
spec:
minAvailable: 2
selector:
matchLabels:
app.kubernetes.io/name: iam-service
```
---
## Helm Chart Templates
### Chart.yaml
```yaml
# charts/goodgo-service/Chart.yaml
apiVersion: v2
name: goodgo-service
description: A Helm chart for GoodGo microservices
type: application
version: 1.0.0
appVersion: "1.0.0"
maintainers:
- name: GoodGo Team
email: team@goodgo.vn
```
### Deployment Template
```yaml
# charts/goodgo-service/templates/deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: {{ include "goodgo-service.fullname" . }}
labels:
{{- include "goodgo-service.labels" . | nindent 4 }}
spec:
{{- if not .Values.autoscaling.enabled }}
replicas: {{ .Values.replicaCount }}
{{- end }}
selector:
matchLabels:
{{- include "goodgo-service.selectorLabels" . | nindent 6 }}
template:
metadata:
annotations:
checksum/config: {{ include (print $.Template.BasePath "/configmap.yaml") . | sha256sum }}
{{- with .Values.podAnnotations }}
{{- toYaml . | nindent 8 }}
{{- end }}
labels:
{{- include "goodgo-service.selectorLabels" . | nindent 8 }}
spec:
serviceAccountName: {{ include "goodgo-service.serviceAccountName" . }}
securityContext:
{{- toYaml .Values.podSecurityContext | nindent 8 }}
containers:
- name: {{ .Chart.Name }}
securityContext:
{{- toYaml .Values.securityContext | nindent 12 }}
image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}"
imagePullPolicy: {{ .Values.image.pullPolicy }}
ports:
- name: http
containerPort: {{ .Values.service.targetPort }}
protocol: TCP
{{- if .Values.env }}
env:
{{- toYaml .Values.env | nindent 12 }}
{{- end }}
{{- if .Values.envFromSecret }}
envFrom:
- secretRef:
name: {{ include "goodgo-service.fullname" . }}-secrets
{{- end }}
livenessProbe:
httpGet:
path: {{ .Values.probes.liveness.path }}
port: http
initialDelaySeconds: {{ .Values.probes.liveness.initialDelaySeconds }}
periodSeconds: {{ .Values.probes.liveness.periodSeconds }}
readinessProbe:
httpGet:
path: {{ .Values.probes.readiness.path }}
port: http
initialDelaySeconds: {{ .Values.probes.readiness.initialDelaySeconds }}
periodSeconds: {{ .Values.probes.readiness.periodSeconds }}
resources:
{{- toYaml .Values.resources | nindent 12 }}
```
### Helpers Template
```yaml
# charts/goodgo-service/templates/_helpers.tpl
{{/*
Expand the name of the chart.
*/}}
{{- define "goodgo-service.name" -}}
{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }}
{{- end }}
{{/*
Create a default fully qualified app name.
*/}}
{{- define "goodgo-service.fullname" -}}
{{- if .Values.fullnameOverride }}
{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }}
{{- else }}
{{- $name := default .Chart.Name .Values.nameOverride }}
{{- if contains $name .Release.Name }}
{{- .Release.Name | trunc 63 | trimSuffix "-" }}
{{- else }}
{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }}
{{- end }}
{{- end }}
{{- end }}
{{/*
Common labels
*/}}
{{- define "goodgo-service.labels" -}}
helm.sh/chart: {{ include "goodgo-service.chart" . }}
{{ include "goodgo-service.selectorLabels" . }}
app.kubernetes.io/version: {{ .Values.image.tag | default .Chart.AppVersion | quote }}
app.kubernetes.io/managed-by: {{ .Release.Service }}
{{- end }}
{{/*
Selector labels
*/}}
{{- define "goodgo-service.selectorLabels" -}}
app.kubernetes.io/name: {{ include "goodgo-service.name" . }}
app.kubernetes.io/instance: {{ .Release.Name }}
{{- end }}
```
### Production Values
```yaml
# charts/goodgo-service/values/production.yaml
replicaCount: 3
image:
repository: gcr.io/goodgo/iam-service
tag: "1.0.0"
pullPolicy: IfNotPresent
resources:
requests:
memory: "512Mi"
cpu: "250m"
limits:
memory: "1Gi"
cpu: "1000m"
autoscaling:
enabled: true
minReplicas: 3
maxReplicas: 20
targetCPUUtilizationPercentage: 70
targetMemoryUtilizationPercentage: 80
ingress:
enabled: true
className: nginx
annotations:
nginx.ingress.kubernetes.io/rate-limit: "100"
nginx.ingress.kubernetes.io/rate-limit-window: "1m"
hosts:
- host: api.goodgo.vn
paths:
- path: /api/v1/iam
pathType: Prefix
tls:
- secretName: goodgo-tls
hosts:
- api.goodgo.vn
probes:
liveness:
path: /health/live
initialDelaySeconds: 10
periodSeconds: 10
readiness:
path: /health/ready
initialDelaySeconds: 5
periodSeconds: 5
```
---
## CI/CD Integration
### GitHub Actions Deploy
```yaml
# .github/workflows/deploy.yml
name: Deploy to Kubernetes
on:
push:
branches: [main]
tags: ['v*']
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Login to GCR
uses: docker/login-action@v3
with:
registry: gcr.io
username: _json_key
password: ${{ secrets.GCP_SA_KEY }}
- name: Build and push
uses: docker/build-push-action@v5
with:
context: ./services/iam-service-net
push: true
tags: gcr.io/goodgo/iam-service:${{ github.sha }}
cache-from: type=gha
cache-to: type=gha,mode=max
- name: Set up kubectl
uses: azure/setup-kubectl@v3
- name: Set up Helm
uses: azure/setup-helm@v3
- name: Deploy to Kubernetes
run: |
helm upgrade --install iam-service ./charts/goodgo-service \
--namespace goodgo \
--create-namespace \
--values ./charts/goodgo-service/values/production.yaml \
--set image.tag=${{ github.sha }} \
--wait
```
---
## Resources / Tài Nguyên
- [Kubernetes Documentation](https://kubernetes.io/docs/)
- [Helm Documentation](https://helm.sh/docs/)
- [Kustomize Documentation](https://kustomize.io/)
- [NGINX Ingress Controller](https://kubernetes.github.io/ingress-nginx/)

View File

@@ -0,0 +1,480 @@
---
name: domain-driven-design
description: DDD patterns cho complex business logic. Use for Aggregates, Value Objects, Entities, Domain Events, và Rich Domain Model.
compatibility: ".NET 8+, EF Core 8+"
metadata:
author: Velik Ho
version: "1.0"
---
# Domain-Driven Design Patterns / Mẫu DDD
DDD patterns cho GoodGo microservices với complex business logic.
## When to Use This Skill / Khi Nào Sử Dụng
Use this skill when:
- Modeling complex business domains / Mô hình hóa domain phức tạp
- Designing aggregates and entities / Thiết kế aggregates và entities
- Implementing business rules in domain / Triển khai business rules trong domain
- Creating value objects / Tạo value objects
- Raising domain events / Raise domain events
## Core Concepts / Khái Niệm Cốt Lõi
### DDD Building Blocks / Các Khối Xây Dựng DDD
```
┌─────────────────────────────────────────────────────────────┐
│ DOMAIN MODEL LAYER │
├─────────────────────────────────────────────────────────────┤
│ ┌─────────────────────────────────────────────────────┐ │
│ │ AGGREGATE ROOT │ │
│ │ ┌─────────────────┐ ┌─────────────────┐ │ │
│ │ │ Entity │ │ Value Object │ │ │
│ │ │ (Identity) │ │ (No Identity) │ │ │
│ │ └─────────────────┘ └─────────────────┘ │ │
│ │ │ │
│ │ • Business Rules / Quy tắc nghiệp vụ │ │
│ │ • Domain Events / Domain Events │ │
│ │ • Invariants / Bất biến │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ ┌─────────────────┐ ┌─────────────────┐ │
│ │ Domain Service │ │ Domain Events │ │
│ └─────────────────┘ └─────────────────┘ │
└─────────────────────────────────────────────────────────────┘
```
### Entity vs Value Object / Entity vs Value Object
| Aspect | Entity | Value Object |
|--------|--------|--------------|
| **Identity** | Has unique ID | No identity |
| **Equality** | By ID | By all properties |
| **Mutability** | Mutable (via methods) | Immutable |
| **Lifecycle** | Independent | Belongs to Entity |
| **Example** | Order, User | Address, Money |
### Aggregate Rules / Quy Tắc Aggregate
1. **One repository per aggregate root** / Một repository cho mỗi aggregate root
2. **Reference only by ID** / Chỉ tham chiếu qua ID
3. **Atomic transaction boundary** / Ranh giới transaction atomic
4. **Consistency within aggregate** / Nhất quán trong aggregate
## Key Patterns / Mẫu Chính
### Entity Base Class
```csharp
/// <summary>
/// EN: Base class for all entities.
/// VI: Base class cho tất cả entities.
/// </summary>
public abstract class Entity
{
private int? _requestedHashCode;
private List<IDomainEvent>? _domainEvents;
public virtual Guid Id { get; protected set; }
public IReadOnlyCollection<IDomainEvent> DomainEvents
=> _domainEvents?.AsReadOnly() ?? Array.Empty<IDomainEvent>().AsReadOnly();
public void AddDomainEvent(IDomainEvent eventItem)
{
_domainEvents ??= new List<IDomainEvent>();
_domainEvents.Add(eventItem);
}
public void RemoveDomainEvent(IDomainEvent eventItem)
{
_domainEvents?.Remove(eventItem);
}
public void ClearDomainEvents()
{
_domainEvents?.Clear();
}
public bool IsTransient()
{
return Id == default;
}
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;
if (IsTransient() || other.IsTransient())
return false;
return Id.Equals(other.Id);
}
public override int GetHashCode()
{
if (!IsTransient())
{
_requestedHashCode ??= Id.GetHashCode() ^ 31;
return _requestedHashCode.Value;
}
return base.GetHashCode();
}
public static bool operator ==(Entity? left, Entity? right)
{
return left?.Equals(right) ?? right is null;
}
public static bool operator !=(Entity? left, Entity? right)
{
return !(left == right);
}
}
```
### Aggregate Root
```csharp
/// <summary>
/// EN: Marker interface for aggregate roots.
/// VI: Interface đánh dấu aggregate roots.
/// </summary>
public interface IAggregateRoot { }
/// <summary>
/// EN: Order aggregate root with business rules.
/// VI: Order aggregate root với business rules.
/// </summary>
public class Order : Entity, IAggregateRoot
{
private readonly List<OrderItem> _orderItems = new();
public string UserId { get; private set; }
public Address ShippingAddress { get; private set; }
public OrderStatus Status { get; private set; }
public decimal TotalAmount { get; private set; }
public DateTime CreatedAt { get; private set; }
public DateTime? SubmittedAt { get; private set; }
public IReadOnlyCollection<OrderItem> OrderItems => _orderItems.AsReadOnly();
// EN: Required by EF Core
private Order() { }
public Order(string userId, Address shippingAddress)
{
Id = Guid.NewGuid();
UserId = userId ?? throw new ArgumentNullException(nameof(userId));
ShippingAddress = shippingAddress ?? throw new ArgumentNullException(nameof(shippingAddress));
Status = OrderStatus.Draft;
CreatedAt = DateTime.UtcNow;
TotalAmount = 0;
// EN: Raise domain event
// VI: Raise domain event
AddDomainEvent(new OrderCreatedDomainEvent(Id, userId));
}
public void AddItem(Guid productId, int quantity, decimal unitPrice)
{
// EN: Business rule: Can only add items to draft orders
// VI: Quy tắc: Chỉ thêm items vào orders draft
if (Status != OrderStatus.Draft)
throw new DomainException("Cannot add items to non-draft order");
if (quantity <= 0)
throw new ArgumentException("Quantity must be positive", nameof(quantity));
if (unitPrice < 0)
throw new ArgumentException("Price cannot be negative", nameof(unitPrice));
var existingItem = _orderItems.FirstOrDefault(i => i.ProductId == productId);
if (existingItem != null)
{
existingItem.IncreaseQuantity(quantity);
}
else
{
_orderItems.Add(new OrderItem(productId, quantity, unitPrice));
}
RecalculateTotal();
}
public void RemoveItem(Guid productId)
{
if (Status != OrderStatus.Draft)
throw new DomainException("Cannot remove items from non-draft order");
var item = _orderItems.FirstOrDefault(i => i.ProductId == productId);
if (item != null)
{
_orderItems.Remove(item);
RecalculateTotal();
}
}
public void Submit()
{
// EN: Business rule: Cannot submit empty order
// VI: Quy tắc: Không thể submit order trống
if (!_orderItems.Any())
throw new DomainException("Cannot submit empty order");
if (Status != OrderStatus.Draft)
throw new DomainException($"Cannot submit order in {Status} status");
Status = OrderStatus.Submitted;
SubmittedAt = DateTime.UtcNow;
AddDomainEvent(new OrderSubmittedDomainEvent(Id, UserId, TotalAmount));
}
public void Cancel(string reason)
{
if (Status == OrderStatus.Shipped || Status == OrderStatus.Delivered)
throw new DomainException("Cannot cancel shipped or delivered order");
Status = OrderStatus.Cancelled;
AddDomainEvent(new OrderCancelledDomainEvent(Id, reason));
}
private void RecalculateTotal()
{
TotalAmount = _orderItems.Sum(i => i.Quantity * i.UnitPrice);
}
}
```
### Value Object
```csharp
/// <summary>
/// EN: Base class for value objects.
/// VI: Base class cho value objects.
/// </summary>
public abstract class ValueObject
{
protected abstract IEnumerable<object?> GetEqualityComponents();
public override bool Equals(object? obj)
{
if (obj is null || obj.GetType() != GetType())
return false;
var other = (ValueObject)obj;
return GetEqualityComponents().SequenceEqual(other.GetEqualityComponents());
}
public override int GetHashCode()
{
return GetEqualityComponents()
.Select(x => x?.GetHashCode() ?? 0)
.Aggregate((x, y) => x ^ y);
}
public static bool operator ==(ValueObject? left, ValueObject? right)
{
return left?.Equals(right) ?? right is null;
}
public static bool operator !=(ValueObject? left, ValueObject? right)
{
return !(left == right);
}
}
/// <summary>
/// EN: Address value object.
/// VI: Value object địa chỉ.
/// </summary>
public class Address : ValueObject
{
public string Street { get; }
public string City { get; }
public string State { get; }
public string PostalCode { get; }
public string Country { get; }
public Address(string street, string city, string state, string postalCode, string country)
{
Street = street ?? throw new ArgumentNullException(nameof(street));
City = city ?? throw new ArgumentNullException(nameof(city));
State = state ?? throw new ArgumentNullException(nameof(state));
PostalCode = postalCode ?? throw new ArgumentNullException(nameof(postalCode));
Country = country ?? throw new ArgumentNullException(nameof(country));
}
protected override IEnumerable<object?> GetEqualityComponents()
{
yield return Street;
yield return City;
yield return State;
yield return PostalCode;
yield return Country;
}
}
/// <summary>
/// EN: Money value object with currency.
/// VI: Value object tiền tệ.
/// </summary>
public class Money : ValueObject
{
public decimal Amount { get; }
public string Currency { get; }
public Money(decimal amount, string currency)
{
if (amount < 0)
throw new ArgumentException("Amount cannot be negative");
Amount = amount;
Currency = currency?.ToUpperInvariant()
?? throw new ArgumentNullException(nameof(currency));
}
public Money Add(Money other)
{
if (Currency != other.Currency)
throw new InvalidOperationException("Cannot add different currencies");
return new Money(Amount + other.Amount, Currency);
}
protected override IEnumerable<object?> GetEqualityComponents()
{
yield return Amount;
yield return Currency;
}
}
```
### Domain Events
```csharp
/// <summary>
/// EN: Domain event interface.
/// VI: Interface domain event.
/// </summary>
public interface IDomainEvent : INotification
{
Guid Id { get; }
DateTime OccurredOn { get; }
}
/// <summary>
/// EN: Domain event when order is submitted.
/// VI: Domain event khi order được submit.
/// </summary>
public record OrderSubmittedDomainEvent : IDomainEvent
{
public Guid Id { get; } = Guid.NewGuid();
public DateTime OccurredOn { get; } = DateTime.UtcNow;
public Guid OrderId { get; init; }
public string UserId { get; init; }
public decimal TotalAmount { get; init; }
public OrderSubmittedDomainEvent(Guid orderId, string userId, decimal totalAmount)
{
OrderId = orderId;
UserId = userId;
TotalAmount = totalAmount;
}
}
```
## Common Mistakes / Lỗi Thường Gặp
### 1. Anemic Domain Model
```csharp
// ❌ BAD: Anemic model with no behavior
public class Order
{
public Guid Id { get; set; }
public string Status { get; set; }
public List<OrderItem> Items { get; set; }
}
// ✅ GOOD: Rich domain model with behavior
public class Order
{
private readonly List<OrderItem> _items = new();
public OrderStatus Status { get; private set; }
public void AddItem(Guid productId, int quantity, decimal price)
{
if (Status != OrderStatus.Draft)
throw new DomainException("Cannot modify non-draft order");
// ...
}
}
```
### 2. Direct Property Modification
```csharp
// ❌ BAD: Direct modification bypasses rules
order.Status = OrderStatus.Submitted;
// ✅ GOOD: Use domain methods
order.Submit();
```
### 3. Logic in Application Layer
```csharp
// ❌ BAD: Business logic in handler
public async Task Handle(CreateOrderCommand cmd)
{
if (cmd.Items.Count == 0)
throw new Exception("Empty order");
// ...
}
// ✅ GOOD: Business logic in domain
public void Submit()
{
if (!_orderItems.Any())
throw new DomainException("Cannot submit empty order");
}
```
## Quick Reference / Tham Chiếu Nhanh
### Aggregate Design Guidelines
| Guideline | Description |
|-----------|-------------|
| Small aggregates | Keep aggregates focused |
| Reference by ID | 외부 aggregates chỉ tham chiếu qua ID |
| Eventual consistency | Between aggregates |
| Immediate consistency | Within aggregate |
### When to Use Each Pattern
| Pattern | Use When |
|---------|----------|
| Entity | Has identity, lifecycle |
| Value Object | No identity, immutable |
| Aggregate | Group of related entities |
| Domain Event | Side effects needed |
| Domain Service | Logic doesn't fit entity |
## Resources / Tài Nguyên
- [Detailed Examples](./references/REFERENCE.md) - Full code examples
- [Repository Pattern](../repository-pattern/SKILL.md) - Data access
- [CQRS MediatR](../cqrs-mediatr/SKILL.md) - Command handlers
- [Testing Patterns](../testing-patterns/SKILL.md) - Domain testing

View File

@@ -0,0 +1,530 @@
# Domain-Driven Design - Detailed Reference
Detailed code examples cho DDD patterns trong GoodGo.
## Table of Contents
1. [Entity Patterns](#entity-patterns)
2. [Value Objects](#value-objects)
3. [Aggregates](#aggregates)
4. [Domain Events](#domain-events)
5. [Domain Services](#domain-services)
6. [Specifications](#specifications)
---
## Entity Patterns
### Complete Entity Base Class
```csharp
/// <summary>
/// EN: Base Entity class with domain events support.
/// VI: Base Entity class với hỗ trợ domain events.
/// </summary>
public abstract class Entity : IEquatable<Entity>
{
private readonly List<IDomainEvent> _domainEvents = new();
private int? _requestedHashCode;
public virtual Guid Id { get; protected set; }
public IReadOnlyCollection<IDomainEvent> DomainEvents => _domainEvents.AsReadOnly();
protected void AddDomainEvent(IDomainEvent domainEvent)
{
_domainEvents.Add(domainEvent);
}
public void ClearDomainEvents() => _domainEvents.Clear();
public bool IsTransient() => Id == default;
public bool Equals(Entity? other)
{
if (other is null) return false;
if (ReferenceEquals(this, other)) return true;
if (GetType() != other.GetType()) return false;
if (IsTransient() || other.IsTransient()) return false;
return Id.Equals(other.Id);
}
public override bool Equals(object? obj) => Equals(obj as Entity);
public override int GetHashCode()
{
if (IsTransient()) return base.GetHashCode();
_requestedHashCode ??= Id.GetHashCode() ^ 31;
return _requestedHashCode.Value;
}
public static bool operator ==(Entity? left, Entity? right) => Equals(left, right);
public static bool operator !=(Entity? left, Entity? right) => !Equals(left, right);
}
```
### Child Entity (OrderItem)
```csharp
/// <summary>
/// EN: OrderItem entity belonging to Order aggregate.
/// VI: Entity OrderItem thuộc Order aggregate.
/// </summary>
public class OrderItem : Entity
{
public Guid ProductId { get; private set; }
public string ProductName { get; private set; }
public int Quantity { get; private set; }
public decimal UnitPrice { get; private set; }
public decimal TotalPrice => Quantity * UnitPrice;
// EN: Required by EF Core
private OrderItem() { }
internal OrderItem(Guid productId, string productName, int quantity, decimal unitPrice)
{
Id = Guid.NewGuid();
ProductId = productId;
ProductName = productName ?? throw new ArgumentNullException(nameof(productName));
SetQuantity(quantity);
UnitPrice = unitPrice >= 0 ? unitPrice
: throw new ArgumentException("Price cannot be negative");
}
internal void SetQuantity(int quantity)
{
if (quantity <= 0)
throw new DomainException("Quantity must be positive");
Quantity = quantity;
}
internal void IncreaseQuantity(int amount)
{
if (amount <= 0)
throw new ArgumentException("Amount must be positive");
Quantity += amount;
}
}
```
---
## Value Objects
### Value Object Base
```csharp
/// <summary>
/// EN: Base class for value objects with equality support.
/// VI: Base class cho value objects với hỗ trợ equality.
/// </summary>
public abstract class ValueObject : IEquatable<ValueObject>
{
protected abstract IEnumerable<object?> GetEqualityComponents();
public bool Equals(ValueObject? other)
{
if (other is null) return false;
if (ReferenceEquals(this, other)) return true;
if (GetType() != other.GetType()) return false;
return GetEqualityComponents().SequenceEqual(other.GetEqualityComponents());
}
public override bool Equals(object? obj) => Equals(obj as ValueObject);
public override int GetHashCode()
{
return GetEqualityComponents()
.Aggregate(0, (hash, component) =>
HashCode.Combine(hash, component?.GetHashCode() ?? 0));
}
public static bool operator ==(ValueObject? left, ValueObject? right)
=> Equals(left, right);
public static bool operator !=(ValueObject? left, ValueObject? right)
=> !Equals(left, right);
}
```
### Complex Value Objects
```csharp
/// <summary>
/// EN: Email value object with validation.
/// VI: Value object email với validation.
/// </summary>
public sealed class Email : ValueObject
{
public string Value { get; }
private Email(string value) => Value = value;
public static Email Create(string email)
{
if (string.IsNullOrWhiteSpace(email))
throw new DomainException("Email cannot be empty");
if (!IsValidEmail(email))
throw new DomainException("Invalid email format");
return new Email(email.ToLowerInvariant());
}
private static bool IsValidEmail(string email)
{
try
{
var addr = new System.Net.Mail.MailAddress(email);
return addr.Address == email;
}
catch
{
return false;
}
}
protected override IEnumerable<object?> GetEqualityComponents()
{
yield return Value;
}
public override string ToString() => Value;
public static implicit operator string(Email email) => email.Value;
}
/// <summary>
/// EN: DateRange value object.
/// VI: Value object khoảng thời gian.
/// </summary>
public sealed class DateRange : ValueObject
{
public DateTime Start { get; }
public DateTime End { get; }
public TimeSpan Duration => End - Start;
private DateRange(DateTime start, DateTime end)
{
Start = start;
End = end;
}
public static DateRange Create(DateTime start, DateTime end)
{
if (end < start)
throw new DomainException("End date must be after start date");
return new DateRange(start, end);
}
public bool Contains(DateTime date) => date >= Start && date <= End;
public bool Overlaps(DateRange other)
=> Start < other.End && End > other.Start;
protected override IEnumerable<object?> GetEqualityComponents()
{
yield return Start;
yield return End;
}
}
/// <summary>
/// EN: Money value object with currency operations.
/// VI: Value object tiền tệ với các phép toán.
/// </summary>
public sealed class Money : ValueObject
{
public decimal Amount { get; }
public Currency Currency { get; }
private Money(decimal amount, Currency currency)
{
Amount = amount;
Currency = currency;
}
public static Money Create(decimal amount, Currency currency)
{
if (amount < 0)
throw new DomainException("Amount cannot be negative");
return new Money(amount, currency);
}
public static Money Zero(Currency currency) => new(0, currency);
public Money Add(Money other)
{
EnsureSameCurrency(other);
return new Money(Amount + other.Amount, Currency);
}
public Money Subtract(Money other)
{
EnsureSameCurrency(other);
var result = Amount - other.Amount;
if (result < 0)
throw new DomainException("Insufficient funds");
return new Money(result, Currency);
}
public Money Multiply(decimal factor)
{
if (factor < 0)
throw new ArgumentException("Factor cannot be negative");
return new Money(Amount * factor, Currency);
}
private void EnsureSameCurrency(Money other)
{
if (Currency != other.Currency)
throw new DomainException("Cannot perform operation on different currencies");
}
protected override IEnumerable<object?> GetEqualityComponents()
{
yield return Amount;
yield return Currency;
}
public override string ToString() => $"{Amount:N2} {Currency}";
}
public enum Currency { VND, USD, EUR }
```
---
## Aggregates
### Complete Aggregate Example
```csharp
/// <summary>
/// EN: Order aggregate with full business logic.
/// VI: Order aggregate với business logic đầy đủ.
/// </summary>
public class Order : Entity, IAggregateRoot
{
private readonly List<OrderItem> _items = new();
public string UserId { get; private set; } = default!;
public Address ShippingAddress { get; private set; } = default!;
public Address? BillingAddress { get; private set; }
public OrderStatus Status { get; private set; }
public Money TotalAmount { get; private set; } = Money.Zero(Currency.VND);
public string? CancellationReason { get; private set; }
public DateTime CreatedAt { get; private set; }
public DateTime? SubmittedAt { get; private set; }
public DateTime? ShippedAt { get; private set; }
public DateTime? DeliveredAt { get; private set; }
public IReadOnlyCollection<OrderItem> Items => _items.AsReadOnly();
private Order() { } // EF Core
public Order(string userId, Address shippingAddress, Address? billingAddress = null)
{
Id = Guid.NewGuid();
UserId = userId ?? throw new ArgumentNullException(nameof(userId));
ShippingAddress = shippingAddress ?? throw new ArgumentNullException(nameof(shippingAddress));
BillingAddress = billingAddress;
Status = OrderStatus.Draft;
CreatedAt = DateTime.UtcNow;
AddDomainEvent(new OrderCreatedDomainEvent(Id, userId));
}
// EN: Command methods / VI: Các method command
public void AddItem(Guid productId, string productName, int quantity, decimal unitPrice)
{
EnsureOrderIsDraft();
var existingItem = _items.FirstOrDefault(i => i.ProductId == productId);
if (existingItem != null)
{
existingItem.IncreaseQuantity(quantity);
}
else
{
var item = new OrderItem(productId, productName, quantity, unitPrice);
_items.Add(item);
}
RecalculateTotal();
AddDomainEvent(new OrderItemAddedDomainEvent(Id, productId, quantity));
}
public void UpdateItemQuantity(Guid productId, int newQuantity)
{
EnsureOrderIsDraft();
var item = _items.FirstOrDefault(i => i.ProductId == productId)
?? throw new DomainException($"Item {productId} not found in order");
if (newQuantity <= 0)
{
_items.Remove(item);
}
else
{
item.SetQuantity(newQuantity);
}
RecalculateTotal();
}
public void UpdateShippingAddress(Address newAddress)
{
if (Status == OrderStatus.Shipped || Status == OrderStatus.Delivered)
throw new DomainException("Cannot change address for shipped order");
ShippingAddress = newAddress ?? throw new ArgumentNullException(nameof(newAddress));
}
public void Submit()
{
EnsureOrderIsDraft();
if (!_items.Any())
throw new DomainException("Cannot submit empty order");
Status = OrderStatus.Submitted;
SubmittedAt = DateTime.UtcNow;
AddDomainEvent(new OrderSubmittedDomainEvent(Id, UserId, TotalAmount.Amount));
}
public void ConfirmPayment()
{
if (Status != OrderStatus.Submitted)
throw new DomainException($"Cannot confirm payment for order in {Status} status");
Status = OrderStatus.Paid;
AddDomainEvent(new OrderPaidDomainEvent(Id, TotalAmount.Amount));
}
public void Ship(string trackingNumber)
{
if (Status != OrderStatus.Paid)
throw new DomainException("Order must be paid before shipping");
Status = OrderStatus.Shipped;
ShippedAt = DateTime.UtcNow;
AddDomainEvent(new OrderShippedDomainEvent(Id, trackingNumber));
}
public void Cancel(string reason)
{
if (Status == OrderStatus.Shipped || Status == OrderStatus.Delivered)
throw new DomainException("Cannot cancel shipped order");
Status = OrderStatus.Cancelled;
CancellationReason = reason;
AddDomainEvent(new OrderCancelledDomainEvent(Id, reason));
}
// EN: Private helpers / VI: Helpers private
private void EnsureOrderIsDraft()
{
if (Status != OrderStatus.Draft)
throw new DomainException("Order is not in draft status");
}
private void RecalculateTotal()
{
var total = _items.Sum(i => i.TotalPrice);
TotalAmount = Money.Create(total, Currency.VND);
}
}
public enum OrderStatus
{
Draft,
Submitted,
Paid,
Shipped,
Delivered,
Cancelled
}
```
---
## Domain Events
### Domain Event Definitions
```csharp
/// <summary>
/// EN: Domain events for Order aggregate.
/// VI: Domain events cho Order aggregate.
/// </summary>
public record OrderCreatedDomainEvent(Guid OrderId, string UserId) : IDomainEvent
{
public Guid Id { get; } = Guid.NewGuid();
public DateTime OccurredOn { get; } = DateTime.UtcNow;
}
public record OrderItemAddedDomainEvent(Guid OrderId, Guid ProductId, int Quantity) : IDomainEvent
{
public Guid Id { get; } = Guid.NewGuid();
public DateTime OccurredOn { get; } = DateTime.UtcNow;
}
public record OrderSubmittedDomainEvent(Guid OrderId, string UserId, decimal TotalAmount) : IDomainEvent
{
public Guid Id { get; } = Guid.NewGuid();
public DateTime OccurredOn { get; } = DateTime.UtcNow;
}
```
### Domain Event Dispatcher
```csharp
/// <summary>
/// EN: Dispatch domain events via MediatR.
/// VI: Dispatch domain events qua MediatR.
/// </summary>
public class DomainEventDispatcher : IDomainEventDispatcher
{
private readonly IMediator _mediator;
private readonly ILogger<DomainEventDispatcher> _logger;
public async Task DispatchEventsAsync(IEnumerable<Entity> entities, CancellationToken ct = default)
{
var domainEvents = entities
.SelectMany(e => e.DomainEvents)
.ToList();
foreach (var domainEvent in domainEvents)
{
_logger.LogDebug(
"Dispatching domain event {EventType}: {EventId}",
domainEvent.GetType().Name,
domainEvent.Id);
await _mediator.Publish(domainEvent, ct);
}
foreach (var entity in entities)
{
entity.ClearDomainEvents();
}
}
}
```
---
## Resources / Tài Nguyên
- [Domain-Driven Design - Eric Evans](https://www.domainlanguage.com/)
- [Implementing DDD - Vaughn Vernon](https://www.informit.com/store/implementing-domain-driven-design-9780321834577)
- [Microsoft DDD Guidance](https://docs.microsoft.com/en-us/dotnet/architecture/microservices/microservice-ddd-cqrs-patterns/)

View File

@@ -0,0 +1,373 @@
---
name: inter-service-communication
description: Giao tiếp liên dịch vụ. Use for Event Bus (RabbitMQ), Integration Events, HTTP Client với Polly, và gRPC patterns.
compatibility: ".NET 8+, MassTransit, RabbitMQ, Polly, gRPC"
metadata:
author: Velik Ho
version: "1.0"
---
# Inter-Service Communication / Giao Tiếp Liên Dịch Vụ
Patterns giao tiếp giữa các microservices trong GoodGo.
## When to Use This Skill / Khi Nào Sử Dụng
Use this skill when:
- Publishing domain events to other services / Publish domain events đến services khác
- Consuming integration events / Consume integration events
- Making HTTP calls to other services / Gọi HTTP đến services khác
- Implementing resilient communication / Triển khai giao tiếp có khả năng phục hồi
## Core Concepts / Khái Niệm Cốt Lõi
### Communication Patterns / Các Mẫu Giao Tiếp
```
┌─────────────────────────────────────────────────────────────┐
│ COMMUNICATION PATTERNS │
├───────────────────────────────┬─────────────────────────────┤
│ SYNCHRONOUS │ ASYNCHRONOUS │
│ (Request/Response) │ (Fire & Forget) │
├───────────────────────────────┼─────────────────────────────┤
│ • HTTP/REST APIs │ • Message Queue (RabbitMQ) │
│ • gRPC │ • Event Bus │
│ • Direct service calls │ • Pub/Sub │
├───────────────────────────────┼─────────────────────────────┤
│ Use for: Queries, UI │ Use for: Commands, Events │
│ Pro: Immediate response │ Pro: Decoupled, resilient │
│ Con: Tight coupling │ Con: Eventual consistency │
└───────────────────────────────┴─────────────────────────────┘
```
### Event Types / Các Loại Event
| Type | Scope | Transport | Example |
|------|-------|-----------|---------|
| **Domain Event** | Within aggregate | In-memory | OrderItemAdded |
| **Integration Event** | Between services | Message broker | OrderCreatedIntegrationEvent |
### Anti-patterns to Avoid / Anti-patterns Cần Tránh
```
❌ Synchronous HTTP Chains (Anti-pattern)
Service A ──HTTP──▶ Service B ──HTTP──▶ Service C ──HTTP──▶ DB
│ │
└── Single point of failure ──┘
✅ Async Event-Driven (Recommended)
Service A ──Publish──▶ Event Bus ──Subscribe──▶ Service B
└────Subscribe──▶ Service C
```
## Key Patterns / Mẫu Chính
### Integration Event Definition
```csharp
/// <summary>
/// EN: Base interface for integration events.
/// VI: Interface cơ sở cho integration events.
/// </summary>
public interface IIntegrationEvent
{
Guid Id { get; }
DateTime OccurredOn { get; }
}
/// <summary>
/// EN: Event when order is created.
/// VI: Event khi order được tạo.
/// </summary>
public record OrderCreatedIntegrationEvent : IIntegrationEvent
{
public Guid Id { get; init; } = Guid.NewGuid();
public DateTime OccurredOn { get; init; } = DateTime.UtcNow;
public Guid OrderId { get; init; }
public string UserId { get; init; } = default!;
public decimal TotalAmount { get; init; }
public IReadOnlyList<OrderItemInfo> Items { get; init; } = Array.Empty<OrderItemInfo>();
}
public record OrderItemInfo(Guid ProductId, int Quantity, decimal UnitPrice);
```
### Event Publisher with MassTransit
```csharp
/// <summary>
/// EN: Publish integration events via MassTransit.
/// VI: Publish integration events qua MassTransit.
/// </summary>
public class IntegrationEventPublisher : IIntegrationEventPublisher
{
private readonly IPublishEndpoint _publishEndpoint;
private readonly ILogger<IntegrationEventPublisher> _logger;
public IntegrationEventPublisher(
IPublishEndpoint publishEndpoint,
ILogger<IntegrationEventPublisher> logger)
{
_publishEndpoint = publishEndpoint;
_logger = logger;
}
public async Task PublishAsync<T>(T @event, CancellationToken ct = default)
where T : class, IIntegrationEvent
{
_logger.LogInformation(
"Publishing event {EventType} with Id {EventId}",
typeof(T).Name,
@event.Id);
await _publishEndpoint.Publish(@event, ct);
}
}
// EN: Usage in command handler / VI: Sử dụng trong command handler
public class CreateOrderCommandHandler : IRequestHandler<CreateOrderCommand, OrderResult>
{
private readonly IOrderRepository _orderRepository;
private readonly IIntegrationEventPublisher _eventPublisher;
public async Task<OrderResult> Handle(CreateOrderCommand request, CancellationToken ct)
{
var order = new Order(request.UserId, request.ShippingAddress);
// ... add items
await _orderRepository.AddAsync(order, ct);
await _orderRepository.UnitOfWork.SaveChangesAsync(ct);
// EN: Publish integration event
// VI: Publish integration event
await _eventPublisher.PublishAsync(new OrderCreatedIntegrationEvent
{
OrderId = order.Id,
UserId = order.UserId,
TotalAmount = order.TotalAmount,
Items = order.OrderItems.Select(i => new OrderItemInfo(
i.ProductId, i.Quantity, i.UnitPrice)).ToList()
}, ct);
return new OrderResult(order.Id);
}
}
```
### Event Consumer
```csharp
/// <summary>
/// EN: Consumer for OrderCreatedIntegrationEvent in Inventory Service.
/// VI: Consumer cho OrderCreatedIntegrationEvent trong Inventory Service.
/// </summary>
public class OrderCreatedConsumer : IConsumer<OrderCreatedIntegrationEvent>
{
private readonly IInventoryService _inventoryService;
private readonly ILogger<OrderCreatedConsumer> _logger;
public OrderCreatedConsumer(
IInventoryService inventoryService,
ILogger<OrderCreatedConsumer> logger)
{
_inventoryService = inventoryService;
_logger = logger;
}
public async Task Consume(ConsumeContext<OrderCreatedIntegrationEvent> context)
{
var @event = context.Message;
_logger.LogInformation(
"Received OrderCreatedEvent: {OrderId}",
@event.OrderId);
try
{
// EN: Reserve inventory for order items
// VI: Đặt trước inventory cho order items
foreach (var item in @event.Items)
{
await _inventoryService.ReserveAsync(
item.ProductId,
item.Quantity,
@event.OrderId,
context.CancellationToken);
}
_logger.LogInformation(
"Inventory reserved for Order: {OrderId}",
@event.OrderId);
}
catch (Exception ex)
{
_logger.LogError(ex,
"Failed to reserve inventory for Order: {OrderId}",
@event.OrderId);
throw; // EN: Will retry based on retry policy
}
}
}
```
### MassTransit Configuration
```csharp
/// <summary>
/// EN: Configure MassTransit with RabbitMQ.
/// VI: Cấu hình MassTransit với RabbitMQ.
/// </summary>
// Program.cs
builder.Services.AddMassTransit(x =>
{
// EN: Register consumers
// VI: Đăng ký consumers
x.AddConsumer<OrderCreatedConsumer>();
x.AddConsumer<ProductPriceChangedConsumer>();
x.UsingRabbitMq((context, cfg) =>
{
cfg.Host(builder.Configuration["RabbitMQ:Host"], "/", h =>
{
h.Username(builder.Configuration["RabbitMQ:Username"]!);
h.Password(builder.Configuration["RabbitMQ:Password"]!);
});
// EN: Configure retry policy
// VI: Cấu hình retry policy
cfg.UseMessageRetry(r => r.Intervals(
TimeSpan.FromSeconds(1),
TimeSpan.FromSeconds(5),
TimeSpan.FromSeconds(15)));
// EN: Configure endpoints
// VI: Cấu hình endpoints
cfg.ConfigureEndpoints(context);
});
});
```
### Resilient HTTP Client with Polly
```csharp
/// <summary>
/// EN: Configure resilient HTTP client for inter-service calls.
/// VI: Cấu hình HTTP client có khả năng phục hồi cho gọi liên dịch vụ.
/// </summary>
// Program.cs
builder.Services.AddHttpClient<IIamServiceClient, IamServiceClient>(client =>
{
client.BaseAddress = new Uri(builder.Configuration["Services:Iam:BaseUrl"]!);
client.DefaultRequestHeaders.Add("Accept", "application/json");
})
.AddResilienceHandler("iam-service", builder =>
{
// EN: Retry with exponential backoff
builder.AddRetry(new HttpRetryStrategyOptions
{
MaxRetryAttempts = 3,
Delay = TimeSpan.FromMilliseconds(500),
BackoffType = DelayBackoffType.Exponential,
ShouldHandle = new PredicateBuilder<HttpResponseMessage>()
.HandleResult(r => (int)r.StatusCode >= 500)
.Handle<HttpRequestException>()
});
// EN: Circuit breaker
builder.AddCircuitBreaker(new HttpCircuitBreakerStrategyOptions
{
FailureRatio = 0.5,
SamplingDuration = TimeSpan.FromSeconds(30),
BreakDuration = TimeSpan.FromSeconds(30)
});
// EN: Timeout
builder.AddTimeout(TimeSpan.FromSeconds(10));
});
```
## Common Mistakes / Lỗi Thường Gặp
### 1. Synchronous HTTP Chains
```csharp
// ❌ BAD: Chain of HTTP calls
public async Task ProcessOrder(Order order)
{
await _inventoryService.ReserveAsync(order); // HTTP
await _paymentService.ChargeAsync(order); // HTTP
await _shippingService.CreateShipmentAsync(order); // HTTP
}
// ✅ GOOD: Event-driven, async
public async Task ProcessOrder(Order order)
{
await _eventPublisher.PublishAsync(new OrderCreatedEvent(order));
// Each service subscribes and processes independently
}
```
### 2. Missing Idempotency
```csharp
// ❌ BAD: No idempotency check
public async Task Consume(ConsumeContext<OrderCreatedEvent> context)
{
await _service.CreateShipment(context.Message.OrderId);
}
// ✅ GOOD: Idempotent processing
public async Task Consume(ConsumeContext<OrderCreatedEvent> context)
{
var eventId = context.Message.Id;
if (await _processedEvents.ExistsAsync(eventId))
return; // Already processed
await _service.CreateShipment(context.Message.OrderId);
await _processedEvents.MarkProcessedAsync(eventId);
}
```
### 3. No Retry Policy
```csharp
// ❌ BAD: Raw HTTP call without resilience
var response = await _httpClient.GetAsync("/api/users/123");
// ✅ GOOD: Use resilient HTTP client (configured at DI level)
// Retries, circuit breaker, and timeout are handled automatically
var response = await _resilientClient.GetAsync("/api/users/123");
```
## Quick Reference / Tham Chiếu Nhanh
### When to Use What
| Scenario | Pattern | Transport |
|----------|---------|-----------|
| Get data for UI | Sync HTTP | REST/gRPC |
| Notify state change | Async Event | RabbitMQ |
| Long-running process | Async Event | RabbitMQ |
| Real-time updates | Pub/Sub | RabbitMQ/SignalR |
### Message Broker vs Direct Call
| Aspect | Message Broker | Direct HTTP |
|--------|----------------|-------------|
| Coupling | Loose | Tight |
| Failure handling | Built-in retry | Manual |
| Scalability | High | Medium |
| Debugging | Harder | Easier |
## Resources / Tài Nguyên
- [Detailed Examples](./references/REFERENCE.md) - Full code examples
- [Error Handling](../error-handling-patterns/SKILL.md) - Polly patterns
- [CQRS MediatR](../cqrs-mediatr/SKILL.md) - Domain events
- [Docker Traefik](../docker-traefik/SKILL.md) - RabbitMQ container

View File

@@ -0,0 +1,520 @@
# Inter-Service Communication - Detailed Reference
Detailed code examples cho giao tiếp liên dịch vụ trong GoodGo.
## Table of Contents
1. [MassTransit Setup](#masstransit-setup)
2. [Integration Events](#integration-events)
3. [Event Consumers](#event-consumers)
4. [HTTP Client Patterns](#http-client-patterns)
5. [Outbox Pattern](#outbox-pattern)
6. [gRPC Setup](#grpc-setup)
---
## MassTransit Setup
### Complete MassTransit Configuration
```csharp
/// <summary>
/// EN: Configure MassTransit with RabbitMQ for production.
/// VI: Cấu hình MassTransit với RabbitMQ cho production.
/// </summary>
// Program.cs
builder.Services.AddMassTransit(x =>
{
// EN: Register all consumers from assembly
// VI: Đăng ký tất cả consumers từ assembly
x.AddConsumers(typeof(Program).Assembly);
// EN: Configure saga if needed
// x.AddSagaStateMachine<OrderStateMachine, OrderState>()
// .EntityFrameworkRepository(...);
x.UsingRabbitMq((context, cfg) =>
{
var rabbitConfig = builder.Configuration.GetSection("RabbitMQ");
cfg.Host(rabbitConfig["Host"], rabbitConfig["VirtualHost"] ?? "/", h =>
{
h.Username(rabbitConfig["Username"]!);
h.Password(rabbitConfig["Password"]!);
});
// EN: Global retry policy
// VI: Retry policy toàn cục
cfg.UseMessageRetry(r =>
{
r.Incremental(3, TimeSpan.FromSeconds(1), TimeSpan.FromSeconds(2));
r.Ignore<ValidationException>();
});
// EN: Dead letter queue for failed messages
// VI: Dead letter queue cho messages thất bại
cfg.UseDelayedRedelivery(r => r.Intervals(
TimeSpan.FromMinutes(5),
TimeSpan.FromMinutes(15),
TimeSpan.FromMinutes(30)));
// EN: Configure endpoints
// VI: Cấu hình endpoints
cfg.ConfigureEndpoints(context, new KebabCaseEndpointNameFormatter("goodgo", false));
});
});
```
### appsettings.json
```json
{
"RabbitMQ": {
"Host": "localhost",
"VirtualHost": "/",
"Username": "guest",
"Password": "guest"
}
}
```
---
## Integration Events
### Event Definitions
```csharp
/// <summary>
/// EN: Integration event base record.
/// VI: Integration event record cơ sở.
/// </summary>
public abstract record IntegrationEvent
{
public Guid Id { get; init; } = Guid.NewGuid();
public DateTime OccurredOn { get; init; } = DateTime.UtcNow;
public string CorrelationId { get; init; } = string.Empty;
}
/// <summary>
/// EN: Event when order status changes.
/// VI: Event khi trạng thái order thay đổi.
/// </summary>
public record OrderStatusChangedIntegrationEvent : IntegrationEvent
{
public Guid OrderId { get; init; }
public string PreviousStatus { get; init; } = default!;
public string NewStatus { get; init; } = default!;
public string UserId { get; init; } = default!;
}
/// <summary>
/// EN: Event when product price changes.
/// VI: Event khi giá sản phẩm thay đổi.
/// </summary>
public record ProductPriceChangedIntegrationEvent : IntegrationEvent
{
public Guid ProductId { get; init; }
public decimal OldPrice { get; init; }
public decimal NewPrice { get; init; }
}
/// <summary>
/// EN: Event when payment is completed.
/// VI: Event khi thanh toán hoàn thành.
/// </summary>
public record PaymentCompletedIntegrationEvent : IntegrationEvent
{
public Guid PaymentId { get; init; }
public Guid OrderId { get; init; }
public decimal Amount { get; init; }
public string PaymentMethod { get; init; } = default!;
}
```
### Event Publisher Service
```csharp
/// <summary>
/// EN: Service to publish integration events.
/// VI: Service publish integration events.
/// </summary>
public interface IEventPublisher
{
Task PublishAsync<T>(T @event, CancellationToken ct = default) where T : class;
}
public class MassTransitEventPublisher : IEventPublisher
{
private readonly IPublishEndpoint _publishEndpoint;
private readonly ILogger<MassTransitEventPublisher> _logger;
public MassTransitEventPublisher(
IPublishEndpoint publishEndpoint,
ILogger<MassTransitEventPublisher> logger)
{
_publishEndpoint = publishEndpoint;
_logger = logger;
}
public async Task PublishAsync<T>(T @event, CancellationToken ct = default)
where T : class
{
try
{
await _publishEndpoint.Publish(@event, ct);
_logger.LogInformation(
"Published event {EventType}: {@Event}",
typeof(T).Name, @event);
}
catch (Exception ex)
{
_logger.LogError(ex,
"Failed to publish event {EventType}: {@Event}",
typeof(T).Name, @event);
throw;
}
}
}
```
---
## Event Consumers
### Consumer with Idempotency
```csharp
/// <summary>
/// EN: Idempotent consumer for payment completed events.
/// VI: Consumer idempotent cho payment completed events.
/// </summary>
public class PaymentCompletedConsumer : IConsumer<PaymentCompletedIntegrationEvent>
{
private readonly IOrderService _orderService;
private readonly IIdempotencyService _idempotency;
private readonly ILogger<PaymentCompletedConsumer> _logger;
public PaymentCompletedConsumer(
IOrderService orderService,
IIdempotencyService idempotency,
ILogger<PaymentCompletedConsumer> logger)
{
_orderService = orderService;
_idempotency = idempotency;
_logger = logger;
}
public async Task Consume(ConsumeContext<PaymentCompletedIntegrationEvent> context)
{
var @event = context.Message;
// EN: Check if already processed
// VI: Kiểm tra đã xử lý chưa
if (await _idempotency.HasBeenProcessedAsync(@event.Id))
{
_logger.LogInformation(
"Event {EventId} already processed, skipping",
@event.Id);
return;
}
try
{
_logger.LogInformation(
"Processing PaymentCompleted for Order {OrderId}",
@event.OrderId);
await _orderService.ConfirmPaymentAsync(
@event.OrderId,
@event.PaymentId,
@event.Amount,
context.CancellationToken);
// EN: Mark as processed
// VI: Đánh dấu đã xử lý
await _idempotency.MarkAsProcessedAsync(@event.Id);
_logger.LogInformation(
"Successfully processed PaymentCompleted for Order {OrderId}",
@event.OrderId);
}
catch (Exception ex)
{
_logger.LogError(ex,
"Failed to process PaymentCompleted for Order {OrderId}",
@event.OrderId);
throw; // EN: Will trigger retry
}
}
}
```
### Consumer Definition (Advanced Config)
```csharp
/// <summary>
/// EN: Consumer definition with custom retry and concurrency.
/// VI: Consumer definition với retry và concurrency tùy chỉnh.
/// </summary>
public class PaymentCompletedConsumerDefinition
: ConsumerDefinition<PaymentCompletedConsumer>
{
public PaymentCompletedConsumerDefinition()
{
// EN: Endpoint name
// VI: Tên endpoint
EndpointName = "order-service-payment-completed";
// EN: Prefetch count for concurrency
// VI: Prefetch count cho concurrency
ConcurrentMessageLimit = 10;
}
protected override void ConfigureConsumer(
IReceiveEndpointConfigurator endpointConfigurator,
IConsumerConfigurator<PaymentCompletedConsumer> consumerConfigurator,
IRegistrationContext context)
{
// EN: Custom retry for this consumer
// VI: Retry tùy chỉnh cho consumer này
endpointConfigurator.UseMessageRetry(r =>
{
r.Intervals(
TimeSpan.FromSeconds(5),
TimeSpan.FromSeconds(30),
TimeSpan.FromMinutes(5));
r.Ignore<OrderNotFoundException>();
});
// EN: Circuit breaker
// VI: Circuit breaker
endpointConfigurator.UseCircuitBreaker(cb =>
{
cb.TrackingPeriod = TimeSpan.FromMinutes(1);
cb.TripThreshold = 15;
cb.ActiveThreshold = 10;
cb.ResetInterval = TimeSpan.FromMinutes(5);
});
}
}
```
---
## HTTP Client Patterns
### Typed HTTP Client
```csharp
/// <summary>
/// EN: Typed HTTP client for IAM service.
/// VI: Typed HTTP client cho IAM service.
/// </summary>
public interface IIamServiceClient
{
Task<UserInfoDto?> GetUserAsync(string userId, CancellationToken ct = default);
Task<bool> ValidateTokenAsync(string token, CancellationToken ct = default);
}
public class IamServiceClient : IIamServiceClient
{
private readonly HttpClient _httpClient;
private readonly ILogger<IamServiceClient> _logger;
public IamServiceClient(
HttpClient httpClient,
ILogger<IamServiceClient> logger)
{
_httpClient = httpClient;
_logger = logger;
}
public async Task<UserInfoDto?> GetUserAsync(string userId, CancellationToken ct = default)
{
try
{
var response = await _httpClient.GetAsync($"/api/v1/users/{userId}", ct);
if (response.StatusCode == HttpStatusCode.NotFound)
return null;
response.EnsureSuccessStatusCode();
var result = await response.Content.ReadFromJsonAsync<ApiResponse<UserInfoDto>>(ct);
return result?.Data;
}
catch (HttpRequestException ex)
{
_logger.LogError(ex, "Failed to get user {UserId} from IAM service", userId);
throw;
}
}
public async Task<bool> ValidateTokenAsync(string token, CancellationToken ct = default)
{
try
{
var request = new HttpRequestMessage(HttpMethod.Post, "/api/v1/auth/validate");
request.Content = JsonContent.Create(new { Token = token });
var response = await _httpClient.SendAsync(request, ct);
return response.IsSuccessStatusCode;
}
catch (HttpRequestException ex)
{
_logger.LogWarning(ex, "Token validation failed");
return false;
}
}
}
```
### HTTP Client Registration with Polly
```csharp
/// <summary>
/// EN: Register resilient HTTP clients.
/// VI: Đăng ký HTTP clients có khả năng phục hồi.
/// </summary>
builder.Services.AddHttpClient<IIamServiceClient, IamServiceClient>(client =>
{
client.BaseAddress = new Uri(builder.Configuration["Services:Iam:BaseUrl"]!);
client.Timeout = TimeSpan.FromSeconds(30);
})
.AddResilienceHandler("iam-client", builder =>
{
builder.AddRetry(new HttpRetryStrategyOptions
{
MaxRetryAttempts = 3,
Delay = TimeSpan.FromMilliseconds(300),
BackoffType = DelayBackoffType.Exponential,
UseJitter = true,
ShouldHandle = new PredicateBuilder<HttpResponseMessage>()
.HandleResult(r => r.StatusCode >= HttpStatusCode.InternalServerError)
.Handle<HttpRequestException>()
.Handle<TimeoutRejectedException>()
});
builder.AddCircuitBreaker(new HttpCircuitBreakerStrategyOptions
{
FailureRatio = 0.3,
SamplingDuration = TimeSpan.FromSeconds(60),
MinimumThroughput = 10,
BreakDuration = TimeSpan.FromSeconds(30)
});
builder.AddTimeout(TimeSpan.FromSeconds(10));
});
```
---
## Outbox Pattern
### Outbox Entity
```csharp
/// <summary>
/// EN: Outbox message entity for reliable event publishing.
/// VI: Outbox message entity cho event publishing đáng tin cậy.
/// </summary>
public class OutboxMessage
{
public Guid Id { get; set; }
public string Type { get; set; } = default!;
public string Payload { get; set; } = default!;
public DateTime OccurredOn { get; set; }
public DateTime? ProcessedOn { get; set; }
public string? Error { get; set; }
public int RetryCount { get; set; }
}
// EN: DbContext configuration
// VI: Cấu hình DbContext
modelBuilder.Entity<OutboxMessage>(entity =>
{
entity.ToTable("OutboxMessages");
entity.HasKey(e => e.Id);
entity.HasIndex(e => e.ProcessedOn);
});
```
### Outbox Processor
```csharp
/// <summary>
/// EN: Background service to process outbox messages.
/// VI: Background service xử lý outbox messages.
/// </summary>
public class OutboxProcessor : BackgroundService
{
private readonly IServiceProvider _serviceProvider;
private readonly ILogger<OutboxProcessor> _logger;
private readonly TimeSpan _pollingInterval = TimeSpan.FromSeconds(5);
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
while (!stoppingToken.IsCancellationRequested)
{
try
{
await ProcessPendingMessagesAsync(stoppingToken);
}
catch (Exception ex)
{
_logger.LogError(ex, "Error processing outbox messages");
}
await Task.Delay(_pollingInterval, stoppingToken);
}
}
private async Task ProcessPendingMessagesAsync(CancellationToken ct)
{
using var scope = _serviceProvider.CreateScope();
var dbContext = scope.ServiceProvider.GetRequiredService<ApplicationDbContext>();
var publisher = scope.ServiceProvider.GetRequiredService<IPublishEndpoint>();
var messages = await dbContext.OutboxMessages
.Where(m => m.ProcessedOn == null && m.RetryCount < 3)
.OrderBy(m => m.OccurredOn)
.Take(100)
.ToListAsync(ct);
foreach (var message in messages)
{
try
{
var eventType = Type.GetType(message.Type)!;
var @event = JsonSerializer.Deserialize(message.Payload, eventType)!;
await publisher.Publish(@event, eventType, ct);
message.ProcessedOn = DateTime.UtcNow;
_logger.LogDebug("Processed outbox message {Id}", message.Id);
}
catch (Exception ex)
{
message.RetryCount++;
message.Error = ex.Message;
_logger.LogWarning(ex, "Failed to process outbox message {Id}", message.Id);
}
}
await dbContext.SaveChangesAsync(ct);
}
}
```
---
## Resources / Tài Nguyên
- [MassTransit Documentation](https://masstransit.io/)
- [RabbitMQ Documentation](https://www.rabbitmq.com/documentation.html)
- [Polly Documentation](https://github.com/App-vNext/Polly)
- [gRPC for .NET](https://docs.microsoft.com/en-us/aspnet/core/grpc/)

View File

@@ -0,0 +1,356 @@
---
name: redis-caching
description: Redis caching patterns cho distributed systems. Use for cache-aside, session storage, rate limiting, và distributed locks.
compatibility: ".NET 8+, StackExchange.Redis, Microsoft.Extensions.Caching.StackExchangeRedis"
metadata:
author: Velik Ho
version: "1.0"
---
# Redis Caching Patterns / Mẫu Caching Redis
Redis caching và distributed data patterns cho GoodGo microservices.
## When to Use This Skill / Khi Nào Sử Dụng
Use this skill when:
- Implementing distributed cache / Triển khai distributed cache
- Caching API responses / Caching responses API
- Managing user sessions / Quản lý user sessions
- Implementing rate limiting / Triển khai rate limiting
- Using Redis as primary store (e.g., shopping cart) / Dùng Redis làm store chính
## Core Concepts / Khái Niệm Cốt Lõi
### Caching Strategies / Các Chiến Lược Caching
```
┌─────────────────────────────────────────────────────────────┐
│ CACHING PATTERNS │
├─────────────────────┬─────────────────────┬─────────────────┤
│ CACHE-ASIDE │ WRITE-THROUGH │ WRITE-BEHIND │
│ (Read pattern) │ (Write pattern) │ (Async write) │
├─────────────────────┼─────────────────────┼─────────────────┤
│ 1. Check cache │ 1. Write to cache │ 1. Write cache │
│ 2. If miss, get DB │ 2. Write to DB sync │ 2. Queue DB │
│ 3. Populate cache │ │ 3. Async flush │
└─────────────────────┴─────────────────────┴─────────────────┘
```
### Redis Data Types / Các Kiểu Dữ Liệu Redis
| Type | Use Case | Example |
|------|----------|---------|
| **String** | Simple cache | User profile, JWT token |
| **Hash** | Object cache | Shopping cart |
| **List** | Queues | Task queue |
| **Set** | Unique items | Active users |
| **Sorted Set** | Rankings | Leaderboard |
### Cache Invalidation / Xóa Cache
| Strategy | Description | Use When |
|----------|-------------|----------|
| **TTL** | Auto-expire after time | Data changes rarely |
| **Event-based** | Invalidate on update | Data changes often |
| **Version key** | Cache with version | Complex objects |
## Key Patterns / Mẫu Chính
### Redis Configuration
```csharp
/// <summary>
/// EN: Configure Redis distributed cache.
/// VI: Cấu hình Redis distributed cache.
/// </summary>
// Program.cs
builder.Services.AddStackExchangeRedisCache(options =>
{
options.Configuration = builder.Configuration["Redis:ConnectionString"];
options.InstanceName = "GoodGo:";
});
// EN: Register Redis connection multiplexer for advanced scenarios
// VI: Đăng ký Redis connection multiplexer cho các trường hợp nâng cao
builder.Services.AddSingleton<IConnectionMultiplexer>(sp =>
ConnectionMultiplexer.Connect(builder.Configuration["Redis:ConnectionString"]!));
```
### Cache Service Implementation
```csharp
/// <summary>
/// EN: Generic cache service with typed operations.
/// VI: Cache service generic với typed operations.
/// </summary>
public interface ICacheService
{
Task<T?> GetAsync<T>(string key, CancellationToken ct = default);
Task SetAsync<T>(string key, T value, TimeSpan? expiry = null, CancellationToken ct = default);
Task RemoveAsync(string key, CancellationToken ct = default);
Task<T> GetOrSetAsync<T>(string key, Func<Task<T>> factory, TimeSpan? expiry = null, CancellationToken ct = default);
}
public class RedisCacheService : ICacheService
{
private readonly IDistributedCache _cache;
private readonly ILogger<RedisCacheService> _logger;
private static readonly TimeSpan DefaultExpiry = TimeSpan.FromMinutes(30);
public RedisCacheService(
IDistributedCache cache,
ILogger<RedisCacheService> logger)
{
_cache = cache;
_logger = logger;
}
public async Task<T?> GetAsync<T>(string key, CancellationToken ct = default)
{
var data = await _cache.GetStringAsync(key, ct);
if (data == null)
{
_logger.LogDebug("Cache miss for key: {Key}", key);
return default;
}
_logger.LogDebug("Cache hit for key: {Key}", key);
return JsonSerializer.Deserialize<T>(data);
}
public async Task SetAsync<T>(
string key,
T value,
TimeSpan? expiry = null,
CancellationToken ct = default)
{
var options = new DistributedCacheEntryOptions
{
AbsoluteExpirationRelativeToNow = expiry ?? DefaultExpiry
};
var data = JsonSerializer.Serialize(value);
await _cache.SetStringAsync(key, data, options, ct);
_logger.LogDebug("Cached key: {Key} with expiry: {Expiry}", key, expiry ?? DefaultExpiry);
}
public async Task RemoveAsync(string key, CancellationToken ct = default)
{
await _cache.RemoveAsync(key, ct);
_logger.LogDebug("Removed cache key: {Key}", key);
}
public async Task<T> GetOrSetAsync<T>(
string key,
Func<Task<T>> factory,
TimeSpan? expiry = null,
CancellationToken ct = default)
{
var cached = await GetAsync<T>(key, ct);
if (cached != null)
return cached;
var value = await factory();
await SetAsync(key, value, expiry, ct);
return value;
}
}
```
### Cache-Aside Pattern in Query Handler
```csharp
/// <summary>
/// EN: Query handler with cache-aside pattern.
/// VI: Query handler với cache-aside pattern.
/// </summary>
public class GetUserProfileQueryHandler
: IRequestHandler<GetUserProfileQuery, UserProfileDto?>
{
private readonly ICacheService _cache;
private readonly IUserRepository _userRepository;
private static readonly TimeSpan CacheExpiry = TimeSpan.FromMinutes(15);
public async Task<UserProfileDto?> Handle(
GetUserProfileQuery request,
CancellationToken ct)
{
var cacheKey = $"user:profile:{request.UserId}";
// EN: Try get from cache first
// VI: Thử lấy từ cache trước
return await _cache.GetOrSetAsync(
cacheKey,
async () =>
{
var user = await _userRepository.GetByIdAsync(request.UserId, ct);
return user?.ToProfileDto();
},
CacheExpiry,
ct);
}
}
```
### Cache Invalidation on Write
```csharp
/// <summary>
/// EN: Invalidate cache when data changes.
/// VI: Xóa cache khi dữ liệu thay đổi.
/// </summary>
public class UpdateUserProfileCommandHandler
: IRequestHandler<UpdateUserProfileCommand, Unit>
{
private readonly ICacheService _cache;
private readonly IUserRepository _userRepository;
public async Task<Unit> Handle(
UpdateUserProfileCommand request,
CancellationToken ct)
{
var user = await _userRepository.GetByIdAsync(request.UserId, ct)
?? throw new NotFoundException("User", request.UserId);
user.UpdateProfile(request.DisplayName, request.Bio);
await _userRepository.UnitOfWork.SaveChangesAsync(ct);
// EN: Invalidate cache after update
// VI: Xóa cache sau khi cập nhật
await _cache.RemoveAsync($"user:profile:{request.UserId}", ct);
return Unit.Value;
}
}
```
### Shopping Cart with Redis Hash
```csharp
/// <summary>
/// EN: Shopping cart repository using Redis Hash.
/// VI: Repository giỏ hàng dùng Redis Hash.
/// </summary>
public class RedisCartRepository : ICartRepository
{
private readonly IDatabase _redis;
private static readonly TimeSpan CartExpiry = TimeSpan.FromDays(7);
public RedisCartRepository(IConnectionMultiplexer redis)
{
_redis = redis.GetDatabase();
}
public async Task<Cart?> GetAsync(string cartId)
{
var data = await _redis.StringGetAsync($"cart:{cartId}");
return data.IsNullOrEmpty
? null
: JsonSerializer.Deserialize<Cart>(data!);
}
public async Task SaveAsync(Cart cart)
{
var key = $"cart:{cart.Id}";
var data = JsonSerializer.Serialize(cart);
await _redis.StringSetAsync(key, data, CartExpiry);
}
public async Task DeleteAsync(string cartId)
{
await _redis.KeyDeleteAsync($"cart:{cartId}");
}
}
```
## Common Mistakes / Lỗi Thường Gặp
### 1. No Cache Expiry
```csharp
// ❌ BAD: No expiry leads to stale data
await _cache.SetStringAsync(key, data);
// ✅ GOOD: Always set expiry
await _cache.SetStringAsync(key, data, new DistributedCacheEntryOptions
{
AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(30)
});
```
### 2. Cache Stampede
```csharp
// ❌ BAD: Multiple requests hit DB simultaneously on cache miss
var data = await _cache.GetAsync<Data>(key);
if (data == null)
{
data = await _db.GetDataAsync();
await _cache.SetAsync(key, data);
}
// ✅ GOOD: Use locking to prevent stampede
var data = await _cache.GetOrSetWithLockAsync(key, async () =>
{
return await _db.GetDataAsync();
});
```
### 3. Caching Null Values
```csharp
// ❌ BAD: Not caching null causes repeated DB calls
if (data != null)
await _cache.SetAsync(key, data);
// ✅ GOOD: Cache null with short TTL
var cacheValue = new CacheWrapper<Data> { Value = data, IsNull = data == null };
await _cache.SetAsync(key, cacheValue, data == null ? TimeSpan.FromMinutes(1) : TimeSpan.FromHours(1));
```
## Quick Reference / Tham Chiếu Nhanh
### Cache Key Naming
```csharp
// EN: Pattern: {entity}:{id}:{optional-subkey}
// VI: Pattern: {entity}:{id}:{optional-subkey}
"user:profile:123"
"order:items:456"
"product:details:789"
"user:orders:123:page:1"
```
### Common TTL Values
| Data Type | TTL | Reason |
|-----------|-----|--------|
| User profile | 15-30 min | Changes moderately |
| Product catalog | 1-4 hours | Batch updates |
| Shopping cart | 7 days | User convenience |
| Session | 30 min sliding | Security |
| Rate limit | 1 min | Short window |
### Redis Commands via CLI
```bash
# EN: View all keys with pattern
redis-cli KEYS "user:*"
# EN: Get TTL of key
redis-cli TTL "user:profile:123"
# EN: Delete keys by pattern
redis-cli KEYS "cart:*" | xargs redis-cli DEL
```
## Resources / Tài Nguyên
- [Detailed Examples](./references/REFERENCE.md) - Full code examples
- [Error Handling](../error-handling-patterns/SKILL.md) - Cache failure handling
- [Repository Pattern](../repository-pattern/SKILL.md) - Data access patterns
- [Docker Traefik](../docker-traefik/SKILL.md) - Redis container setup

View File

@@ -0,0 +1,479 @@
# Redis Caching - Detailed Reference
Detailed code examples cho Redis caching patterns trong GoodGo.
## Table of Contents
1. [Connection Setup](#connection-setup)
2. [Cache Service](#cache-service)
3. [Shopping Cart](#shopping-cart)
4. [Rate Limiting](#rate-limiting)
5. [Distributed Locks](#distributed-locks)
6. [Session Management](#session-management)
---
## Connection Setup
### Redis Configuration
```csharp
/// <summary>
/// EN: Configure Redis for distributed cache and direct access.
/// VI: Cấu hình Redis cho distributed cache và truy cập trực tiếp.
/// </summary>
// appsettings.json
{
"Redis": {
"ConnectionString": "localhost:6379,abortConnect=false,connectTimeout=5000",
"InstanceName": "GoodGo:",
"DefaultDatabase": 0
}
}
// Program.cs
builder.Services.AddStackExchangeRedisCache(options =>
{
options.Configuration = builder.Configuration["Redis:ConnectionString"];
options.InstanceName = builder.Configuration["Redis:InstanceName"];
});
// EN: For advanced scenarios (Lua scripts, pub/sub, etc.)
// VI: Cho các trường hợp nâng cao (Lua scripts, pub/sub, v.v.)
builder.Services.AddSingleton<IConnectionMultiplexer>(sp =>
{
var config = ConfigurationOptions.Parse(
builder.Configuration["Redis:ConnectionString"]!);
config.AbortOnConnectFail = false;
config.ConnectRetry = 3;
return ConnectionMultiplexer.Connect(config);
});
```
---
## Cache Service
### Complete Cache Service Implementation
```csharp
/// <summary>
/// EN: Full-featured cache service with serialization and error handling.
/// VI: Cache service đầy đủ tính năng với serialization và xử lý lỗi.
/// </summary>
public class RedisCacheService : ICacheService
{
private readonly IDistributedCache _cache;
private readonly IConnectionMultiplexer _redis;
private readonly ILogger<RedisCacheService> _logger;
private readonly JsonSerializerOptions _jsonOptions;
public RedisCacheService(
IDistributedCache cache,
IConnectionMultiplexer redis,
ILogger<RedisCacheService> logger)
{
_cache = cache;
_redis = redis;
_logger = logger;
_jsonOptions = new JsonSerializerOptions
{
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull
};
}
public async Task<T?> GetAsync<T>(string key, CancellationToken ct = default)
{
try
{
var data = await _cache.GetStringAsync(key, ct);
if (string.IsNullOrEmpty(data))
{
_logger.LogDebug("Cache MISS: {Key}", key);
return default;
}
_logger.LogDebug("Cache HIT: {Key}", key);
return JsonSerializer.Deserialize<T>(data, _jsonOptions);
}
catch (RedisConnectionException ex)
{
_logger.LogWarning(ex, "Redis connection failed for key: {Key}", key);
return default; // EN: Graceful degradation / VI: Xử lý graceful
}
}
public async Task SetAsync<T>(
string key,
T value,
TimeSpan? absoluteExpiry = null,
TimeSpan? slidingExpiry = null,
CancellationToken ct = default)
{
try
{
var options = new DistributedCacheEntryOptions();
if (absoluteExpiry.HasValue)
options.AbsoluteExpirationRelativeToNow = absoluteExpiry;
if (slidingExpiry.HasValue)
options.SlidingExpiration = slidingExpiry;
var data = JsonSerializer.Serialize(value, _jsonOptions);
await _cache.SetStringAsync(key, data, options, ct);
_logger.LogDebug("Cache SET: {Key}", key);
}
catch (RedisConnectionException ex)
{
_logger.LogWarning(ex, "Failed to cache key: {Key}", key);
// EN: Don't throw - cache is not critical / VI: Không throw - cache không critical
}
}
public async Task<T> GetOrSetAsync<T>(
string key,
Func<CancellationToken, Task<T>> factory,
TimeSpan? expiry = null,
CancellationToken ct = default)
{
var cached = await GetAsync<T>(key, ct);
if (cached != null)
return cached;
// EN: Use lock to prevent cache stampede
// VI: Dùng lock để tránh cache stampede
var lockKey = $"lock:{key}";
var db = _redis.GetDatabase();
if (await db.LockTakeAsync(lockKey, Environment.MachineName, TimeSpan.FromSeconds(10)))
{
try
{
// EN: Double-check after acquiring lock
// VI: Kiểm tra lại sau khi lấy lock
cached = await GetAsync<T>(key, ct);
if (cached != null)
return cached;
var value = await factory(ct);
await SetAsync(key, value, expiry, ct: ct);
return value;
}
finally
{
await db.LockReleaseAsync(lockKey, Environment.MachineName);
}
}
// EN: Lock not acquired, wait and retry
// VI: Không lấy được lock, chờ và thử lại
await Task.Delay(100, ct);
return await GetOrSetAsync(key, factory, expiry, ct);
}
public async Task RemoveAsync(string key, CancellationToken ct = default)
{
await _cache.RemoveAsync(key, ct);
_logger.LogDebug("Cache REMOVE: {Key}", key);
}
public async Task RemoveByPatternAsync(string pattern, CancellationToken ct = default)
{
var server = _redis.GetServer(_redis.GetEndPoints().First());
var keys = server.Keys(pattern: pattern).ToArray();
if (keys.Length > 0)
{
var db = _redis.GetDatabase();
await db.KeyDeleteAsync(keys);
_logger.LogDebug("Cache REMOVE pattern: {Pattern}, Count: {Count}", pattern, keys.Length);
}
}
}
```
---
## Shopping Cart
### Cart Entity
```csharp
/// <summary>
/// EN: Shopping cart stored in Redis.
/// VI: Giỏ hàng lưu trong Redis.
/// </summary>
public class Cart
{
public string Id { get; set; } = default!;
public string UserId { get; set; } = default!;
public List<CartItem> Items { get; set; } = new();
public DateTime UpdatedAt { get; set; } = DateTime.UtcNow;
public decimal TotalAmount => Items.Sum(i => i.Quantity * i.UnitPrice);
public void AddItem(Guid productId, string productName, int quantity, decimal unitPrice)
{
var existing = Items.FirstOrDefault(i => i.ProductId == productId);
if (existing != null)
{
existing.Quantity += quantity;
existing.UnitPrice = unitPrice; // EN: Update price
}
else
{
Items.Add(new CartItem
{
ProductId = productId,
ProductName = productName,
Quantity = quantity,
UnitPrice = unitPrice
});
}
UpdatedAt = DateTime.UtcNow;
}
public void RemoveItem(Guid productId)
{
Items.RemoveAll(i => i.ProductId == productId);
UpdatedAt = DateTime.UtcNow;
}
}
public class CartItem
{
public Guid ProductId { get; set; }
public string ProductName { get; set; } = default!;
public int Quantity { get; set; }
public decimal UnitPrice { get; set; }
}
```
### Cart Repository
```csharp
/// <summary>
/// EN: Redis-backed cart repository.
/// VI: Repository giỏ hàng với Redis.
/// </summary>
public class RedisCartRepository : ICartRepository
{
private readonly IDatabase _db;
private readonly ILogger<RedisCartRepository> _logger;
private static readonly TimeSpan DefaultExpiry = TimeSpan.FromDays(7);
public RedisCartRepository(
IConnectionMultiplexer redis,
ILogger<RedisCartRepository> logger)
{
_db = redis.GetDatabase();
_logger = logger;
}
public async Task<Cart?> GetByUserIdAsync(string userId, CancellationToken ct = default)
{
var key = GetKey(userId);
var data = await _db.StringGetAsync(key);
if (data.IsNullOrEmpty)
return null;
return JsonSerializer.Deserialize<Cart>(data!);
}
public async Task SaveAsync(Cart cart, CancellationToken ct = default)
{
var key = GetKey(cart.UserId);
var data = JsonSerializer.Serialize(cart);
await _db.StringSetAsync(key, data, DefaultExpiry);
_logger.LogInformation(
"Cart saved for user {UserId}, Items: {ItemCount}",
cart.UserId, cart.Items.Count);
}
public async Task DeleteAsync(string userId, CancellationToken ct = default)
{
var key = GetKey(userId);
await _db.KeyDeleteAsync(key);
_logger.LogInformation("Cart deleted for user {UserId}", userId);
}
private static string GetKey(string userId) => $"cart:{userId}";
}
```
---
## Rate Limiting
### Redis Rate Limiter
```csharp
/// <summary>
/// EN: Redis-based sliding window rate limiter.
/// VI: Rate limiter sliding window với Redis.
/// </summary>
public class RedisRateLimiter : IRateLimiter
{
private readonly IDatabase _db;
private readonly ILogger<RedisRateLimiter> _logger;
public RedisRateLimiter(
IConnectionMultiplexer redis,
ILogger<RedisRateLimiter> logger)
{
_db = redis.GetDatabase();
_logger = logger;
}
public async Task<RateLimitResult> CheckAsync(
string key,
int maxRequests,
TimeSpan window)
{
var now = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
var windowStart = now - (long)window.TotalMilliseconds;
var redisKey = $"ratelimit:{key}";
// EN: Lua script for atomic sliding window
// VI: Lua script cho sliding window atomic
var script = @"
redis.call('ZREMRANGEBYSCORE', KEYS[1], 0, ARGV[1])
local count = redis.call('ZCARD', KEYS[1])
if count < tonumber(ARGV[2]) then
redis.call('ZADD', KEYS[1], ARGV[3], ARGV[3])
redis.call('EXPIRE', KEYS[1], ARGV[4])
return {1, count + 1, tonumber(ARGV[2])}
else
local oldest = redis.call('ZRANGE', KEYS[1], 0, 0, 'WITHSCORES')
local retryAfter = oldest[2] + tonumber(ARGV[5]) - tonumber(ARGV[3])
return {0, count, tonumber(ARGV[2]), retryAfter}
end
";
var result = await _db.ScriptEvaluateAsync(script,
new RedisKey[] { redisKey },
new RedisValue[]
{
windowStart,
maxRequests,
now,
(int)window.TotalSeconds,
(long)window.TotalMilliseconds
});
var values = (RedisResult[])result!;
var allowed = (int)values[0] == 1;
var currentCount = (int)values[1];
var limit = (int)values[2];
var retryAfter = values.Length > 3 ? TimeSpan.FromMilliseconds((long)values[3]) : TimeSpan.Zero;
if (!allowed)
{
_logger.LogWarning(
"Rate limit exceeded for {Key}: {Count}/{Limit}",
key, currentCount, limit);
}
return new RateLimitResult(allowed, currentCount, limit, retryAfter);
}
}
public record RateLimitResult(
bool IsAllowed,
int CurrentCount,
int Limit,
TimeSpan RetryAfter);
```
---
## Distributed Locks
### Redis Lock Service
```csharp
/// <summary>
/// EN: Distributed lock using Redis.
/// VI: Distributed lock dùng Redis.
/// </summary>
public class RedisLockService : IDistributedLockService
{
private readonly IDatabase _db;
private readonly ILogger<RedisLockService> _logger;
public async Task<IAsyncDisposable?> AcquireAsync(
string resource,
TimeSpan expiry,
TimeSpan? waitTime = null,
CancellationToken ct = default)
{
var lockKey = $"lock:{resource}";
var lockValue = Guid.NewGuid().ToString();
var deadline = DateTime.UtcNow + (waitTime ?? TimeSpan.Zero);
do
{
if (await _db.LockTakeAsync(lockKey, lockValue, expiry))
{
_logger.LogDebug("Lock acquired: {Resource}", resource);
return new RedisLock(_db, lockKey, lockValue, _logger);
}
if (waitTime.HasValue)
await Task.Delay(50, ct);
} while (waitTime.HasValue && DateTime.UtcNow < deadline && !ct.IsCancellationRequested);
_logger.LogWarning("Failed to acquire lock: {Resource}", resource);
return null;
}
private class RedisLock : IAsyncDisposable
{
private readonly IDatabase _db;
private readonly string _key;
private readonly string _value;
private readonly ILogger _logger;
public RedisLock(IDatabase db, string key, string value, ILogger logger)
{
_db = db;
_key = key;
_value = value;
_logger = logger;
}
public async ValueTask DisposeAsync()
{
await _db.LockReleaseAsync(_key, _value);
_logger.LogDebug("Lock released: {Key}", _key);
}
}
}
// EN: Usage / VI: Cách dùng
await using var lockHandle = await _lockService.AcquireAsync(
$"order:{orderId}",
TimeSpan.FromMinutes(1),
waitTime: TimeSpan.FromSeconds(5));
if (lockHandle == null)
throw new ConflictException("Order is being processed");
await ProcessOrderAsync(orderId);
```
---
## Resources / Tài Nguyên
- [StackExchange.Redis](https://stackexchange.github.io/StackExchange.Redis/)
- [Microsoft Distributed Caching](https://docs.microsoft.com/en-us/aspnet/core/performance/caching/distributed)
- [Redis Documentation](https://redis.io/documentation)

40
note.md
View File

@@ -2,46 +2,6 @@ Tài khoản: hongochai10@icloud.com
Mật Khẩu: Velik@2026
📋 Đề Xuất Agent Skills Mới cho GoodGo Platform
Skills Hiện Có (6 skills):
Skill Mô Tả
api-design RESTful API design standards
comment-code Bilingual comments EN/VI
documentation Documentation guidelines
project-rules Coding standards & architecture
security Security patterns
skill-authoring Hướng dẫn viết Agent Skills
🆕 Đề Xuất Skills Mới
1. testing-patterns
Mô tả: Unit/Integration testing patterns cho .NET microservices. Use for xUnit, NSubstitute, Testcontainers, và testing MediatR handlers.
Lý do: Dự án có thư mục tests/ trong các services nhưng chưa có skill hướng dẫn chuẩn testing.
2. repository-pattern
Mô tả: Entity Framework Core repository và data access patterns. Use for DbContext, repositories, migrations, và query optimization.
Lý do: Tất cả services sử dụng EF Core theo Clean Architecture pattern.
3. error-handling-patterns
Mô tả: Global error handling, domain exceptions, và Result pattern. Use for exception middleware, validation errors, và logging.
Lý do: Được reference trong api-design và project-rules nhưng chưa tồn tại.
4. docker-traefik
Mô tả: Docker containerization và Traefik reverse proxy. Use for Dockerfile, docker-compose, routing rules, và load balancing.
Lý do: Dự án sử dụng Traefik làm API Gateway (thư mục infra/traefik/).
5. observability
Mô tả: Monitoring, logging, và tracing patterns. Use for Prometheus metrics, Grafana dashboards, Loki logging, và distributed tracing.
Lý do: Có thư mục infra/observability/ với stack monitoring đầy đủ.
6. cqrs-mediatr
Mô tả: CQRS pattern với MediatR. Use for Commands, Queries, Handlers, Behaviors, và Pipeline.
Lý do: Tất cả controllers sử dụng MediatR pattern (Commands/Queries).
7. inter-service-communication
Mô tả: Giao tiếp giữa các microservices. Use for HttpClient, gRPC, message queues, và service discovery.
Lý do: Các services như storage-service-net gọi iam-service-net.
8. redis-caching
Mô tả: Redis caching strategies và patterns. Use for cache-aside, cache invalidation, distributed caching.
Lý do: Tech stack bao gồm Redis (StackExchange.Redis).
9. domain-driven-design
Mô tả: DDD patterns cho microservices. Use for Aggregates, Entities, Value Objects, Domain Events, và Bounded Contexts.
Lý do: Cấu trúc Domain/AggregatesModel/ cho thấy dự án tuân thủ DDD.
10. deployment-kubernetes
Mô tả: Kubernetes deployment patterns. Use for manifests, Helm charts, ConfigMaps, Secrets, và rolling updates.
Lý do: Thư mục deployments/kubernetes/ cho K8s deployment.
📊 Mức Độ Ưu Tiên
Ưu Tiên Skills Lý Do
Cao testing-patterns, repository-pattern, error-handling-patterns Core development patterns