diff --git a/.agent/skills/deployment-kubernetes/SKILL.md b/.agent/skills/deployment-kubernetes/SKILL.md new file mode 100644 index 00000000..3cf136ac --- /dev/null +++ b/.agent/skills/deployment-kubernetes/SKILL.md @@ -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 diff --git a/.agent/skills/deployment-kubernetes/references/REFERENCE.md b/.agent/skills/deployment-kubernetes/references/REFERENCE.md new file mode 100644 index 00000000..c8d35515 --- /dev/null +++ b/.agent/skills/deployment-kubernetes/references/REFERENCE.md @@ -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/) diff --git a/.agent/skills/domain-driven-design/SKILL.md b/.agent/skills/domain-driven-design/SKILL.md new file mode 100644 index 00000000..355d5534 --- /dev/null +++ b/.agent/skills/domain-driven-design/SKILL.md @@ -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 +/// +/// EN: Base class for all entities. +/// VI: Base class cho tất cả entities. +/// +public abstract class Entity +{ + private int? _requestedHashCode; + private List? _domainEvents; + + public virtual Guid Id { get; protected set; } + + public IReadOnlyCollection DomainEvents + => _domainEvents?.AsReadOnly() ?? Array.Empty().AsReadOnly(); + + public void AddDomainEvent(IDomainEvent eventItem) + { + _domainEvents ??= new List(); + _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 +/// +/// EN: Marker interface for aggregate roots. +/// VI: Interface đánh dấu aggregate roots. +/// +public interface IAggregateRoot { } + +/// +/// EN: Order aggregate root with business rules. +/// VI: Order aggregate root với business rules. +/// +public class Order : Entity, IAggregateRoot +{ + private readonly List _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 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 +/// +/// EN: Base class for value objects. +/// VI: Base class cho value objects. +/// +public abstract class ValueObject +{ + protected abstract IEnumerable 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); + } +} + +/// +/// EN: Address value object. +/// VI: Value object địa chỉ. +/// +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 GetEqualityComponents() + { + yield return Street; + yield return City; + yield return State; + yield return PostalCode; + yield return Country; + } +} + +/// +/// EN: Money value object with currency. +/// VI: Value object tiền tệ. +/// +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 GetEqualityComponents() + { + yield return Amount; + yield return Currency; + } +} +``` + +### Domain Events + +```csharp +/// +/// EN: Domain event interface. +/// VI: Interface domain event. +/// +public interface IDomainEvent : INotification +{ + Guid Id { get; } + DateTime OccurredOn { get; } +} + +/// +/// EN: Domain event when order is submitted. +/// VI: Domain event khi order được submit. +/// +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 Items { get; set; } +} + +// ✅ GOOD: Rich domain model with behavior +public class Order +{ + private readonly List _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 diff --git a/.agent/skills/domain-driven-design/references/REFERENCE.md b/.agent/skills/domain-driven-design/references/REFERENCE.md new file mode 100644 index 00000000..154bcf83 --- /dev/null +++ b/.agent/skills/domain-driven-design/references/REFERENCE.md @@ -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 +/// +/// EN: Base Entity class with domain events support. +/// VI: Base Entity class với hỗ trợ domain events. +/// +public abstract class Entity : IEquatable +{ + private readonly List _domainEvents = new(); + private int? _requestedHashCode; + + public virtual Guid Id { get; protected set; } + + public IReadOnlyCollection 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 +/// +/// EN: OrderItem entity belonging to Order aggregate. +/// VI: Entity OrderItem thuộc Order aggregate. +/// +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 +/// +/// EN: Base class for value objects with equality support. +/// VI: Base class cho value objects với hỗ trợ equality. +/// +public abstract class ValueObject : IEquatable +{ + protected abstract IEnumerable 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 +/// +/// EN: Email value object with validation. +/// VI: Value object email với validation. +/// +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 GetEqualityComponents() + { + yield return Value; + } + + public override string ToString() => Value; + + public static implicit operator string(Email email) => email.Value; +} + +/// +/// EN: DateRange value object. +/// VI: Value object khoảng thời gian. +/// +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 GetEqualityComponents() + { + yield return Start; + yield return End; + } +} + +/// +/// EN: Money value object with currency operations. +/// VI: Value object tiền tệ với các phép toán. +/// +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 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 +/// +/// EN: Order aggregate with full business logic. +/// VI: Order aggregate với business logic đầy đủ. +/// +public class Order : Entity, IAggregateRoot +{ + private readonly List _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 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 +/// +/// EN: Domain events for Order aggregate. +/// VI: Domain events cho Order aggregate. +/// +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 +/// +/// EN: Dispatch domain events via MediatR. +/// VI: Dispatch domain events qua MediatR. +/// +public class DomainEventDispatcher : IDomainEventDispatcher +{ + private readonly IMediator _mediator; + private readonly ILogger _logger; + + public async Task DispatchEventsAsync(IEnumerable 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/) diff --git a/.agent/skills/inter-service-communication/SKILL.md b/.agent/skills/inter-service-communication/SKILL.md new file mode 100644 index 00000000..edd2c789 --- /dev/null +++ b/.agent/skills/inter-service-communication/SKILL.md @@ -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 +/// +/// EN: Base interface for integration events. +/// VI: Interface cơ sở cho integration events. +/// +public interface IIntegrationEvent +{ + Guid Id { get; } + DateTime OccurredOn { get; } +} + +/// +/// EN: Event when order is created. +/// VI: Event khi order được tạo. +/// +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 Items { get; init; } = Array.Empty(); +} + +public record OrderItemInfo(Guid ProductId, int Quantity, decimal UnitPrice); +``` + +### Event Publisher with MassTransit + +```csharp +/// +/// EN: Publish integration events via MassTransit. +/// VI: Publish integration events qua MassTransit. +/// +public class IntegrationEventPublisher : IIntegrationEventPublisher +{ + private readonly IPublishEndpoint _publishEndpoint; + private readonly ILogger _logger; + + public IntegrationEventPublisher( + IPublishEndpoint publishEndpoint, + ILogger logger) + { + _publishEndpoint = publishEndpoint; + _logger = logger; + } + + public async Task PublishAsync(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 +{ + private readonly IOrderRepository _orderRepository; + private readonly IIntegrationEventPublisher _eventPublisher; + + public async Task 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 +/// +/// EN: Consumer for OrderCreatedIntegrationEvent in Inventory Service. +/// VI: Consumer cho OrderCreatedIntegrationEvent trong Inventory Service. +/// +public class OrderCreatedConsumer : IConsumer +{ + private readonly IInventoryService _inventoryService; + private readonly ILogger _logger; + + public OrderCreatedConsumer( + IInventoryService inventoryService, + ILogger logger) + { + _inventoryService = inventoryService; + _logger = logger; + } + + public async Task Consume(ConsumeContext 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 +/// +/// EN: Configure MassTransit with RabbitMQ. +/// VI: Cấu hình MassTransit với RabbitMQ. +/// + +// Program.cs +builder.Services.AddMassTransit(x => +{ + // EN: Register consumers + // VI: Đăng ký consumers + x.AddConsumer(); + x.AddConsumer(); + + 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 +/// +/// 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ụ. +/// + +// Program.cs +builder.Services.AddHttpClient(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() + .HandleResult(r => (int)r.StatusCode >= 500) + .Handle() + }); + + // 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 context) +{ + await _service.CreateShipment(context.Message.OrderId); +} + +// ✅ GOOD: Idempotent processing +public async Task Consume(ConsumeContext 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 diff --git a/.agent/skills/inter-service-communication/references/REFERENCE.md b/.agent/skills/inter-service-communication/references/REFERENCE.md new file mode 100644 index 00000000..7c75f0af --- /dev/null +++ b/.agent/skills/inter-service-communication/references/REFERENCE.md @@ -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 +/// +/// EN: Configure MassTransit with RabbitMQ for production. +/// VI: Cấu hình MassTransit với RabbitMQ cho production. +/// + +// 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() + // .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(); + }); + + // 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 +/// +/// EN: Integration event base record. +/// VI: Integration event record cơ sở. +/// +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; +} + +/// +/// EN: Event when order status changes. +/// VI: Event khi trạng thái order thay đổi. +/// +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!; +} + +/// +/// EN: Event when product price changes. +/// VI: Event khi giá sản phẩm thay đổi. +/// +public record ProductPriceChangedIntegrationEvent : IntegrationEvent +{ + public Guid ProductId { get; init; } + public decimal OldPrice { get; init; } + public decimal NewPrice { get; init; } +} + +/// +/// EN: Event when payment is completed. +/// VI: Event khi thanh toán hoàn thành. +/// +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 +/// +/// EN: Service to publish integration events. +/// VI: Service publish integration events. +/// +public interface IEventPublisher +{ + Task PublishAsync(T @event, CancellationToken ct = default) where T : class; +} + +public class MassTransitEventPublisher : IEventPublisher +{ + private readonly IPublishEndpoint _publishEndpoint; + private readonly ILogger _logger; + + public MassTransitEventPublisher( + IPublishEndpoint publishEndpoint, + ILogger logger) + { + _publishEndpoint = publishEndpoint; + _logger = logger; + } + + public async Task PublishAsync(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 +/// +/// EN: Idempotent consumer for payment completed events. +/// VI: Consumer idempotent cho payment completed events. +/// +public class PaymentCompletedConsumer : IConsumer +{ + private readonly IOrderService _orderService; + private readonly IIdempotencyService _idempotency; + private readonly ILogger _logger; + + public PaymentCompletedConsumer( + IOrderService orderService, + IIdempotencyService idempotency, + ILogger logger) + { + _orderService = orderService; + _idempotency = idempotency; + _logger = logger; + } + + public async Task Consume(ConsumeContext 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 +/// +/// EN: Consumer definition with custom retry and concurrency. +/// VI: Consumer definition với retry và concurrency tùy chỉnh. +/// +public class PaymentCompletedConsumerDefinition + : ConsumerDefinition +{ + 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 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(); + }); + + // 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 +/// +/// EN: Typed HTTP client for IAM service. +/// VI: Typed HTTP client cho IAM service. +/// +public interface IIamServiceClient +{ + Task GetUserAsync(string userId, CancellationToken ct = default); + Task ValidateTokenAsync(string token, CancellationToken ct = default); +} + +public class IamServiceClient : IIamServiceClient +{ + private readonly HttpClient _httpClient; + private readonly ILogger _logger; + + public IamServiceClient( + HttpClient httpClient, + ILogger logger) + { + _httpClient = httpClient; + _logger = logger; + } + + public async Task 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>(ct); + return result?.Data; + } + catch (HttpRequestException ex) + { + _logger.LogError(ex, "Failed to get user {UserId} from IAM service", userId); + throw; + } + } + + public async Task 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 +/// +/// EN: Register resilient HTTP clients. +/// VI: Đăng ký HTTP clients có khả năng phục hồi. +/// + +builder.Services.AddHttpClient(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() + .HandleResult(r => r.StatusCode >= HttpStatusCode.InternalServerError) + .Handle() + .Handle() + }); + + 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 +/// +/// EN: Outbox message entity for reliable event publishing. +/// VI: Outbox message entity cho event publishing đáng tin cậy. +/// +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(entity => +{ + entity.ToTable("OutboxMessages"); + entity.HasKey(e => e.Id); + entity.HasIndex(e => e.ProcessedOn); +}); +``` + +### Outbox Processor + +```csharp +/// +/// EN: Background service to process outbox messages. +/// VI: Background service xử lý outbox messages. +/// +public class OutboxProcessor : BackgroundService +{ + private readonly IServiceProvider _serviceProvider; + private readonly ILogger _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(); + var publisher = scope.ServiceProvider.GetRequiredService(); + + 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/) diff --git a/.agent/skills/redis-caching/SKILL.md b/.agent/skills/redis-caching/SKILL.md new file mode 100644 index 00000000..a3835b41 --- /dev/null +++ b/.agent/skills/redis-caching/SKILL.md @@ -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 +/// +/// EN: Configure Redis distributed cache. +/// VI: Cấu hình Redis distributed cache. +/// + +// 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(sp => + ConnectionMultiplexer.Connect(builder.Configuration["Redis:ConnectionString"]!)); +``` + +### Cache Service Implementation + +```csharp +/// +/// EN: Generic cache service with typed operations. +/// VI: Cache service generic với typed operations. +/// +public interface ICacheService +{ + Task GetAsync(string key, CancellationToken ct = default); + Task SetAsync(string key, T value, TimeSpan? expiry = null, CancellationToken ct = default); + Task RemoveAsync(string key, CancellationToken ct = default); + Task GetOrSetAsync(string key, Func> factory, TimeSpan? expiry = null, CancellationToken ct = default); +} + +public class RedisCacheService : ICacheService +{ + private readonly IDistributedCache _cache; + private readonly ILogger _logger; + private static readonly TimeSpan DefaultExpiry = TimeSpan.FromMinutes(30); + + public RedisCacheService( + IDistributedCache cache, + ILogger logger) + { + _cache = cache; + _logger = logger; + } + + public async Task GetAsync(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(data); + } + + public async Task SetAsync( + 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 GetOrSetAsync( + string key, + Func> factory, + TimeSpan? expiry = null, + CancellationToken ct = default) + { + var cached = await GetAsync(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 +/// +/// EN: Query handler with cache-aside pattern. +/// VI: Query handler với cache-aside pattern. +/// +public class GetUserProfileQueryHandler + : IRequestHandler +{ + private readonly ICacheService _cache; + private readonly IUserRepository _userRepository; + private static readonly TimeSpan CacheExpiry = TimeSpan.FromMinutes(15); + + public async Task 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 +/// +/// EN: Invalidate cache when data changes. +/// VI: Xóa cache khi dữ liệu thay đổi. +/// +public class UpdateUserProfileCommandHandler + : IRequestHandler +{ + private readonly ICacheService _cache; + private readonly IUserRepository _userRepository; + + public async Task 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 +/// +/// EN: Shopping cart repository using Redis Hash. +/// VI: Repository giỏ hàng dùng Redis Hash. +/// +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 GetAsync(string cartId) + { + var data = await _redis.StringGetAsync($"cart:{cartId}"); + return data.IsNullOrEmpty + ? null + : JsonSerializer.Deserialize(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(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 { 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 diff --git a/.agent/skills/redis-caching/references/REFERENCE.md b/.agent/skills/redis-caching/references/REFERENCE.md new file mode 100644 index 00000000..eb2b7748 --- /dev/null +++ b/.agent/skills/redis-caching/references/REFERENCE.md @@ -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 +/// +/// 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. +/// + +// 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(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 +/// +/// 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. +/// +public class RedisCacheService : ICacheService +{ + private readonly IDistributedCache _cache; + private readonly IConnectionMultiplexer _redis; + private readonly ILogger _logger; + private readonly JsonSerializerOptions _jsonOptions; + + public RedisCacheService( + IDistributedCache cache, + IConnectionMultiplexer redis, + ILogger logger) + { + _cache = cache; + _redis = redis; + _logger = logger; + _jsonOptions = new JsonSerializerOptions + { + PropertyNamingPolicy = JsonNamingPolicy.CamelCase, + DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull + }; + } + + public async Task GetAsync(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(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( + 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 GetOrSetAsync( + string key, + Func> factory, + TimeSpan? expiry = null, + CancellationToken ct = default) + { + var cached = await GetAsync(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(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 +/// +/// EN: Shopping cart stored in Redis. +/// VI: Giỏ hàng lưu trong Redis. +/// +public class Cart +{ + public string Id { get; set; } = default!; + public string UserId { get; set; } = default!; + public List 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 +/// +/// EN: Redis-backed cart repository. +/// VI: Repository giỏ hàng với Redis. +/// +public class RedisCartRepository : ICartRepository +{ + private readonly IDatabase _db; + private readonly ILogger _logger; + private static readonly TimeSpan DefaultExpiry = TimeSpan.FromDays(7); + + public RedisCartRepository( + IConnectionMultiplexer redis, + ILogger logger) + { + _db = redis.GetDatabase(); + _logger = logger; + } + + public async Task GetByUserIdAsync(string userId, CancellationToken ct = default) + { + var key = GetKey(userId); + var data = await _db.StringGetAsync(key); + + if (data.IsNullOrEmpty) + return null; + + return JsonSerializer.Deserialize(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 +/// +/// EN: Redis-based sliding window rate limiter. +/// VI: Rate limiter sliding window với Redis. +/// +public class RedisRateLimiter : IRateLimiter +{ + private readonly IDatabase _db; + private readonly ILogger _logger; + + public RedisRateLimiter( + IConnectionMultiplexer redis, + ILogger logger) + { + _db = redis.GetDatabase(); + _logger = logger; + } + + public async Task 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 +/// +/// EN: Distributed lock using Redis. +/// VI: Distributed lock dùng Redis. +/// +public class RedisLockService : IDistributedLockService +{ + private readonly IDatabase _db; + private readonly ILogger _logger; + + public async Task 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) diff --git a/note.md b/note.md index 8cdd569b..7e5eca21 100644 --- a/note.md +++ b/note.md @@ -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