diff --git a/services/mkt-facebook-service-net/.env.example b/services/mkt-facebook-service-net/.env.example
new file mode 100644
index 00000000..f9053bc3
--- /dev/null
+++ b/services/mkt-facebook-service-net/.env.example
@@ -0,0 +1,40 @@
+# Environment / Môi Trường
+ASPNETCORE_ENVIRONMENT=Development
+
+# Database / Cơ Sở Dữ Liệu
+# PostgreSQL connection string (Neon or local)
+DATABASE_URL=Host=localhost;Port=5432;Database=myservice_db;Username=postgres;Password=postgres
+
+# Redis Cache
+REDIS_URL=localhost:6379
+REDIS_PASSWORD=
+
+# JWT Authentication / Xác Thực JWT
+JWT_SECRET=your-secret-key-min-32-characters-long-here
+JWT_ISSUER=goodgo-platform
+JWT_AUDIENCE=goodgo-services
+JWT_ACCESS_TOKEN_EXPIRY_MINUTES=15
+JWT_REFRESH_TOKEN_EXPIRY_DAYS=7
+
+# API Configuration / Cấu Hình API
+API_PORT=5000
+API_BASE_PATH=/api/v1/myservice
+
+# Observability / Quan Sát
+OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4317
+OTEL_SERVICE_NAME=myservice
+
+# Logging
+LOG_LEVEL=Information
+SEQ_URL=http://localhost:5341
+
+# Feature Flags
+FEATURE_SWAGGER_ENABLED=true
+FEATURE_DETAILED_ERRORS=true
+
+# Rate Limiting
+RATE_LIMIT_PERMITS_PER_MINUTE=100
+RATE_LIMIT_QUEUE_LIMIT=10
+
+# Health Checks
+HEALTHCHECK_TIMEOUT_SECONDS=5
diff --git a/services/mkt-facebook-service-net/.gitignore b/services/mkt-facebook-service-net/.gitignore
new file mode 100644
index 00000000..84b02a53
--- /dev/null
+++ b/services/mkt-facebook-service-net/.gitignore
@@ -0,0 +1,75 @@
+# Build results
+[Bb]in/
+[Oo]bj/
+[Ll]og/
+[Ll]ogs/
+
+# Visual Studio
+.vs/
+*.user
+*.userosscache
+*.suo
+*.userprefs
+*.sln.docstates
+
+# Rider
+.idea/
+*.sln.iml
+
+# Visual Studio Code
+.vscode/
+
+# NuGet
+*.nupkg
+*.snupkg
+.nuget/
+packages/
+project.lock.json
+project.fragment.lock.json
+
+# MSTest test Results
+[Tt]est[Rr]esult*/
+[Bb]uild[Ll]og.*
+
+# Coverage
+TestResults/
+*.coverage
+*.coveragexml
+coverage*.json
+coverage*.xml
+
+# Publish output
+publish/
+out/
+
+# Environment files
+.env
+.env.local
+.env.*.local
+*.env
+
+# Secrets
+appsettings.*.json
+!appsettings.json
+!appsettings.Development.json
+
+# macOS
+.DS_Store
+
+# Windows
+Thumbs.db
+ehthumbs.db
+
+# JetBrains
+*.resharper
+
+# dotnet tools
+.config/dotnet-tools.json
+
+# Migration scripts (only keep structure)
+Migrations/
+
+# Temp files
+*.tmp
+*.temp
+~$*
diff --git a/services/mkt-facebook-service-net/Directory.Build.props b/services/mkt-facebook-service-net/Directory.Build.props
new file mode 100644
index 00000000..c3b74373
--- /dev/null
+++ b/services/mkt-facebook-service-net/Directory.Build.props
@@ -0,0 +1,22 @@
+
+
+ net10.0
+ 14.0
+ enable
+ enable
+ true
+ true
+ $(NoWarn);1591;CA2017
+
+
+
+ GoodGo Team
+ GoodGo
+ © 2026 GoodGo. All rights reserved.
+ git
+
+
+
+
+
+
diff --git a/services/mkt-facebook-service-net/Dockerfile b/services/mkt-facebook-service-net/Dockerfile
new file mode 100644
index 00000000..192106ab
--- /dev/null
+++ b/services/mkt-facebook-service-net/Dockerfile
@@ -0,0 +1,66 @@
+# Build stage / Giai đoạn build
+FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build
+WORKDIR /src
+
+# EN: Copy project files for layer caching
+# VI: Sao chép các file project để tận dụng layer caching
+COPY ["src/MyService.API/MyService.API.csproj", "src/MyService.API/"]
+COPY ["src/MyService.Domain/MyService.Domain.csproj", "src/MyService.Domain/"]
+COPY ["src/MyService.Infrastructure/MyService.Infrastructure.csproj", "src/MyService.Infrastructure/"]
+COPY ["Directory.Build.props", "./"]
+
+# EN: Restore dependencies
+# VI: Khôi phục dependencies
+RUN dotnet restore "src/MyService.API/MyService.API.csproj"
+
+# EN: Copy all source code
+# VI: Sao chép toàn bộ source code
+COPY src/ ./src/
+
+# EN: Build the application
+# VI: Build ứng dụng
+WORKDIR "/src/src/MyService.API"
+RUN dotnet build "MyService.API.csproj" -c Release -o /app/build --no-restore
+
+# Publish stage / Giai đoạn publish
+FROM build AS publish
+RUN dotnet publish "MyService.API.csproj" -c Release -o /app/publish /p:UseAppHost=false --no-restore
+
+# Runtime stage / Giai đoạn runtime
+FROM mcr.microsoft.com/dotnet/aspnet:10.0 AS final
+WORKDIR /app
+
+# EN: Create non-root user for security
+# VI: Tạo user non-root cho bảo mật
+RUN groupadd -g 1001 dotnetuser && \
+ useradd -u 1001 -g dotnetuser -s /bin/sh dotnetuser
+
+# EN: Copy published application
+# VI: Sao chép ứng dụng đã publish
+COPY --from=publish /app/publish .
+
+# EN: Change ownership to non-root user
+# VI: Thay đổi quyền sở hữu sang user non-root
+RUN chown -R dotnetuser:dotnetuser /app
+
+# EN: Switch to non-root user
+# VI: Chuyển sang user non-root
+USER dotnetuser
+
+# EN: Expose port
+# VI: Mở cổng
+EXPOSE 8080
+
+# EN: Set environment variables
+# VI: Thiết lập biến môi trường
+ENV ASPNETCORE_URLS=http://+:8080
+ENV ASPNETCORE_ENVIRONMENT=Production
+
+# EN: Health check
+# VI: Kiểm tra health
+HEALTHCHECK --interval=30s --timeout=3s --start-period=10s --retries=3 \
+ CMD curl -f http://localhost:8080/health/live || exit 1
+
+# EN: Start the application
+# VI: Khởi động ứng dụng
+ENTRYPOINT ["dotnet", "MyService.API.dll"]
diff --git a/services/mkt-facebook-service-net/FacebookService.slnx b/services/mkt-facebook-service-net/FacebookService.slnx
new file mode 100644
index 00000000..b9980c53
--- /dev/null
+++ b/services/mkt-facebook-service-net/FacebookService.slnx
@@ -0,0 +1,11 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/services/mkt-facebook-service-net/docker-compose.yml b/services/mkt-facebook-service-net/docker-compose.yml
new file mode 100644
index 00000000..254ceb12
--- /dev/null
+++ b/services/mkt-facebook-service-net/docker-compose.yml
@@ -0,0 +1,72 @@
+version: '3.8'
+
+# EN: Docker Compose for local development
+# VI: Docker Compose cho phát triển local
+
+services:
+ myservice-api:
+ build:
+ context: .
+ dockerfile: Dockerfile
+ container_name: myservice-api
+ ports:
+ - "5000:8080"
+ environment:
+ - ASPNETCORE_ENVIRONMENT=Development
+ - DATABASE_URL=Host=postgres;Port=5432;Database=myservice_db;Username=postgres;Password=postgres
+ - REDIS_URL=redis:6379
+ depends_on:
+ postgres:
+ condition: service_healthy
+ redis:
+ condition: service_healthy
+ networks:
+ - myservice-network
+ healthcheck:
+ test: ["CMD", "curl", "-f", "http://localhost:8080/health/live"]
+ interval: 30s
+ timeout: 10s
+ retries: 3
+ start_period: 10s
+
+ postgres:
+ image: postgres:16-alpine
+ container_name: myservice-postgres
+ environment:
+ POSTGRES_USER: postgres
+ POSTGRES_PASSWORD: postgres
+ POSTGRES_DB: myservice_db
+ ports:
+ - "5432:5432"
+ volumes:
+ - postgres_data:/var/lib/postgresql/data
+ networks:
+ - myservice-network
+ healthcheck:
+ test: ["CMD-SHELL", "pg_isready -U postgres"]
+ interval: 10s
+ timeout: 5s
+ retries: 5
+
+ redis:
+ image: redis:7-alpine
+ container_name: myservice-redis
+ ports:
+ - "6379:6379"
+ volumes:
+ - redis_data:/data
+ networks:
+ - myservice-network
+ healthcheck:
+ test: ["CMD", "redis-cli", "ping"]
+ interval: 10s
+ timeout: 5s
+ retries: 5
+
+volumes:
+ postgres_data:
+ redis_data:
+
+networks:
+ myservice-network:
+ driver: bridge
diff --git a/services/mkt-facebook-service-net/docs_backup_20260119_003109.tar.gz b/services/mkt-facebook-service-net/docs_backup_20260119_003109.tar.gz
new file mode 100644
index 00000000..2f7695cf
Binary files /dev/null and b/services/mkt-facebook-service-net/docs_backup_20260119_003109.tar.gz differ
diff --git a/services/mkt-facebook-service-net/global.json b/services/mkt-facebook-service-net/global.json
new file mode 100644
index 00000000..f78eeaf4
--- /dev/null
+++ b/services/mkt-facebook-service-net/global.json
@@ -0,0 +1,7 @@
+{
+ "sdk": {
+ "version": "10.0.101",
+ "rollForward": "latestMinor",
+ "allowPrerelease": false
+ }
+}
\ No newline at end of file
diff --git a/services/mkt-facebook-service-net/src/FacebookService.API/Application/Behaviors/LoggingBehavior.cs b/services/mkt-facebook-service-net/src/FacebookService.API/Application/Behaviors/LoggingBehavior.cs
new file mode 100644
index 00000000..14264122
--- /dev/null
+++ b/services/mkt-facebook-service-net/src/FacebookService.API/Application/Behaviors/LoggingBehavior.cs
@@ -0,0 +1,58 @@
+using System.Diagnostics;
+using MediatR;
+
+namespace FacebookService.API.Application.Behaviors;
+
+///
+/// EN: MediatR behavior for logging request handling.
+/// VI: MediatR behavior để logging việc xử lý request.
+///
+/// EN: Request type / VI: Loại request
+/// EN: Response type / VI: Loại response
+public class LoggingBehavior : IPipelineBehavior
+ where TRequest : IRequest
+{
+ private readonly ILogger> _logger;
+
+ public LoggingBehavior(ILogger> logger)
+ {
+ _logger = logger ?? throw new ArgumentNullException(nameof(logger));
+ }
+
+ public async Task Handle(
+ TRequest request,
+ RequestHandlerDelegate next,
+ CancellationToken cancellationToken)
+ {
+ var requestName = typeof(TRequest).Name;
+
+ _logger.LogInformation(
+ "Handling {RequestName} / Đang xử lý {RequestName}",
+ requestName);
+
+ var stopwatch = Stopwatch.StartNew();
+
+ try
+ {
+ var response = await next();
+
+ stopwatch.Stop();
+
+ _logger.LogInformation(
+ "Handled {RequestName} in {ElapsedMs}ms / Đã xử lý {RequestName} trong {ElapsedMs}ms",
+ requestName, stopwatch.ElapsedMilliseconds);
+
+ return response;
+ }
+ catch (Exception ex)
+ {
+ stopwatch.Stop();
+
+ _logger.LogError(ex,
+ "Error handling {RequestName} after {ElapsedMs}ms / Lỗi xử lý {RequestName} sau {ElapsedMs}ms",
+ requestName, stopwatch.ElapsedMilliseconds);
+
+ throw;
+ }
+ }
+}
diff --git a/services/mkt-facebook-service-net/src/FacebookService.API/Application/Behaviors/TransactionBehavior.cs b/services/mkt-facebook-service-net/src/FacebookService.API/Application/Behaviors/TransactionBehavior.cs
new file mode 100644
index 00000000..edfe5224
--- /dev/null
+++ b/services/mkt-facebook-service-net/src/FacebookService.API/Application/Behaviors/TransactionBehavior.cs
@@ -0,0 +1,84 @@
+using MediatR;
+using Microsoft.EntityFrameworkCore;
+using FacebookService.Infrastructure;
+
+namespace FacebookService.API.Application.Behaviors;
+
+///
+/// EN: MediatR behavior for handling database transactions.
+/// VI: MediatR behavior để xử lý database transactions.
+///
+/// EN: Request type / VI: Loại request
+/// EN: Response type / VI: Loại response
+public class TransactionBehavior : IPipelineBehavior
+ where TRequest : IRequest
+{
+ private readonly FacebookServiceContext _dbContext;
+ private readonly ILogger> _logger;
+
+ public TransactionBehavior(
+ FacebookServiceContext dbContext,
+ ILogger> logger)
+ {
+ _dbContext = dbContext ?? throw new ArgumentNullException(nameof(dbContext));
+ _logger = logger ?? throw new ArgumentNullException(nameof(logger));
+ }
+
+ public async Task Handle(
+ TRequest request,
+ RequestHandlerDelegate next,
+ CancellationToken cancellationToken)
+ {
+ var requestName = typeof(TRequest).Name;
+
+ // EN: Skip transaction for queries (read operations)
+ // VI: Bỏ qua transaction cho queries (các thao tác đọc)
+ if (requestName.EndsWith("Query"))
+ {
+ return await next();
+ }
+
+ // EN: Skip if already in a transaction
+ // VI: Bỏ qua nếu đã trong transaction
+ if (_dbContext.HasActiveTransaction)
+ {
+ return await next();
+ }
+
+ var strategy = _dbContext.Database.CreateExecutionStrategy();
+
+ return await strategy.ExecuteAsync(async () =>
+ {
+ await using var transaction = await _dbContext.BeginTransactionAsync();
+
+ _logger.LogInformation(
+ "Begin transaction {TransactionId} for {RequestName} / Bắt đầu transaction {TransactionId} cho {RequestName}",
+ transaction?.TransactionId, requestName);
+
+ try
+ {
+ var response = await next();
+
+ if (transaction != null)
+ {
+ await _dbContext.CommitTransactionAsync(transaction);
+
+ _logger.LogInformation(
+ "Committed transaction {TransactionId} for {RequestName} / Đã commit transaction {TransactionId} cho {RequestName}",
+ transaction.TransactionId, requestName);
+ }
+
+ return response;
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError(ex,
+ "Error during transaction {TransactionId} for {RequestName} / Lỗi trong transaction {TransactionId} cho {RequestName}",
+ transaction?.TransactionId, requestName);
+
+ _dbContext.RollbackTransaction();
+ throw;
+ }
+ });
+ }
+}
diff --git a/services/mkt-facebook-service-net/src/FacebookService.API/Application/Behaviors/ValidatorBehavior.cs b/services/mkt-facebook-service-net/src/FacebookService.API/Application/Behaviors/ValidatorBehavior.cs
new file mode 100644
index 00000000..3c105605
--- /dev/null
+++ b/services/mkt-facebook-service-net/src/FacebookService.API/Application/Behaviors/ValidatorBehavior.cs
@@ -0,0 +1,63 @@
+using FluentValidation;
+using MediatR;
+
+namespace FacebookService.API.Application.Behaviors;
+
+///
+/// EN: MediatR behavior for FluentValidation integration.
+/// VI: MediatR behavior để tích hợp FluentValidation.
+///
+/// EN: Request type / VI: Loại request
+/// EN: Response type / VI: Loại response
+public class ValidatorBehavior : IPipelineBehavior
+ where TRequest : IRequest
+{
+ private readonly IEnumerable> _validators;
+ private readonly ILogger> _logger;
+
+ public ValidatorBehavior(
+ IEnumerable> validators,
+ ILogger> logger)
+ {
+ _validators = validators ?? throw new ArgumentNullException(nameof(validators));
+ _logger = logger ?? throw new ArgumentNullException(nameof(logger));
+ }
+
+ public async Task Handle(
+ TRequest request,
+ RequestHandlerDelegate next,
+ CancellationToken cancellationToken)
+ {
+ var requestName = typeof(TRequest).Name;
+
+ if (!_validators.Any())
+ {
+ return await next();
+ }
+
+ _logger.LogDebug(
+ "Validating {RequestName} / Đang validate {RequestName}",
+ requestName);
+
+ var context = new ValidationContext(request);
+
+ var validationResults = await Task.WhenAll(
+ _validators.Select(v => v.ValidateAsync(context, cancellationToken)));
+
+ var failures = validationResults
+ .SelectMany(r => r.Errors)
+ .Where(f => f != null)
+ .ToList();
+
+ if (failures.Count != 0)
+ {
+ _logger.LogWarning(
+ "Validation failed for {RequestName} with {ErrorCount} errors / Validation thất bại cho {RequestName} với {ErrorCount} lỗi",
+ requestName, failures.Count);
+
+ throw new ValidationException(failures);
+ }
+
+ return await next();
+ }
+}
diff --git a/services/mkt-facebook-service-net/src/FacebookService.API/Application/Commands/ChangeSampleStatusCommand.cs b/services/mkt-facebook-service-net/src/FacebookService.API/Application/Commands/ChangeSampleStatusCommand.cs
new file mode 100644
index 00000000..18e60890
--- /dev/null
+++ b/services/mkt-facebook-service-net/src/FacebookService.API/Application/Commands/ChangeSampleStatusCommand.cs
@@ -0,0 +1,14 @@
+using MediatR;
+
+namespace FacebookService.API.Application.Commands;
+
+///
+/// EN: Command to change status of a Sample.
+/// VI: Command để thay đổi trạng thái của Sample.
+///
+/// EN: Sample ID / VI: ID sample
+/// EN: New status (activate, complete, cancel) / VI: Trạng thái mới (activate, complete, cancel)
+public record ChangeSampleStatusCommand(
+ Guid SampleId,
+ string NewStatus
+) : IRequest;
diff --git a/services/mkt-facebook-service-net/src/FacebookService.API/Application/Commands/ChangeSampleStatusCommandHandler.cs b/services/mkt-facebook-service-net/src/FacebookService.API/Application/Commands/ChangeSampleStatusCommandHandler.cs
new file mode 100644
index 00000000..91555c76
--- /dev/null
+++ b/services/mkt-facebook-service-net/src/FacebookService.API/Application/Commands/ChangeSampleStatusCommandHandler.cs
@@ -0,0 +1,70 @@
+using MediatR;
+using FacebookService.Domain.AggregatesModel.SampleAggregate;
+
+namespace FacebookService.API.Application.Commands;
+
+///
+/// EN: Handler for ChangeSampleStatusCommand.
+/// VI: Handler cho ChangeSampleStatusCommand.
+///
+public class ChangeSampleStatusCommandHandler : IRequestHandler
+{
+ private readonly ISampleRepository _sampleRepository;
+ private readonly ILogger _logger;
+
+ public ChangeSampleStatusCommandHandler(
+ ISampleRepository sampleRepository,
+ ILogger logger)
+ {
+ _sampleRepository = sampleRepository ?? throw new ArgumentNullException(nameof(sampleRepository));
+ _logger = logger ?? throw new ArgumentNullException(nameof(logger));
+ }
+
+ public async Task Handle(
+ ChangeSampleStatusCommand request,
+ CancellationToken cancellationToken)
+ {
+ _logger.LogInformation(
+ "Changing status of sample {SampleId} to {NewStatus} / Thay đổi trạng thái sample {SampleId} thành {NewStatus}",
+ request.SampleId, request.NewStatus);
+
+ // EN: Get existing sample / VI: Lấy sample đã tồn tại
+ var sample = await _sampleRepository.GetAsync(request.SampleId);
+
+ if (sample is null)
+ {
+ _logger.LogWarning(
+ "Sample {SampleId} not found / Sample {SampleId} không tìm thấy",
+ request.SampleId);
+ return false;
+ }
+
+ // EN: Change status based on action / VI: Thay đổi trạng thái dựa trên action
+ switch (request.NewStatus.ToLowerInvariant())
+ {
+ case "activate":
+ sample.Activate();
+ break;
+ case "complete":
+ sample.Complete();
+ break;
+ case "cancel":
+ sample.Cancel();
+ break;
+ default:
+ _logger.LogWarning(
+ "Invalid status action: {NewStatus} / Action trạng thái không hợp lệ: {NewStatus}",
+ request.NewStatus);
+ return false;
+ }
+
+ // EN: Save changes / VI: Lưu thay đổi
+ await _sampleRepository.UnitOfWork.SaveEntitiesAsync(cancellationToken);
+
+ _logger.LogInformation(
+ "Sample {SampleId} status changed to {NewStatus} / Trạng thái sample {SampleId} đã đổi thành {NewStatus}",
+ request.SampleId, request.NewStatus);
+
+ return true;
+ }
+}
diff --git a/services/mkt-facebook-service-net/src/FacebookService.API/Application/Commands/CreateSampleCommand.cs b/services/mkt-facebook-service-net/src/FacebookService.API/Application/Commands/CreateSampleCommand.cs
new file mode 100644
index 00000000..9782f000
--- /dev/null
+++ b/services/mkt-facebook-service-net/src/FacebookService.API/Application/Commands/CreateSampleCommand.cs
@@ -0,0 +1,21 @@
+using MediatR;
+
+namespace FacebookService.API.Application.Commands;
+
+///
+/// EN: Command to create a new Sample.
+/// VI: Command để tạo một Sample mới.
+///
+/// EN: Sample name / VI: Tên sample
+/// EN: Optional description / VI: Mô tả tùy chọn
+public record CreateSampleCommand(
+ string Name,
+ string? Description
+) : IRequest;
+
+///
+/// EN: Result of CreateSampleCommand.
+/// VI: Kết quả của CreateSampleCommand.
+///
+/// EN: Created sample ID / VI: ID sample đã tạo
+public record CreateSampleCommandResult(Guid Id);
diff --git a/services/mkt-facebook-service-net/src/FacebookService.API/Application/Commands/CreateSampleCommandHandler.cs b/services/mkt-facebook-service-net/src/FacebookService.API/Application/Commands/CreateSampleCommandHandler.cs
new file mode 100644
index 00000000..524bee64
--- /dev/null
+++ b/services/mkt-facebook-service-net/src/FacebookService.API/Application/Commands/CreateSampleCommandHandler.cs
@@ -0,0 +1,46 @@
+using MediatR;
+using FacebookService.Domain.AggregatesModel.SampleAggregate;
+
+namespace FacebookService.API.Application.Commands;
+
+///
+/// EN: Handler for CreateSampleCommand.
+/// VI: Handler cho CreateSampleCommand.
+///
+public class CreateSampleCommandHandler : IRequestHandler
+{
+ private readonly ISampleRepository _sampleRepository;
+ private readonly ILogger _logger;
+
+ public CreateSampleCommandHandler(
+ ISampleRepository sampleRepository,
+ ILogger logger)
+ {
+ _sampleRepository = sampleRepository ?? throw new ArgumentNullException(nameof(sampleRepository));
+ _logger = logger ?? throw new ArgumentNullException(nameof(logger));
+ }
+
+ public async Task Handle(
+ CreateSampleCommand request,
+ CancellationToken cancellationToken)
+ {
+ _logger.LogInformation(
+ "Creating new sample with name: {Name} / Tạo sample mới với tên: {Name}",
+ request.Name);
+
+ // EN: Create domain entity / VI: Tạo domain entity
+ var sample = new Sample(request.Name, request.Description);
+
+ // EN: Add to repository / VI: Thêm vào repository
+ _sampleRepository.Add(sample);
+
+ // EN: Save changes (dispatches domain events) / VI: Lưu thay đổi (dispatch domain events)
+ await _sampleRepository.UnitOfWork.SaveEntitiesAsync(cancellationToken);
+
+ _logger.LogInformation(
+ "Sample created successfully with ID: {SampleId} / Sample đã tạo thành công với ID: {SampleId}",
+ sample.Id);
+
+ return new CreateSampleCommandResult(sample.Id);
+ }
+}
diff --git a/services/mkt-facebook-service-net/src/FacebookService.API/Application/Commands/DeleteSampleCommand.cs b/services/mkt-facebook-service-net/src/FacebookService.API/Application/Commands/DeleteSampleCommand.cs
new file mode 100644
index 00000000..eea27daf
--- /dev/null
+++ b/services/mkt-facebook-service-net/src/FacebookService.API/Application/Commands/DeleteSampleCommand.cs
@@ -0,0 +1,10 @@
+using MediatR;
+
+namespace FacebookService.API.Application.Commands;
+
+///
+/// EN: Command to delete a Sample.
+/// VI: Command để xóa một Sample.
+///
+/// EN: Sample ID to delete / VI: ID sample cần xóa
+public record DeleteSampleCommand(Guid SampleId) : IRequest;
diff --git a/services/mkt-facebook-service-net/src/FacebookService.API/Application/Commands/DeleteSampleCommandHandler.cs b/services/mkt-facebook-service-net/src/FacebookService.API/Application/Commands/DeleteSampleCommandHandler.cs
new file mode 100644
index 00000000..c46c54a2
--- /dev/null
+++ b/services/mkt-facebook-service-net/src/FacebookService.API/Application/Commands/DeleteSampleCommandHandler.cs
@@ -0,0 +1,54 @@
+using MediatR;
+using FacebookService.Domain.AggregatesModel.SampleAggregate;
+
+namespace FacebookService.API.Application.Commands;
+
+///
+/// EN: Handler for DeleteSampleCommand.
+/// VI: Handler cho DeleteSampleCommand.
+///
+public class DeleteSampleCommandHandler : IRequestHandler
+{
+ private readonly ISampleRepository _sampleRepository;
+ private readonly ILogger _logger;
+
+ public DeleteSampleCommandHandler(
+ ISampleRepository sampleRepository,
+ ILogger logger)
+ {
+ _sampleRepository = sampleRepository ?? throw new ArgumentNullException(nameof(sampleRepository));
+ _logger = logger ?? throw new ArgumentNullException(nameof(logger));
+ }
+
+ public async Task Handle(
+ DeleteSampleCommand request,
+ CancellationToken cancellationToken)
+ {
+ _logger.LogInformation(
+ "Deleting sample {SampleId} / Xóa sample {SampleId}",
+ request.SampleId);
+
+ // EN: Get existing sample / VI: Lấy sample đã tồn tại
+ var sample = await _sampleRepository.GetAsync(request.SampleId);
+
+ if (sample is null)
+ {
+ _logger.LogWarning(
+ "Sample {SampleId} not found / Sample {SampleId} không tìm thấy",
+ request.SampleId);
+ return false;
+ }
+
+ // EN: Delete sample / VI: Xóa sample
+ _sampleRepository.Delete(sample);
+
+ // EN: Save changes / VI: Lưu thay đổi
+ await _sampleRepository.UnitOfWork.SaveEntitiesAsync(cancellationToken);
+
+ _logger.LogInformation(
+ "Sample {SampleId} deleted successfully / Sample {SampleId} đã xóa thành công",
+ request.SampleId);
+
+ return true;
+ }
+}
diff --git a/services/mkt-facebook-service-net/src/FacebookService.API/Application/Commands/UpdateSampleCommand.cs b/services/mkt-facebook-service-net/src/FacebookService.API/Application/Commands/UpdateSampleCommand.cs
new file mode 100644
index 00000000..ec51816b
--- /dev/null
+++ b/services/mkt-facebook-service-net/src/FacebookService.API/Application/Commands/UpdateSampleCommand.cs
@@ -0,0 +1,16 @@
+using MediatR;
+
+namespace FacebookService.API.Application.Commands;
+
+///
+/// EN: Command to update an existing Sample.
+/// VI: Command để cập nhật một Sample đã tồn tại.
+///
+/// EN: Sample ID to update / VI: ID sample cần cập nhật
+/// EN: New name / VI: Tên mới
+/// EN: New description / VI: Mô tả mới
+public record UpdateSampleCommand(
+ Guid SampleId,
+ string Name,
+ string? Description
+) : IRequest;
diff --git a/services/mkt-facebook-service-net/src/FacebookService.API/Application/Commands/UpdateSampleCommandHandler.cs b/services/mkt-facebook-service-net/src/FacebookService.API/Application/Commands/UpdateSampleCommandHandler.cs
new file mode 100644
index 00000000..b8f0f0a6
--- /dev/null
+++ b/services/mkt-facebook-service-net/src/FacebookService.API/Application/Commands/UpdateSampleCommandHandler.cs
@@ -0,0 +1,54 @@
+using MediatR;
+using FacebookService.Domain.AggregatesModel.SampleAggregate;
+
+namespace FacebookService.API.Application.Commands;
+
+///
+/// EN: Handler for UpdateSampleCommand.
+/// VI: Handler cho UpdateSampleCommand.
+///
+public class UpdateSampleCommandHandler : IRequestHandler
+{
+ private readonly ISampleRepository _sampleRepository;
+ private readonly ILogger _logger;
+
+ public UpdateSampleCommandHandler(
+ ISampleRepository sampleRepository,
+ ILogger logger)
+ {
+ _sampleRepository = sampleRepository ?? throw new ArgumentNullException(nameof(sampleRepository));
+ _logger = logger ?? throw new ArgumentNullException(nameof(logger));
+ }
+
+ public async Task Handle(
+ UpdateSampleCommand request,
+ CancellationToken cancellationToken)
+ {
+ _logger.LogInformation(
+ "Updating sample {SampleId} / Cập nhật sample {SampleId}",
+ request.SampleId);
+
+ // EN: Get existing sample / VI: Lấy sample đã tồn tại
+ var sample = await _sampleRepository.GetAsync(request.SampleId);
+
+ if (sample is null)
+ {
+ _logger.LogWarning(
+ "Sample {SampleId} not found / Sample {SampleId} không tìm thấy",
+ request.SampleId);
+ return false;
+ }
+
+ // EN: Update sample using domain method / VI: Cập nhật sample sử dụng domain method
+ sample.Update(request.Name, request.Description);
+
+ // EN: Save changes / VI: Lưu thay đổi
+ await _sampleRepository.UnitOfWork.SaveEntitiesAsync(cancellationToken);
+
+ _logger.LogInformation(
+ "Sample {SampleId} updated successfully / Sample {SampleId} đã cập nhật thành công",
+ request.SampleId);
+
+ return true;
+ }
+}
diff --git a/services/mkt-facebook-service-net/src/FacebookService.API/Application/Queries/GetSampleQuery.cs b/services/mkt-facebook-service-net/src/FacebookService.API/Application/Queries/GetSampleQuery.cs
new file mode 100644
index 00000000..8f2b8002
--- /dev/null
+++ b/services/mkt-facebook-service-net/src/FacebookService.API/Application/Queries/GetSampleQuery.cs
@@ -0,0 +1,23 @@
+using MediatR;
+
+namespace FacebookService.API.Application.Queries;
+
+///
+/// EN: Query to get a Sample by ID.
+/// VI: Query để lấy một Sample theo ID.
+///
+/// EN: Sample ID / VI: ID sample
+public record GetSampleQuery(Guid SampleId) : IRequest;
+
+///
+/// EN: Sample view model for API responses.
+/// VI: Sample view model cho API responses.
+///
+public record SampleViewModel(
+ Guid Id,
+ string Name,
+ string? Description,
+ string Status,
+ DateTime CreatedAt,
+ DateTime? UpdatedAt
+);
diff --git a/services/mkt-facebook-service-net/src/FacebookService.API/Application/Queries/GetSampleQueryHandler.cs b/services/mkt-facebook-service-net/src/FacebookService.API/Application/Queries/GetSampleQueryHandler.cs
new file mode 100644
index 00000000..f25c5a57
--- /dev/null
+++ b/services/mkt-facebook-service-net/src/FacebookService.API/Application/Queries/GetSampleQueryHandler.cs
@@ -0,0 +1,39 @@
+using MediatR;
+using FacebookService.Domain.AggregatesModel.SampleAggregate;
+
+namespace FacebookService.API.Application.Queries;
+
+///
+/// EN: Handler for GetSampleQuery.
+/// VI: Handler cho GetSampleQuery.
+///
+public class GetSampleQueryHandler : IRequestHandler
+{
+ private readonly ISampleRepository _sampleRepository;
+
+ public GetSampleQueryHandler(ISampleRepository sampleRepository)
+ {
+ _sampleRepository = sampleRepository ?? throw new ArgumentNullException(nameof(sampleRepository));
+ }
+
+ public async Task Handle(
+ GetSampleQuery request,
+ CancellationToken cancellationToken)
+ {
+ var sample = await _sampleRepository.GetAsync(request.SampleId);
+
+ if (sample is null)
+ {
+ return null;
+ }
+
+ return new SampleViewModel(
+ sample.Id,
+ sample.Name,
+ sample.Description,
+ sample.Status.Name,
+ sample.CreatedAt,
+ sample.UpdatedAt
+ );
+ }
+}
diff --git a/services/mkt-facebook-service-net/src/FacebookService.API/Application/Queries/GetSamplesQuery.cs b/services/mkt-facebook-service-net/src/FacebookService.API/Application/Queries/GetSamplesQuery.cs
new file mode 100644
index 00000000..70f5c947
--- /dev/null
+++ b/services/mkt-facebook-service-net/src/FacebookService.API/Application/Queries/GetSamplesQuery.cs
@@ -0,0 +1,9 @@
+using MediatR;
+
+namespace FacebookService.API.Application.Queries;
+
+///
+/// EN: Query to get all Samples.
+/// VI: Query để lấy tất cả Samples.
+///
+public record GetSamplesQuery : IRequest>;
diff --git a/services/mkt-facebook-service-net/src/FacebookService.API/Application/Queries/GetSamplesQueryHandler.cs b/services/mkt-facebook-service-net/src/FacebookService.API/Application/Queries/GetSamplesQueryHandler.cs
new file mode 100644
index 00000000..c6ff620e
--- /dev/null
+++ b/services/mkt-facebook-service-net/src/FacebookService.API/Application/Queries/GetSamplesQueryHandler.cs
@@ -0,0 +1,34 @@
+using MediatR;
+using FacebookService.Domain.AggregatesModel.SampleAggregate;
+
+namespace FacebookService.API.Application.Queries;
+
+///
+/// EN: Handler for GetSamplesQuery.
+/// VI: Handler cho GetSamplesQuery.
+///
+public class GetSamplesQueryHandler : IRequestHandler>
+{
+ private readonly ISampleRepository _sampleRepository;
+
+ public GetSamplesQueryHandler(ISampleRepository sampleRepository)
+ {
+ _sampleRepository = sampleRepository ?? throw new ArgumentNullException(nameof(sampleRepository));
+ }
+
+ public async Task> Handle(
+ GetSamplesQuery request,
+ CancellationToken cancellationToken)
+ {
+ var samples = await _sampleRepository.GetAllAsync();
+
+ return samples.Select(sample => new SampleViewModel(
+ sample.Id,
+ sample.Name,
+ sample.Description,
+ sample.Status.Name,
+ sample.CreatedAt,
+ sample.UpdatedAt
+ ));
+ }
+}
diff --git a/services/mkt-facebook-service-net/src/FacebookService.API/Application/Validations/CreateSampleCommandValidator.cs b/services/mkt-facebook-service-net/src/FacebookService.API/Application/Validations/CreateSampleCommandValidator.cs
new file mode 100644
index 00000000..d4a751df
--- /dev/null
+++ b/services/mkt-facebook-service-net/src/FacebookService.API/Application/Validations/CreateSampleCommandValidator.cs
@@ -0,0 +1,25 @@
+using FluentValidation;
+using FacebookService.API.Application.Commands;
+
+namespace FacebookService.API.Application.Validations;
+
+///
+/// EN: Validator for CreateSampleCommand.
+/// VI: Validator cho CreateSampleCommand.
+///
+public class CreateSampleCommandValidator : AbstractValidator
+{
+ public CreateSampleCommandValidator()
+ {
+ RuleFor(x => x.Name)
+ .NotEmpty()
+ .WithMessage("Name is required / Tên là bắt buộc")
+ .MaximumLength(200)
+ .WithMessage("Name must be less than 200 characters / Tên phải ít hơn 200 ký tự");
+
+ RuleFor(x => x.Description)
+ .MaximumLength(1000)
+ .WithMessage("Description must be less than 1000 characters / Mô tả phải ít hơn 1000 ký tự")
+ .When(x => x.Description != null);
+ }
+}
diff --git a/services/mkt-facebook-service-net/src/FacebookService.API/Application/Validations/UpdateSampleCommandValidator.cs b/services/mkt-facebook-service-net/src/FacebookService.API/Application/Validations/UpdateSampleCommandValidator.cs
new file mode 100644
index 00000000..a10189e2
--- /dev/null
+++ b/services/mkt-facebook-service-net/src/FacebookService.API/Application/Validations/UpdateSampleCommandValidator.cs
@@ -0,0 +1,29 @@
+using FluentValidation;
+using FacebookService.API.Application.Commands;
+
+namespace FacebookService.API.Application.Validations;
+
+///
+/// EN: Validator for UpdateSampleCommand.
+/// VI: Validator cho UpdateSampleCommand.
+///
+public class UpdateSampleCommandValidator : AbstractValidator
+{
+ public UpdateSampleCommandValidator()
+ {
+ RuleFor(x => x.SampleId)
+ .NotEmpty()
+ .WithMessage("Sample ID is required / ID sample là bắt buộc");
+
+ RuleFor(x => x.Name)
+ .NotEmpty()
+ .WithMessage("Name is required / Tên là bắt buộc")
+ .MaximumLength(200)
+ .WithMessage("Name must be less than 200 characters / Tên phải ít hơn 200 ký tự");
+
+ RuleFor(x => x.Description)
+ .MaximumLength(1000)
+ .WithMessage("Description must be less than 1000 characters / Mô tả phải ít hơn 1000 ký tự")
+ .When(x => x.Description != null);
+ }
+}
diff --git a/services/mkt-facebook-service-net/src/FacebookService.API/Controllers/SamplesController.cs b/services/mkt-facebook-service-net/src/FacebookService.API/Controllers/SamplesController.cs
new file mode 100644
index 00000000..6f400561
--- /dev/null
+++ b/services/mkt-facebook-service-net/src/FacebookService.API/Controllers/SamplesController.cs
@@ -0,0 +1,200 @@
+using Asp.Versioning;
+using MediatR;
+using Microsoft.AspNetCore.Mvc;
+using FacebookService.API.Application.Commands;
+using FacebookService.API.Application.Queries;
+
+namespace FacebookService.API.Controllers;
+
+///
+/// EN: Controller for Sample CRUD operations using CQRS pattern.
+/// VI: Controller cho các thao tác CRUD Sample sử dụng pattern CQRS.
+///
+[ApiController]
+[ApiVersion("1.0")]
+[Route("api/v{version:apiVersion}/[controller]")]
+[Produces("application/json")]
+public class SamplesController : ControllerBase
+{
+ private readonly IMediator _mediator;
+ private readonly ILogger _logger;
+
+ public SamplesController(IMediator mediator, ILogger logger)
+ {
+ _mediator = mediator ?? throw new ArgumentNullException(nameof(mediator));
+ _logger = logger ?? throw new ArgumentNullException(nameof(logger));
+ }
+
+ ///
+ /// EN: Get all samples.
+ /// VI: Lấy tất cả samples.
+ ///
+ /// EN: List of samples / VI: Danh sách samples
+ [HttpGet]
+ [ProducesResponseType(typeof(IEnumerable), StatusCodes.Status200OK)]
+ public async Task GetSamples()
+ {
+ var samples = await _mediator.Send(new GetSamplesQuery());
+ return Ok(new { success = true, data = samples });
+ }
+
+ ///
+ /// EN: Get a sample by ID.
+ /// VI: Lấy một sample theo ID.
+ ///
+ /// EN: Sample ID / VI: ID sample
+ /// EN: Sample details / VI: Chi tiết sample
+ [HttpGet("{id:guid}")]
+ [ProducesResponseType(typeof(SampleViewModel), StatusCodes.Status200OK)]
+ [ProducesResponseType(StatusCodes.Status404NotFound)]
+ public async Task GetSample(Guid id)
+ {
+ var sample = await _mediator.Send(new GetSampleQuery(id));
+
+ if (sample is null)
+ {
+ return NotFound(new
+ {
+ success = false,
+ error = new
+ {
+ code = "SAMPLE_NOT_FOUND",
+ message = $"Sample with ID {id} not found / Sample với ID {id} không tìm thấy"
+ }
+ });
+ }
+
+ return Ok(new { success = true, data = sample });
+ }
+
+ ///
+ /// EN: Create a new sample.
+ /// VI: Tạo một sample mới.
+ ///
+ /// EN: Create request / VI: Request tạo
+ /// EN: Created sample ID / VI: ID sample đã tạo
+ [HttpPost]
+ [ProducesResponseType(typeof(CreateSampleCommandResult), StatusCodes.Status201Created)]
+ [ProducesResponseType(StatusCodes.Status400BadRequest)]
+ public async Task CreateSample([FromBody] CreateSampleRequest request)
+ {
+ var command = new CreateSampleCommand(request.Name, request.Description);
+ var result = await _mediator.Send(command);
+
+ return CreatedAtAction(
+ nameof(GetSample),
+ new { id = result.Id },
+ new { success = true, data = result });
+ }
+
+ ///
+ /// EN: Update an existing sample.
+ /// VI: Cập nhật một sample đã tồn tại.
+ ///
+ /// EN: Sample ID / VI: ID sample
+ /// EN: Update request / VI: Request cập nhật
+ /// EN: Success status / VI: Trạng thái thành công
+ [HttpPut("{id:guid}")]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ [ProducesResponseType(StatusCodes.Status404NotFound)]
+ public async Task UpdateSample(Guid id, [FromBody] UpdateSampleRequest request)
+ {
+ var command = new UpdateSampleCommand(id, request.Name, request.Description);
+ var result = await _mediator.Send(command);
+
+ if (!result)
+ {
+ return NotFound(new
+ {
+ success = false,
+ error = new
+ {
+ code = "SAMPLE_NOT_FOUND",
+ message = $"Sample with ID {id} not found / Sample với ID {id} không tìm thấy"
+ }
+ });
+ }
+
+ return Ok(new { success = true, message = "Sample updated successfully / Sample đã cập nhật thành công" });
+ }
+
+ ///
+ /// EN: Delete a sample.
+ /// VI: Xóa một sample.
+ ///
+ /// EN: Sample ID / VI: ID sample
+ /// EN: Success status / VI: Trạng thái thành công
+ [HttpDelete("{id:guid}")]
+ [ProducesResponseType(StatusCodes.Status204NoContent)]
+ [ProducesResponseType(StatusCodes.Status404NotFound)]
+ public async Task DeleteSample(Guid id)
+ {
+ var command = new DeleteSampleCommand(id);
+ var result = await _mediator.Send(command);
+
+ if (!result)
+ {
+ return NotFound(new
+ {
+ success = false,
+ error = new
+ {
+ code = "SAMPLE_NOT_FOUND",
+ message = $"Sample with ID {id} not found / Sample với ID {id} không tìm thấy"
+ }
+ });
+ }
+
+ return NoContent();
+ }
+
+ ///
+ /// EN: Change sample status.
+ /// VI: Thay đổi trạng thái sample.
+ ///
+ /// EN: Sample ID / VI: ID sample
+ /// EN: Status change request / VI: Request thay đổi trạng thái
+ /// EN: Success status / VI: Trạng thái thành công
+ [HttpPatch("{id:guid}/status")]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ [ProducesResponseType(StatusCodes.Status400BadRequest)]
+ [ProducesResponseType(StatusCodes.Status404NotFound)]
+ public async Task ChangeSampleStatus(Guid id, [FromBody] ChangeStatusRequest request)
+ {
+ var command = new ChangeSampleStatusCommand(id, request.Status);
+ var result = await _mediator.Send(command);
+
+ if (!result)
+ {
+ return BadRequest(new
+ {
+ success = false,
+ error = new
+ {
+ code = "STATUS_CHANGE_FAILED",
+ message = "Failed to change sample status / Thay đổi trạng thái sample thất bại"
+ }
+ });
+ }
+
+ return Ok(new { success = true, message = "Sample status changed successfully / Trạng thái sample đã thay đổi thành công" });
+ }
+}
+
+///
+/// EN: Request model for creating a sample.
+/// VI: Model request để tạo sample.
+///
+public record CreateSampleRequest(string Name, string? Description);
+
+///
+/// EN: Request model for updating a sample.
+/// VI: Model request để cập nhật sample.
+///
+public record UpdateSampleRequest(string Name, string? Description);
+
+///
+/// EN: Request model for changing sample status.
+/// VI: Model request để thay đổi trạng thái sample.
+///
+public record ChangeStatusRequest(string Status);
diff --git a/services/mkt-facebook-service-net/src/FacebookService.API/FacebookService.API.csproj b/services/mkt-facebook-service-net/src/FacebookService.API/FacebookService.API.csproj
new file mode 100644
index 00000000..b90689e4
--- /dev/null
+++ b/services/mkt-facebook-service-net/src/FacebookService.API/FacebookService.API.csproj
@@ -0,0 +1,43 @@
+
+
+
+ FacebookService.API
+ FacebookService.API
+ Web API layer with CQRS pattern
+ myservice-api
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/services/mkt-facebook-service-net/src/FacebookService.API/Program.cs b/services/mkt-facebook-service-net/src/FacebookService.API/Program.cs
new file mode 100644
index 00000000..cfc9c14f
--- /dev/null
+++ b/services/mkt-facebook-service-net/src/FacebookService.API/Program.cs
@@ -0,0 +1,144 @@
+using Asp.Versioning;
+using FluentValidation;
+using Hellang.Middleware.ProblemDetails;
+using FacebookService.API.Application.Behaviors;
+using FacebookService.Infrastructure;
+using Serilog;
+
+// EN: Configure Serilog early / VI: Cấu hình Serilog sớm
+Log.Logger = new LoggerConfiguration()
+ .WriteTo.Console()
+ .CreateBootstrapLogger();
+
+try
+{
+ Log.Information("Starting FacebookService API / Khởi động FacebookService API");
+
+ var builder = WebApplication.CreateBuilder(args);
+
+ // EN: Configure Serilog / VI: Cấu hình Serilog
+ builder.Host.UseSerilog((context, services, configuration) => configuration
+ .ReadFrom.Configuration(context.Configuration)
+ .ReadFrom.Services(services)
+ .Enrich.FromLogContext()
+ .WriteTo.Console());
+
+ // EN: Add Infrastructure services / VI: Thêm Infrastructure services
+ builder.Services.AddInfrastructure(builder.Configuration);
+
+ // EN: Add MediatR with behaviors / VI: Thêm MediatR với behaviors
+ builder.Services.AddMediatR(cfg =>
+ {
+ cfg.RegisterServicesFromAssemblyContaining();
+ cfg.AddOpenBehavior(typeof(LoggingBehavior<,>));
+ cfg.AddOpenBehavior(typeof(ValidatorBehavior<,>));
+ cfg.AddOpenBehavior(typeof(TransactionBehavior<,>));
+ });
+
+ // EN: Add FluentValidation / VI: Thêm FluentValidation
+ builder.Services.AddValidatorsFromAssemblyContaining();
+
+ // EN: Add API versioning / VI: Thêm API versioning
+ builder.Services.AddApiVersioning(options =>
+ {
+ options.DefaultApiVersion = new ApiVersion(1, 0);
+ options.AssumeDefaultVersionWhenUnspecified = true;
+ options.ReportApiVersions = true;
+ options.ApiVersionReader = ApiVersionReader.Combine(
+ new UrlSegmentApiVersionReader(),
+ new HeaderApiVersionReader("X-Api-Version"));
+ })
+ .AddApiExplorer(options =>
+ {
+ options.GroupNameFormat = "'v'VVV";
+ options.SubstituteApiVersionInUrl = true;
+ });
+
+ // EN: Add controllers / VI: Thêm controllers
+ builder.Services.AddControllers();
+
+ // EN: Add ProblemDetails middleware (RFC 7807) / VI: Thêm ProblemDetails middleware
+ builder.Services.AddProblemDetails(options =>
+ {
+ options.IncludeExceptionDetails = (ctx, ex) =>
+ builder.Environment.IsDevelopment();
+ });
+
+ // EN: Add Swagger / VI: Thêm Swagger
+ builder.Services.AddEndpointsApiExplorer();
+ builder.Services.AddSwaggerGen(options =>
+ {
+ options.SwaggerDoc("v1", new()
+ {
+ Title = "FacebookService API",
+ Version = "v1",
+ Description = "FacebookService microservice API / API microservice FacebookService"
+ });
+ });
+
+ // EN: Add health checks / VI: Thêm health checks
+ builder.Services.AddHealthChecks()
+ .AddNpgSql(
+ builder.Configuration.GetConnectionString("DefaultConnection")
+ ?? builder.Configuration["DATABASE_URL"]
+ ?? "",
+ name: "postgresql",
+ tags: ["db", "postgresql"]);
+
+ // EN: Add CORS / VI: Thêm CORS
+ builder.Services.AddCors(options =>
+ {
+ options.AddDefaultPolicy(policy =>
+ {
+ policy.AllowAnyOrigin()
+ .AllowAnyMethod()
+ .AllowAnyHeader();
+ });
+ });
+
+ var app = builder.Build();
+
+ // EN: Configure middleware pipeline / VI: Cấu hình middleware pipeline
+ app.UseSerilogRequestLogging();
+ app.UseProblemDetails();
+
+ if (app.Environment.IsDevelopment())
+ {
+ app.UseSwagger();
+ app.UseSwaggerUI(c =>
+ {
+ c.SwaggerEndpoint("/swagger/v1/swagger.json", "FacebookService API v1");
+ c.RoutePrefix = "swagger";
+ });
+ }
+
+ app.UseCors();
+ app.UseRouting();
+
+ // EN: Map health check endpoints / VI: Map health check endpoints
+ app.MapHealthChecks("/health");
+ app.MapHealthChecks("/health/live", new()
+ {
+ Predicate = _ => false // EN: Just checks app is running / VI: Chỉ kiểm tra app đang chạy
+ });
+ app.MapHealthChecks("/health/ready");
+
+ // EN: Map controllers / VI: Map controllers
+ app.MapControllers();
+
+ // EN: Run the application / VI: Chạy ứng dụng
+ app.Run();
+}
+catch (Exception ex)
+{
+ Log.Fatal(ex, "Application terminated unexpectedly / Ứng dụng kết thúc bất ngờ");
+ throw;
+}
+finally
+{
+ Log.CloseAndFlush();
+}
+
+// EN: Make Program class accessible for integration tests
+// VI: Làm cho class Program có thể truy cập cho integration tests
+public partial class Program { }
diff --git a/services/mkt-facebook-service-net/src/FacebookService.API/Properties/launchSettings.json b/services/mkt-facebook-service-net/src/FacebookService.API/Properties/launchSettings.json
new file mode 100644
index 00000000..6355d40b
--- /dev/null
+++ b/services/mkt-facebook-service-net/src/FacebookService.API/Properties/launchSettings.json
@@ -0,0 +1,15 @@
+{
+ "$schema": "http://json.schemastore.org/launchsettings.json",
+ "profiles": {
+ "http": {
+ "commandName": "Project",
+ "dotnetRunMessages": true,
+ "launchBrowser": true,
+ "launchUrl": "swagger",
+ "applicationUrl": "http://localhost:5000",
+ "environmentVariables": {
+ "ASPNETCORE_ENVIRONMENT": "Development"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/services/mkt-facebook-service-net/src/FacebookService.API/appsettings.Development.json b/services/mkt-facebook-service-net/src/FacebookService.API/appsettings.Development.json
new file mode 100644
index 00000000..e407ac85
--- /dev/null
+++ b/services/mkt-facebook-service-net/src/FacebookService.API/appsettings.Development.json
@@ -0,0 +1,19 @@
+{
+ "Logging": {
+ "LogLevel": {
+ "Default": "Debug",
+ "Microsoft.AspNetCore": "Information",
+ "Microsoft.EntityFrameworkCore.Database.Command": "Information"
+ }
+ },
+ "Serilog": {
+ "MinimumLevel": {
+ "Default": "Debug",
+ "Override": {
+ "Microsoft": "Information",
+ "Microsoft.EntityFrameworkCore.Database.Command": "Information",
+ "System": "Information"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/services/mkt-facebook-service-net/src/FacebookService.API/appsettings.json b/services/mkt-facebook-service-net/src/FacebookService.API/appsettings.json
new file mode 100644
index 00000000..523dc0fc
--- /dev/null
+++ b/services/mkt-facebook-service-net/src/FacebookService.API/appsettings.json
@@ -0,0 +1,46 @@
+{
+ "Logging": {
+ "LogLevel": {
+ "Default": "Information",
+ "Microsoft.AspNetCore": "Warning",
+ "Microsoft.EntityFrameworkCore": "Warning"
+ }
+ },
+ "Serilog": {
+ "MinimumLevel": {
+ "Default": "Information",
+ "Override": {
+ "Microsoft": "Warning",
+ "Microsoft.EntityFrameworkCore": "Warning",
+ "System": "Warning"
+ }
+ },
+ "WriteTo": [
+ {
+ "Name": "Console",
+ "Args": {
+ "outputTemplate": "[{Timestamp:HH:mm:ss} {Level:u3}] {Message:lj} {Properties:j}{NewLine}{Exception}"
+ }
+ }
+ ],
+ "Enrich": [
+ "FromLogContext",
+ "WithMachineName",
+ "WithThreadId"
+ ]
+ },
+ "ConnectionStrings": {
+ "DefaultConnection": "Host=localhost;Port=5432;Database=myservice_db;Username=postgres;Password=postgres"
+ },
+ "Redis": {
+ "ConnectionString": "localhost:6379"
+ },
+ "Jwt": {
+ "Secret": "your-super-secret-key-min-32-characters",
+ "Issuer": "goodgo-platform",
+ "Audience": "goodgo-services",
+ "AccessTokenExpiryMinutes": 15,
+ "RefreshTokenExpiryDays": 7
+ },
+ "AllowedHosts": "*"
+}
\ No newline at end of file
diff --git a/services/mkt-facebook-service-net/src/FacebookService.Domain/AggregatesModel/SampleAggregate/ISampleRepository.cs b/services/mkt-facebook-service-net/src/FacebookService.Domain/AggregatesModel/SampleAggregate/ISampleRepository.cs
new file mode 100644
index 00000000..06b714c2
--- /dev/null
+++ b/services/mkt-facebook-service-net/src/FacebookService.Domain/AggregatesModel/SampleAggregate/ISampleRepository.cs
@@ -0,0 +1,61 @@
+using FacebookService.Domain.SeedWork;
+
+namespace FacebookService.Domain.AggregatesModel.SampleAggregate;
+
+///
+/// EN: Repository interface for Sample aggregate.
+/// VI: Interface repository cho Sample aggregate.
+///
+///
+/// EN: Following repository pattern, this interface defines the contract
+/// for data access operations on Sample aggregate.
+/// VI: Theo pattern repository, interface này định nghĩa contract
+/// cho các thao tác truy cập dữ liệu trên Sample aggregate.
+///
+public interface ISampleRepository : IRepository
+{
+ ///
+ /// EN: Get a sample by its ID.
+ /// VI: Lấy một sample theo ID.
+ ///
+ /// EN: The sample ID / VI: ID của sample
+ /// EN: The sample or null if not found / VI: Sample hoặc null nếu không tìm thấy
+ Task GetAsync(Guid sampleId);
+
+ ///
+ /// EN: Get all samples.
+ /// VI: Lấy tất cả samples.
+ ///
+ /// EN: List of samples / VI: Danh sách samples
+ Task> GetAllAsync();
+
+ ///
+ /// EN: Add a new sample.
+ /// VI: Thêm một sample mới.
+ ///
+ /// EN: The sample to add / VI: Sample cần thêm
+ /// EN: The added sample / VI: Sample đã thêm
+ Sample Add(Sample sample);
+
+ ///
+ /// EN: Update an existing sample.
+ /// VI: Cập nhật một sample đã tồn tại.
+ ///
+ /// EN: The sample to update / VI: Sample cần cập nhật
+ void Update(Sample sample);
+
+ ///
+ /// EN: Delete a sample.
+ /// VI: Xóa một sample.
+ ///
+ /// EN: The sample to delete / VI: Sample cần xóa
+ void Delete(Sample sample);
+
+ ///
+ /// EN: Get samples by status.
+ /// VI: Lấy samples theo trạng thái.
+ ///
+ /// EN: The status ID / VI: ID trạng thái
+ /// EN: List of samples with given status / VI: Danh sách samples với trạng thái cho trước
+ Task> GetByStatusAsync(int statusId);
+}
diff --git a/services/mkt-facebook-service-net/src/FacebookService.Domain/AggregatesModel/SampleAggregate/Sample.cs b/services/mkt-facebook-service-net/src/FacebookService.Domain/AggregatesModel/SampleAggregate/Sample.cs
new file mode 100644
index 00000000..8e6844bc
--- /dev/null
+++ b/services/mkt-facebook-service-net/src/FacebookService.Domain/AggregatesModel/SampleAggregate/Sample.cs
@@ -0,0 +1,158 @@
+using FacebookService.Domain.Events;
+using FacebookService.Domain.Exceptions;
+using FacebookService.Domain.SeedWork;
+
+namespace FacebookService.Domain.AggregatesModel.SampleAggregate;
+
+///
+/// EN: Sample aggregate root demonstrating DDD patterns.
+/// VI: Sample aggregate root minh họa các pattern DDD.
+///
+public class Sample : Entity, IAggregateRoot
+{
+ // EN: Private fields for encapsulation
+ // VI: Fields private để đóng gói
+ private string _name = null!;
+ private string? _description;
+ private SampleStatus _status = null!;
+ private DateTime _createdAt;
+ private DateTime? _updatedAt;
+
+ ///
+ /// EN: Sample name (required).
+ /// VI: Tên sample (bắt buộc).
+ ///
+ public string Name => _name;
+
+ ///
+ /// EN: Optional description.
+ /// VI: Mô tả tùy chọn.
+ ///
+ public string? Description => _description;
+
+ ///
+ /// EN: Current status.
+ /// VI: Trạng thái hiện tại.
+ ///
+ public SampleStatus Status => _status;
+
+ ///
+ /// EN: Status ID for EF Core mapping.
+ /// VI: ID trạng thái cho EF Core mapping.
+ ///
+ public int StatusId { get; private set; }
+
+ ///
+ /// EN: Creation timestamp.
+ /// VI: Thời gian tạo.
+ ///
+ public DateTime CreatedAt => _createdAt;
+
+ ///
+ /// EN: Last update timestamp.
+ /// VI: Thời gian cập nhật cuối.
+ ///
+ public DateTime? UpdatedAt => _updatedAt;
+
+ ///
+ /// EN: Private constructor for EF Core.
+ /// VI: Constructor private cho EF Core.
+ ///
+ protected Sample()
+ {
+ }
+
+ ///
+ /// EN: Create a new Sample with required information.
+ /// VI: Tạo một Sample mới với thông tin bắt buộc.
+ ///
+ /// EN: Sample name / VI: Tên sample
+ /// EN: Optional description / VI: Mô tả tùy chọn
+ public Sample(string name, string? description = null) : this()
+ {
+ if (string.IsNullOrWhiteSpace(name))
+ throw new SampleDomainException("Sample name cannot be empty");
+
+ Id = Guid.NewGuid();
+ _name = name;
+ _description = description;
+ _status = SampleStatus.Draft;
+ StatusId = SampleStatus.Draft.Id;
+ _createdAt = DateTime.UtcNow;
+
+ // EN: Add domain event for creation
+ // VI: Thêm domain event cho việc tạo
+ AddDomainEvent(new SampleCreatedDomainEvent(this));
+ }
+
+ ///
+ /// EN: Update sample information.
+ /// VI: Cập nhật thông tin sample.
+ ///
+ public void Update(string name, string? description)
+ {
+ if (string.IsNullOrWhiteSpace(name))
+ throw new SampleDomainException("Sample name cannot be empty");
+
+ if (_status == SampleStatus.Cancelled)
+ throw new SampleDomainException("Cannot update a cancelled sample");
+
+ _name = name;
+ _description = description;
+ _updatedAt = DateTime.UtcNow;
+ }
+
+ ///
+ /// EN: Activate the sample.
+ /// VI: Kích hoạt sample.
+ ///
+ public void Activate()
+ {
+ if (_status != SampleStatus.Draft)
+ throw new SampleDomainException("Only draft samples can be activated");
+
+ var previousStatus = _status;
+ _status = SampleStatus.Active;
+ StatusId = SampleStatus.Active.Id;
+ _updatedAt = DateTime.UtcNow;
+
+ AddDomainEvent(new SampleStatusChangedDomainEvent(Id, previousStatus, _status));
+ }
+
+ ///
+ /// EN: Complete the sample.
+ /// VI: Hoàn thành sample.
+ ///
+ public void Complete()
+ {
+ if (_status != SampleStatus.Active)
+ throw new SampleDomainException("Only active samples can be completed");
+
+ var previousStatus = _status;
+ _status = SampleStatus.Completed;
+ StatusId = SampleStatus.Completed.Id;
+ _updatedAt = DateTime.UtcNow;
+
+ AddDomainEvent(new SampleStatusChangedDomainEvent(Id, previousStatus, _status));
+ }
+
+ ///
+ /// EN: Cancel the sample.
+ /// VI: Hủy sample.
+ ///
+ public void Cancel()
+ {
+ if (_status == SampleStatus.Completed)
+ throw new SampleDomainException("Cannot cancel a completed sample");
+
+ if (_status == SampleStatus.Cancelled)
+ throw new SampleDomainException("Sample is already cancelled");
+
+ var previousStatus = _status;
+ _status = SampleStatus.Cancelled;
+ StatusId = SampleStatus.Cancelled.Id;
+ _updatedAt = DateTime.UtcNow;
+
+ AddDomainEvent(new SampleStatusChangedDomainEvent(Id, previousStatus, _status));
+ }
+}
diff --git a/services/mkt-facebook-service-net/src/FacebookService.Domain/AggregatesModel/SampleAggregate/SampleStatus.cs b/services/mkt-facebook-service-net/src/FacebookService.Domain/AggregatesModel/SampleAggregate/SampleStatus.cs
new file mode 100644
index 00000000..e11060d6
--- /dev/null
+++ b/services/mkt-facebook-service-net/src/FacebookService.Domain/AggregatesModel/SampleAggregate/SampleStatus.cs
@@ -0,0 +1,77 @@
+using FacebookService.Domain.SeedWork;
+
+namespace FacebookService.Domain.AggregatesModel.SampleAggregate;
+
+///
+/// EN: Sample status enumeration following type-safe enum pattern.
+/// VI: Enumeration trạng thái Sample theo pattern enum an toàn kiểu.
+///
+public class SampleStatus : Enumeration
+{
+ ///
+ /// EN: Draft status - initial state
+ /// VI: Trạng thái nháp - trạng thái ban đầu
+ ///
+ public static SampleStatus Draft = new(1, nameof(Draft));
+
+ ///
+ /// EN: Active status - ready for use
+ /// VI: Trạng thái hoạt động - sẵn sàng sử dụng
+ ///
+ public static SampleStatus Active = new(2, nameof(Active));
+
+ ///
+ /// EN: Completed status - finished processing
+ /// VI: Trạng thái hoàn thành - đã xử lý xong
+ ///
+ public static SampleStatus Completed = new(3, nameof(Completed));
+
+ ///
+ /// EN: Cancelled status - cancelled by user
+ /// VI: Trạng thái đã hủy - bị hủy bởi người dùng
+ ///
+ public static SampleStatus Cancelled = new(4, nameof(Cancelled));
+
+ public SampleStatus(int id, string name) : base(id, name)
+ {
+ }
+
+ ///
+ /// EN: Get all available statuses.
+ /// VI: Lấy tất cả các trạng thái có sẵn.
+ ///
+ public static IEnumerable List() => GetAll();
+
+ ///
+ /// EN: Parse status from name.
+ /// VI: Parse trạng thái từ tên.
+ ///
+ public static SampleStatus FromName(string name)
+ {
+ var status = List().SingleOrDefault(s =>
+ string.Equals(s.Name, name, StringComparison.CurrentCultureIgnoreCase));
+
+ if (status is null)
+ {
+ throw new ArgumentException($"Possible values for SampleStatus: {string.Join(",", List().Select(s => s.Name))}");
+ }
+
+ return status;
+ }
+
+ ///
+ /// EN: Parse status from ID.
+ /// VI: Parse trạng thái từ ID.
+ ///
+ public static SampleStatus From(int id)
+ {
+ var status = List().SingleOrDefault(s => s.Id == id);
+
+ if (status is null)
+ {
+ throw new ArgumentException($"Possible values for SampleStatus: {string.Join(",", List().Select(s => s.Name))}");
+ }
+
+ return status;
+ }
+}
diff --git a/services/mkt-facebook-service-net/src/FacebookService.Domain/Events/SampleCreatedDomainEvent.cs b/services/mkt-facebook-service-net/src/FacebookService.Domain/Events/SampleCreatedDomainEvent.cs
new file mode 100644
index 00000000..f1e4906b
--- /dev/null
+++ b/services/mkt-facebook-service-net/src/FacebookService.Domain/Events/SampleCreatedDomainEvent.cs
@@ -0,0 +1,22 @@
+using MediatR;
+using FacebookService.Domain.AggregatesModel.SampleAggregate;
+
+namespace FacebookService.Domain.Events;
+
+///
+/// EN: Domain event raised when a new Sample is created.
+/// VI: Domain event được phát ra khi một Sample mới được tạo.
+///
+public class SampleCreatedDomainEvent : INotification
+{
+ ///
+ /// EN: The newly created sample.
+ /// VI: Sample mới được tạo.
+ ///
+ public Sample Sample { get; }
+
+ public SampleCreatedDomainEvent(Sample sample)
+ {
+ Sample = sample;
+ }
+}
diff --git a/services/mkt-facebook-service-net/src/FacebookService.Domain/Events/SampleStatusChangedDomainEvent.cs b/services/mkt-facebook-service-net/src/FacebookService.Domain/Events/SampleStatusChangedDomainEvent.cs
new file mode 100644
index 00000000..5886040e
--- /dev/null
+++ b/services/mkt-facebook-service-net/src/FacebookService.Domain/Events/SampleStatusChangedDomainEvent.cs
@@ -0,0 +1,39 @@
+using MediatR;
+using FacebookService.Domain.AggregatesModel.SampleAggregate;
+
+namespace FacebookService.Domain.Events;
+
+///
+/// EN: Domain event raised when Sample status changes.
+/// VI: Domain event được phát ra khi trạng thái Sample thay đổi.
+///
+public class SampleStatusChangedDomainEvent : INotification
+{
+ ///
+ /// EN: The sample ID.
+ /// VI: ID của sample.
+ ///
+ public Guid SampleId { get; }
+
+ ///
+ /// EN: Previous status before the change.
+ /// VI: Trạng thái trước khi thay đổi.
+ ///
+ public SampleStatus PreviousStatus { get; }
+
+ ///
+ /// EN: New status after the change.
+ /// VI: Trạng thái mới sau khi thay đổi.
+ ///
+ public SampleStatus NewStatus { get; }
+
+ public SampleStatusChangedDomainEvent(
+ Guid sampleId,
+ SampleStatus previousStatus,
+ SampleStatus newStatus)
+ {
+ SampleId = sampleId;
+ PreviousStatus = previousStatus;
+ NewStatus = newStatus;
+ }
+}
diff --git a/services/mkt-facebook-service-net/src/FacebookService.Domain/Exceptions/DomainException.cs b/services/mkt-facebook-service-net/src/FacebookService.Domain/Exceptions/DomainException.cs
new file mode 100644
index 00000000..a5cd80d7
--- /dev/null
+++ b/services/mkt-facebook-service-net/src/FacebookService.Domain/Exceptions/DomainException.cs
@@ -0,0 +1,21 @@
+namespace FacebookService.Domain.Exceptions;
+
+///
+/// EN: Base exception for domain errors.
+/// VI: Exception cơ sở cho các lỗi domain.
+///
+public class DomainException : Exception
+{
+ public DomainException()
+ {
+ }
+
+ public DomainException(string message) : base(message)
+ {
+ }
+
+ public DomainException(string message, Exception innerException)
+ : base(message, innerException)
+ {
+ }
+}
diff --git a/services/mkt-facebook-service-net/src/FacebookService.Domain/Exceptions/SampleDomainException.cs b/services/mkt-facebook-service-net/src/FacebookService.Domain/Exceptions/SampleDomainException.cs
new file mode 100644
index 00000000..f5a85db7
--- /dev/null
+++ b/services/mkt-facebook-service-net/src/FacebookService.Domain/Exceptions/SampleDomainException.cs
@@ -0,0 +1,21 @@
+namespace FacebookService.Domain.Exceptions;
+
+///
+/// EN: Exception for Sample aggregate domain errors.
+/// VI: Exception cho các lỗi domain của Sample aggregate.
+///
+public class SampleDomainException : DomainException
+{
+ public SampleDomainException()
+ {
+ }
+
+ public SampleDomainException(string message) : base(message)
+ {
+ }
+
+ public SampleDomainException(string message, Exception innerException)
+ : base(message, innerException)
+ {
+ }
+}
diff --git a/services/mkt-facebook-service-net/src/FacebookService.Domain/FacebookService.Domain.csproj b/services/mkt-facebook-service-net/src/FacebookService.Domain/FacebookService.Domain.csproj
new file mode 100644
index 00000000..521580ef
--- /dev/null
+++ b/services/mkt-facebook-service-net/src/FacebookService.Domain/FacebookService.Domain.csproj
@@ -0,0 +1,14 @@
+
+
+
+ FacebookService.Domain
+ FacebookService.Domain
+ Domain layer containing core business logic and entities
+
+
+
+
+
+
+
+
diff --git a/services/mkt-facebook-service-net/src/FacebookService.Domain/SeedWork/Entity.cs b/services/mkt-facebook-service-net/src/FacebookService.Domain/SeedWork/Entity.cs
new file mode 100644
index 00000000..22ea2f61
--- /dev/null
+++ b/services/mkt-facebook-service-net/src/FacebookService.Domain/SeedWork/Entity.cs
@@ -0,0 +1,102 @@
+using MediatR;
+
+namespace FacebookService.Domain.SeedWork;
+
+///
+/// EN: Base class for all domain entities.
+/// VI: Lớp cơ sở cho tất cả các entity trong domain.
+///
+public abstract class Entity
+{
+ private int? _requestedHashCode;
+ private Guid _id;
+ private List _domainEvents = new();
+
+ ///
+ /// EN: Unique identifier for the entity.
+ /// VI: Định danh duy nhất cho entity.
+ ///
+ public virtual Guid Id
+ {
+ get => _id;
+ protected set => _id = value;
+ }
+
+ ///
+ /// EN: Domain events raised by this entity.
+ /// VI: Các domain event được phát ra bởi entity này.
+ ///
+ public IReadOnlyCollection DomainEvents => _domainEvents.AsReadOnly();
+
+ ///
+ /// EN: Add a domain event to be dispatched.
+ /// VI: Thêm một domain event để dispatch.
+ ///
+ public void AddDomainEvent(INotification eventItem)
+ {
+ _domainEvents.Add(eventItem);
+ }
+
+ ///
+ /// EN: Remove a domain event.
+ /// VI: Xóa một domain event.
+ ///
+ public void RemoveDomainEvent(INotification eventItem)
+ {
+ _domainEvents.Remove(eventItem);
+ }
+
+ ///
+ /// EN: Clear all domain events.
+ /// VI: Xóa tất cả domain events.
+ ///
+ public void ClearDomainEvents()
+ {
+ _domainEvents.Clear();
+ }
+
+ ///
+ /// EN: Check if entity is transient (not persisted yet).
+ /// VI: Kiểm tra xem entity có phải là transient (chưa lưu) không.
+ ///
+ public bool IsTransient()
+ {
+ return Id == default;
+ }
+
+ public override bool Equals(object? obj)
+ {
+ if (obj is not Entity item)
+ return false;
+
+ if (ReferenceEquals(this, item))
+ return true;
+
+ if (GetType() != item.GetType())
+ return false;
+
+ if (item.IsTransient() || IsTransient())
+ return false;
+
+ return item.Id == Id;
+ }
+
+ public override int GetHashCode()
+ {
+ if (IsTransient())
+ return base.GetHashCode();
+
+ _requestedHashCode ??= Id.GetHashCode() ^ 31;
+ return _requestedHashCode.Value;
+ }
+
+ 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);
+ }
+}
diff --git a/services/mkt-facebook-service-net/src/FacebookService.Domain/SeedWork/Enumeration.cs b/services/mkt-facebook-service-net/src/FacebookService.Domain/SeedWork/Enumeration.cs
new file mode 100644
index 00000000..3ede55d9
--- /dev/null
+++ b/services/mkt-facebook-service-net/src/FacebookService.Domain/SeedWork/Enumeration.cs
@@ -0,0 +1,95 @@
+using System.Reflection;
+
+namespace FacebookService.Domain.SeedWork;
+
+///
+/// EN: Base class for enumeration classes (type-safe enum pattern).
+/// VI: Lớp cơ sở cho các lớp enumeration (pattern enum an toàn kiểu).
+///
+///
+/// EN: This provides a type-safe alternative to enums with additional functionality
+/// like validation, parsing, and rich behavior.
+/// VI: Cung cấp một thay thế an toàn kiểu cho enums với các chức năng bổ sung
+/// như validation, parsing, và hành vi phong phú.
+///
+public abstract class Enumeration : IComparable
+{
+ ///
+ /// EN: The name of the enumeration value.
+ /// VI: Tên của giá trị enumeration.
+ ///
+ public string Name { get; private set; }
+
+ ///
+ /// EN: The unique identifier of the enumeration value.
+ /// VI: Định danh duy nhất của giá trị enumeration.
+ ///
+ public int Id { get; private set; }
+
+ protected Enumeration(int id, string name) => (Id, Name) = (id, name);
+
+ public override string ToString() => Name;
+
+ ///
+ /// EN: Get all enumeration values of a given type.
+ /// VI: Lấy tất cả các giá trị enumeration của một kiểu cho trước.
+ ///
+ public static IEnumerable GetAll() where T : Enumeration =>
+ typeof(T).GetFields(BindingFlags.Public | BindingFlags.Static | BindingFlags.DeclaredOnly)
+ .Select(f => f.GetValue(null))
+ .Cast();
+
+ public override bool Equals(object? obj)
+ {
+ if (obj is not Enumeration otherValue)
+ return false;
+
+ var typeMatches = GetType() == obj.GetType();
+ var valueMatches = Id.Equals(otherValue.Id);
+
+ return typeMatches && valueMatches;
+ }
+
+ public override int GetHashCode() => Id.GetHashCode();
+
+ ///
+ /// EN: Get absolute difference between two enumeration values.
+ /// VI: Lấy sự khác biệt tuyệt đối giữa hai giá trị enumeration.
+ ///
+ public static int AbsoluteDifference(Enumeration firstValue, Enumeration secondValue)
+ {
+ return Math.Abs(firstValue.Id - secondValue.Id);
+ }
+
+ ///
+ /// EN: Parse an integer ID to the corresponding enumeration value.
+ /// VI: Parse một ID integer thành giá trị enumeration tương ứng.
+ ///
+ public static T FromValue(int value) where T : Enumeration
+ {
+ var matchingItem = Parse(value, "value", item => item.Id == value);
+ return matchingItem;
+ }
+
+ ///
+ /// EN: Parse a display name to the corresponding enumeration value.
+ /// VI: Parse một tên hiển thị thành giá trị enumeration tương ứng.
+ ///
+ public static T FromDisplayName(string displayName) where T : Enumeration
+ {
+ var matchingItem = Parse(displayName, "display name", item => item.Name == displayName);
+ return matchingItem;
+ }
+
+ private static T Parse(TValue value, string description, Func predicate) where T : Enumeration
+ {
+ var matchingItem = GetAll().FirstOrDefault(predicate);
+
+ if (matchingItem is null)
+ throw new InvalidOperationException($"'{value}' is not a valid {description} in {typeof(T)}");
+
+ return matchingItem;
+ }
+
+ public int CompareTo(object? other) => Id.CompareTo(((Enumeration)other!).Id);
+}
diff --git a/services/mkt-facebook-service-net/src/FacebookService.Domain/SeedWork/IAggregateRoot.cs b/services/mkt-facebook-service-net/src/FacebookService.Domain/SeedWork/IAggregateRoot.cs
new file mode 100644
index 00000000..5e388e53
--- /dev/null
+++ b/services/mkt-facebook-service-net/src/FacebookService.Domain/SeedWork/IAggregateRoot.cs
@@ -0,0 +1,15 @@
+namespace FacebookService.Domain.SeedWork;
+
+///
+/// EN: Marker interface for aggregate roots.
+/// VI: Interface đánh dấu cho aggregate roots.
+///
+///
+/// EN: Aggregate roots are the entry points to aggregates and are the only objects
+/// that outside code should hold references to.
+/// VI: Aggregate roots là điểm vào của aggregates và là đối tượng duy nhất
+/// mà code bên ngoài nên giữ tham chiếu đến.
+///
+public interface IAggregateRoot
+{
+}
diff --git a/services/mkt-facebook-service-net/src/FacebookService.Domain/SeedWork/IRepository.cs b/services/mkt-facebook-service-net/src/FacebookService.Domain/SeedWork/IRepository.cs
new file mode 100644
index 00000000..51927672
--- /dev/null
+++ b/services/mkt-facebook-service-net/src/FacebookService.Domain/SeedWork/IRepository.cs
@@ -0,0 +1,15 @@
+namespace FacebookService.Domain.SeedWork;
+
+///
+/// EN: Generic repository interface for aggregate roots.
+/// VI: Interface repository generic cho aggregate roots.
+///
+/// EN: The aggregate root type / VI: Kiểu aggregate root
+public interface IRepository where T : IAggregateRoot
+{
+ ///
+ /// EN: The unit of work for this repository.
+ /// VI: Unit of work cho repository này.
+ ///
+ IUnitOfWork UnitOfWork { get; }
+}
diff --git a/services/mkt-facebook-service-net/src/FacebookService.Domain/SeedWork/IUnitOfWork.cs b/services/mkt-facebook-service-net/src/FacebookService.Domain/SeedWork/IUnitOfWork.cs
new file mode 100644
index 00000000..73c3ce77
--- /dev/null
+++ b/services/mkt-facebook-service-net/src/FacebookService.Domain/SeedWork/IUnitOfWork.cs
@@ -0,0 +1,30 @@
+namespace FacebookService.Domain.SeedWork;
+
+///
+/// EN: Unit of Work pattern interface.
+/// VI: Interface cho Unit of Work pattern.
+///
+///
+/// EN: Maintains a list of objects affected by a business transaction
+/// and coordinates the writing out of changes.
+/// VI: Duy trì danh sách các đối tượng bị ảnh hưởng bởi một transaction nghiệp vụ
+/// và điều phối việc ghi các thay đổi.
+///
+public interface IUnitOfWork : IDisposable
+{
+ ///
+ /// EN: Save all changes made in this unit of work.
+ /// VI: Lưu tất cả các thay đổi được thực hiện trong unit of work này.
+ ///
+ /// EN: Cancellation token / VI: Token hủy
+ /// EN: Number of entities written / VI: Số entity đã ghi
+ Task SaveChangesAsync(CancellationToken cancellationToken = default);
+
+ ///
+ /// EN: Save all changes and dispatch domain events.
+ /// VI: Lưu tất cả thay đổi và dispatch domain events.
+ ///
+ /// EN: Cancellation token / VI: Token hủy
+ /// EN: True if successful / VI: True nếu thành công
+ Task SaveEntitiesAsync(CancellationToken cancellationToken = default);
+}
diff --git a/services/mkt-facebook-service-net/src/FacebookService.Domain/SeedWork/ValueObject.cs b/services/mkt-facebook-service-net/src/FacebookService.Domain/SeedWork/ValueObject.cs
new file mode 100644
index 00000000..0940abed
--- /dev/null
+++ b/services/mkt-facebook-service-net/src/FacebookService.Domain/SeedWork/ValueObject.cs
@@ -0,0 +1,53 @@
+namespace FacebookService.Domain.SeedWork;
+
+///
+/// EN: Base class for Value Objects following DDD patterns.
+/// VI: Lớp cơ sở cho Value Objects theo mẫu DDD.
+///
+///
+/// EN: Value objects are immutable and compared by their values, not identity.
+/// VI: Value objects là bất biến và được so sánh theo giá trị, không phải định danh.
+///
+public abstract class ValueObject
+{
+ ///
+ /// EN: Get the atomic values that make up this value object.
+ /// VI: Lấy các giá trị nguyên tử tạo nên value object này.
+ ///
+ protected abstract IEnumerable