diff --git a/.editorconfig b/.editorconfig
index f773573..7dc9cbf 100644
--- a/.editorconfig
+++ b/.editorconfig
@@ -69,6 +69,12 @@ csharp_style_expression_bodied_accessors = true:error
csharp_style_expression_bodied_lambdas = true:error
csharp_style_expression_bodied_local_functions = true:error
+# S1186: Methods should not be empty
+dotnet_diagnostic.S1186.severity = warning
+
+# CA1303: Do not pass literals as localized parameters
+dotnet_diagnostic.CA1303.severity = none
+
[*.{cs,vb}]
dotnet_style_prefer_simplified_boolean_expressions = true:suggestion
dotnet_style_prefer_simplified_interpolation = true:suggestion
diff --git a/.github/dependabot.yml b/.github/dependabot.yml
new file mode 100644
index 0000000..bc18f00
--- /dev/null
+++ b/.github/dependabot.yml
@@ -0,0 +1,6 @@
+version: 2
+updates:
+ - package-ecosystem: "nuget"
+ directory: "/"
+ schedule:
+ interval: "daily"
diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
new file mode 100644
index 0000000..7f55c4d
--- /dev/null
+++ b/.github/workflows/build.yml
@@ -0,0 +1,66 @@
+name: Build
+
+on:
+ workflow_dispatch:
+ push:
+ branches:
+ - main
+
+ # Pull requests with specific types will trigger the build action
+ pull_request:
+ branches:
+ - main
+ types:
+ - opened
+ - synchronize
+ - reopened
+ - ready_for_review
+
+env:
+ DOTNET_VERSION: "8.x"
+
+jobs:
+ build:
+ runs-on: ubuntu-latest
+ strategy:
+ matrix:
+ dotnet-version: ['8.x']
+ os: [ubuntu-latest]
+ configuration: [Release]
+
+ steps:
+ - uses: actions/checkout@v4
+
+ - name: Setup .NET
+ uses: actions/setup-dotnet@v4
+ with:
+ dotnet-version: ${{ env.DOTNET_VERSION }}
+
+ - name: Cache NuGet packages
+ uses: actions/cache@v4
+ with:
+ path: ~/.nuget/packages
+ key: ${{ runner.os }}-nuget-${{ hashFiles('**/*.csproj') }}
+ restore-keys: |
+ ${{ runner.os }}-nuget-
+
+ - name: Restore
+ run: dotnet restore LegalAssistant.AppService.sln
+
+ - name: Build
+ run: dotnet build LegalAssistant.AppService.sln --configuration ${{ matrix.configuration }} --no-restore
+
+ - name: Test
+ run: dotnet test LegalAssistant.AppService.sln --configuration ${{ matrix.configuration }} --no-restore --no-build --verbosity normal --collect:"XPlat Code Coverage"
+
+ - name: Code Analysis
+ run: dotnet build LegalAssistant.AppService.sln --configuration ${{ matrix.configuration }} --verbosity normal /p:TreatWarningsAsErrors=true
+
+ - name: Publish Web API
+ run: dotnet publish src/Web.Api/Web.Api.csproj --configuration ${{ matrix.configuration }} --no-restore --no-build --output ./publish
+
+ - name: Upload Build Artifacts
+ uses: actions/upload-artifact@v4
+ with:
+ name: published-app
+ path: ./publish
diff --git a/BUILD_SCRIPTS.md b/BUILD_SCRIPTS.md
new file mode 100644
index 0000000..1328a9b
--- /dev/null
+++ b/BUILD_SCRIPTS.md
@@ -0,0 +1,289 @@
+# 🏗️ Build Scripts - Legal Assistant App
+
+Hướng dẫn build project với SonarAnalyzer để check lỗi code quality như trên CI/CD.
+
+## 📋 Mục lục
+- [Build All Projects (Solution)](#-build-all-projects-solution)
+- [Build Individual Projects](#-build-individual-projects)
+- [Quick Commands](#-quick-commands)
+- [PowerShell Scripts](#-powershell-scripts)
+- [Troubleshooting](#-troubleshooting)
+
+---
+
+## 🎯 Build All Projects (Solution)
+
+### Debug Build (Development)
+```bash
+# Build nhanh cho development
+dotnet build LegalAssistant.AppService.sln --configuration Debug
+```
+
+### Release Build (Production/CI-like)
+```bash
+# Build giống CI/CD với SonarAnalyzer enabled
+dotnet build LegalAssistant.AppService.sln --configuration Release --verbosity normal /p:TreatWarningsAsErrors=true
+```
+
+### Full CI/CD Simulation
+```bash
+# Restore packages
+dotnet restore LegalAssistant.AppService.sln
+
+# Build with strict analysis
+dotnet build LegalAssistant.AppService.sln --configuration Release --no-restore --verbosity normal /p:TreatWarningsAsErrors=true
+
+# Run tests
+dotnet test LegalAssistant.AppService.sln --configuration Release --no-restore --no-build --verbosity normal
+
+# Code Analysis (same as CI/CD)
+dotnet build LegalAssistant.AppService.sln --configuration Release --verbosity normal /p:TreatWarningsAsErrors=true
+```
+
+---
+
+## 🔧 Build Individual Projects
+
+### Domain Layer
+```bash
+# Check Domain layer issues
+dotnet build src/Domain/Domain.csproj --configuration Release /p:TreatWarningsAsErrors=true
+```
+
+### Application Layer
+```bash
+# Check Application layer issues
+dotnet build src/Application/Application.csproj --configuration Release /p:TreatWarningsAsErrors=true
+```
+
+### Infrastructure Layer
+```bash
+# Check Infrastructure layer issues
+dotnet build src/Infrastructure/Infrastructure.csproj --configuration Release /p:TreatWarningsAsErrors=true
+```
+
+### Web API
+```bash
+# Check Web API issues
+dotnet build src/Web.Api/Web.Api.csproj --configuration Release /p:TreatWarningsAsErrors=true
+```
+
+---
+
+## ⚡ Quick Commands
+
+### Check All SonarAnalyzer Issues
+```bash
+dotnet build --configuration Release /p:TreatWarningsAsErrors=true
+```
+
+### Build & Test Pipeline
+```bash
+dotnet restore && dotnet build --configuration Release /p:TreatWarningsAsErrors=true && dotnet test --no-build --configuration Release
+```
+
+### Clean & Full Rebuild
+```bash
+dotnet clean && dotnet restore && dotnet build --configuration Release /p:TreatWarningsAsErrors=true
+```
+
+---
+
+## 📜 PowerShell Scripts
+
+### Build Script (`build.ps1`)
+```powershell
+# Save this as build.ps1
+param(
+ [string]$Configuration = "Release",
+ [switch]$SkipTests,
+ [switch]$Verbose
+)
+
+Write-Host "🏗️ Building Legal Assistant App..." -ForegroundColor Green
+
+# Set verbosity
+$verbosity = if ($Verbose) { "normal" } else { "minimal" }
+
+try {
+ # Restore packages
+ Write-Host "📦 Restoring packages..." -ForegroundColor Yellow
+ dotnet restore LegalAssistant.AppService.sln
+
+ if ($LASTEXITCODE -ne 0) { throw "Restore failed" }
+
+ # Build solution
+ Write-Host "🔨 Building solution..." -ForegroundColor Yellow
+ dotnet build LegalAssistant.AppService.sln --configuration $Configuration --no-restore --verbosity $verbosity /p:TreatWarningsAsErrors=true
+
+ if ($LASTEXITCODE -ne 0) { throw "Build failed" }
+
+ # Run tests (optional)
+ if (-not $SkipTests) {
+ Write-Host "🧪 Running tests..." -ForegroundColor Yellow
+ dotnet test LegalAssistant.AppService.sln --configuration $Configuration --no-restore --no-build --verbosity $verbosity
+
+ if ($LASTEXITCODE -ne 0) { throw "Tests failed" }
+ }
+
+ Write-Host "✅ Build completed successfully!" -ForegroundColor Green
+}
+catch {
+ Write-Host "❌ Build failed: $_" -ForegroundColor Red
+ exit 1
+}
+```
+
+### Quick Check Script (`check.ps1`)
+```powershell
+# Save this as check.ps1
+Write-Host "🔍 Quick SonarAnalyzer Check..." -ForegroundColor Cyan
+
+dotnet build --configuration Release /p:TreatWarningsAsErrors=true
+
+if ($LASTEXITCODE -eq 0) {
+ Write-Host "✅ No issues found!" -ForegroundColor Green
+} else {
+ Write-Host "❌ Issues detected. Fix them before CI/CD." -ForegroundColor Red
+}
+```
+
+### Project-specific Script (`build-project.ps1`)
+```powershell
+# Save this as build-project.ps1
+param(
+ [Parameter(Mandatory=$true)]
+ [ValidateSet("Domain", "Application", "Infrastructure", "Web.Api")]
+ [string]$Project
+)
+
+$projectPath = "src/$Project/$Project.csproj"
+Write-Host "🏗️ Building $Project..." -ForegroundColor Green
+
+dotnet build $projectPath --configuration Release /p:TreatWarningsAsErrors=true
+
+if ($LASTEXITCODE -eq 0) {
+ Write-Host "✅ $Project build successful!" -ForegroundColor Green
+} else {
+ Write-Host "❌ $Project build failed!" -ForegroundColor Red
+}
+```
+
+---
+
+## 🐛 Troubleshooting
+
+### Common SonarAnalyzer Issues
+
+#### S1186: Empty Methods
+```csharp
+// ❌ BAD
+public void ProcessData() { }
+
+// ✅ GOOD
+public void ProcessData()
+{
+ // Intentionally empty - placeholder for future implementation
+}
+```
+
+#### S1481: Unused Variables
+```csharp
+// ❌ BAD
+var result = GetData();
+
+// ✅ GOOD
+var result = GetData();
+Console.WriteLine($"Result: {result}");
+```
+
+#### S3400: Methods Returning Constants
+```csharp
+// ❌ BAD
+private bool IsEnabled() => false;
+
+// ✅ GOOD
+private const bool DefaultEnabledStatus = false;
+private bool IsEnabled() => DefaultEnabledStatus;
+```
+
+### Disable Specific Rules Temporarily
+```csharp
+#pragma warning disable S1186 // Empty methods
+public void PlaceholderMethod() { }
+#pragma warning restore S1186
+```
+
+### Suppress Rules in .editorconfig
+```ini
+# Add to .editorconfig for project-wide suppression
+dotnet_diagnostic.S1186.severity = none
+dotnet_diagnostic.S1481.severity = none
+```
+
+---
+
+## 📊 CI/CD Integration
+
+### GitHub Actions Equivalent
+```yaml
+# This is what runs on CI/CD
+- name: Build
+ run: dotnet build LegalAssistant.AppService.sln --configuration Release --no-restore
+
+- name: Code Analysis
+ run: dotnet build LegalAssistant.AppService.sln --configuration Release --verbosity normal /p:TreatWarningsAsErrors=true
+```
+
+### Local Testing Before Push
+```bash
+# Run this before committing to ensure CI/CD will pass
+dotnet restore LegalAssistant.AppService.sln
+dotnet build LegalAssistant.AppService.sln --configuration Release --verbosity normal /p:TreatWarningsAsErrors=true
+dotnet test LegalAssistant.AppService.sln --configuration Release --no-restore --no-build
+```
+
+---
+
+## 🎯 Usage Examples
+
+### Daily Development
+```bash
+# Quick check during development
+./check.ps1
+
+# Or using dotnet directly
+dotnet build --configuration Release /p:TreatWarningsAsErrors=true
+```
+
+### Before Committing
+```bash
+# Full validation
+./build.ps1 -Verbose
+
+# Or manual steps
+dotnet clean
+dotnet restore
+dotnet build --configuration Release /p:TreatWarningsAsErrors=true
+dotnet test --configuration Release --no-build
+```
+
+### Project-specific Issues
+```bash
+# Check specific layer
+./build-project.ps1 -Project Domain
+./build-project.ps1 -Project Application
+./build-project.ps1 -Project Infrastructure
+./build-project.ps1 -Project Web.Api
+```
+
+---
+
+## 📝 Notes
+
+- **Release Configuration**: Bắt buộc để enable tất cả SonarAnalyzer rules như CI/CD
+- **TreatWarningsAsErrors**: Biến warnings thành errors để match CI/CD behavior
+- **Verbosity**: Dùng `normal` để thấy chi tiết lỗi, `minimal` cho output ngắn gọn
+- **No-restore/No-build**: Tối ưu performance khi chạy nhiều commands liên tiếp
+
+💡 **Tip**: Bookmark file này và chạy `check.ps1` thường xuyên để tránh surprise trên CI/CD!
diff --git a/Directory.Build.props b/Directory.Build.props
index f588c79..7a4ddf0 100644
--- a/Directory.Build.props
+++ b/Directory.Build.props
@@ -14,10 +14,10 @@
true
true
-
+
\ No newline at end of file
diff --git a/EXCEPTION_GUIDELINES.md b/EXCEPTION_GUIDELINES.md
new file mode 100644
index 0000000..e7cf098
--- /dev/null
+++ b/EXCEPTION_GUIDELINES.md
@@ -0,0 +1,644 @@
+# 🚨 Exception Guidelines - Legal Assistant App
+
+Hướng dẫn sử dụng Exception trong Clean Architecture với các tình huống cụ thể.
+
+## 📋 Mục lục
+- [Exception Hierarchy](#-exception-hierarchy)
+- [Built-in .NET Exceptions](#-built-in-net-exceptions)
+- [Custom Exceptions](#-custom-exceptions)
+- [Usage by Layer](#-usage-by-layer)
+- [Best Practices](#-best-practices)
+- [Examples](#-examples)
+
+---
+
+## 🏗️ Exception Hierarchy
+
+```
+Exception
+├── SystemException
+│ ├── ArgumentException
+│ │ ├── ArgumentNullException
+│ │ └── ArgumentOutOfRangeException
+│ ├── InvalidOperationException
+│ ├── NotSupportedException
+│ └── NotImplementedException
+└── ApplicationException (Custom base)
+ ├── ValidationException
+ ├── NotFoundException
+ ├── UnauthorizedException
+ ├── BusinessRuleViolationException
+ ├── RequestProcessingException
+ └── DomainEventDispatchException
+```
+
+---
+
+## 🔧 Built-in .NET Exceptions
+
+### **ArgumentException Family**
+```csharp
+// ArgumentNullException - Parameter is null
+public void CreateUser(string email)
+{
+ if (email is null)
+ throw new ArgumentNullException(nameof(email));
+}
+
+// ArgumentException - Invalid parameter value
+public void SetAge(int age)
+{
+ if (age < 0)
+ throw new ArgumentException("Age cannot be negative", nameof(age));
+}
+
+// ArgumentOutOfRangeException - Parameter out of valid range
+public void SetPage(int page)
+{
+ if (page < 1 || page > 1000)
+ throw new ArgumentOutOfRangeException(nameof(page), "Page must be between 1 and 1000");
+}
+```
+
+### **InvalidOperationException**
+```csharp
+// Object state issues
+public void StartGame()
+{
+ if (IsGameRunning)
+ throw new InvalidOperationException("Game is already running");
+}
+
+// Method call sequence issues
+public void SendMessage()
+{
+ if (!IsConnected)
+ throw new InvalidOperationException("Must connect before sending messages");
+}
+
+// Collection state issues
+public T Dequeue()
+{
+ if (Count == 0)
+ throw new InvalidOperationException("Queue is empty");
+}
+```
+
+### **NotSupportedException**
+```csharp
+// Feature not supported
+public void ReadOnlyOperation()
+{
+ throw new NotSupportedException("This collection is read-only");
+}
+```
+
+### **NotImplementedException**
+```csharp
+// Placeholder for future implementation
+public void FutureFeature()
+{
+ throw new NotImplementedException("Feature will be implemented in v2.0");
+}
+```
+
+---
+
+## 🎯 Custom Exceptions
+
+### **Domain Layer Exceptions**
+
+#### **BusinessRuleViolationException**
+```csharp
+namespace Domain.Exceptions;
+
+public sealed class BusinessRuleViolationException : Exception
+{
+ public string RuleName { get; }
+
+ public BusinessRuleViolationException(string ruleName, string message)
+ : base(message)
+ {
+ RuleName = ruleName;
+ }
+}
+
+// Usage
+if (user.Age < 18)
+ throw new BusinessRuleViolationException("MinimumAge", "User must be at least 18 years old");
+```
+
+### **Application Layer Exceptions**
+
+#### **ValidationException**
+```csharp
+namespace Application.Common.Exceptions;
+
+public sealed class ValidationException : Exception
+{
+ public Dictionary Errors { get; }
+
+ public ValidationException(string message) : base(message)
+ {
+ Errors = new Dictionary();
+ }
+
+ public ValidationException(Dictionary errors)
+ : base("One or more validation failures occurred")
+ {
+ Errors = errors;
+ }
+}
+
+// Usage
+var errors = new Dictionary
+{
+ ["Email"] = new[] { "Email is required", "Email format is invalid" },
+ ["Password"] = new[] { "Password must be at least 8 characters" }
+};
+throw new ValidationException(errors);
+```
+
+#### **NotFoundException**
+```csharp
+namespace Application.Common.Exceptions;
+
+public sealed class NotFoundException : Exception
+{
+ public NotFoundException(string entityName, object key)
+ : base($"{entityName} with key '{key}' was not found")
+ {
+ }
+
+ public NotFoundException(string message) : base(message)
+ {
+ }
+}
+
+// Usage
+if (user == null)
+ throw new NotFoundException("User", userId);
+```
+
+#### **UnauthorizedException**
+```csharp
+namespace Application.Common.Exceptions;
+
+public sealed class UnauthorizedException : Exception
+{
+ public string? Permission { get; }
+
+ public UnauthorizedException(string message) : base(message)
+ {
+ }
+
+ public UnauthorizedException(string message, string permission) : base(message)
+ {
+ Permission = permission;
+ }
+}
+
+// Usage
+if (!user.HasRole("Admin"))
+ throw new UnauthorizedException("Access denied", "Admin");
+```
+
+#### **RequestProcessingException**
+```csharp
+namespace Application.Common.Exceptions;
+
+public sealed class RequestProcessingException : Exception
+{
+ public RequestProcessingException(string message) : base(message)
+ {
+ }
+
+ public RequestProcessingException(string message, Exception innerException)
+ : base(message, innerException)
+ {
+ }
+}
+
+// Usage in MediatR behaviors
+catch (Exception ex)
+{
+ throw new RequestProcessingException($"Request {requestName} failed after {duration}ms", ex);
+}
+```
+
+### **Infrastructure Layer Exceptions**
+
+#### **DomainEventDispatchException**
+```csharp
+namespace Infrastructure.Exceptions;
+
+public sealed class DomainEventDispatchException : Exception
+{
+ public DomainEventDispatchException(string message) : base(message)
+ {
+ }
+
+ public DomainEventDispatchException(string message, Exception innerException)
+ : base(message, innerException)
+ {
+ }
+}
+
+// Usage
+catch (Exception ex)
+{
+ throw new DomainEventDispatchException($"Failed to dispatch {eventType}", ex);
+}
+```
+
+#### **DataPersistenceException**
+```csharp
+namespace Infrastructure.Exceptions;
+
+public sealed class DataPersistenceException : Exception
+{
+ public DataPersistenceException(string message) : base(message)
+ {
+ }
+
+ public DataPersistenceException(string message, Exception innerException)
+ : base(message, innerException)
+ {
+ }
+}
+
+// Usage
+try
+{
+ await context.SaveChangesAsync();
+}
+catch (DbUpdateException ex)
+{
+ throw new DataPersistenceException("Failed to save changes to database", ex);
+}
+```
+
+---
+
+## 🏛️ Usage by Layer
+
+### **Domain Layer**
+```csharp
+// ✅ Use for business rules
+throw new BusinessRuleViolationException("UserAge", "User must be 18+");
+throw new InvalidOperationException("Cannot deactivate user with active subscriptions");
+
+// ✅ Use for invariant violations
+if (string.IsNullOrEmpty(Email))
+ throw new ArgumentException("Email cannot be empty", nameof(Email));
+
+// ❌ Don't use infrastructure exceptions
+// throw new SqlException(...); // WRONG LAYER
+```
+
+### **Application Layer**
+```csharp
+// ✅ Use for validation
+throw new ValidationException("Invalid input data");
+
+// ✅ Use for authorization
+throw new UnauthorizedException("Insufficient permissions");
+
+// ✅ Use for not found
+throw new NotFoundException("User", userId);
+
+// ✅ Use for request processing
+throw new RequestProcessingException("Command processing failed", ex);
+
+// ❌ Don't use low-level exceptions
+// throw new SqlException(...); // TOO LOW LEVEL
+```
+
+### **Infrastructure Layer**
+```csharp
+// ✅ Use for data persistence
+throw new DataPersistenceException("Database save failed", ex);
+
+// ✅ Use for event dispatching
+throw new DomainEventDispatchException("Event dispatch failed", ex);
+
+// ✅ Wrap low-level exceptions
+try { /* database call */ }
+catch (SqlException ex)
+{
+ throw new DataPersistenceException("SQL operation failed", ex);
+}
+
+// ❌ Don't leak infrastructure details
+// throw new SqlException(...); // DON'T LEAK TO UPPER LAYERS
+```
+
+### **Web API Layer**
+```csharp
+// ✅ Catch and convert to HTTP responses
+try
+{
+ await mediator.Send(command);
+}
+catch (ValidationException ex)
+{
+ return BadRequest(ex.Errors); // 400
+}
+catch (NotFoundException ex)
+{
+ return NotFound(ex.Message); // 404
+}
+catch (UnauthorizedException ex)
+{
+ return Unauthorized(ex.Message); // 401
+}
+catch (Exception ex)
+{
+ return StatusCode(500, "Internal server error"); // 500
+}
+```
+
+---
+
+## 🎯 Best Practices
+
+### **1. Exception Naming**
+```csharp
+// ✅ GOOD - Descriptive and specific
+ValidationException
+NotFoundException
+UserNotFoundexception
+InvalidEmailFormatException
+
+// ❌ BAD - Generic and vague
+Exception
+ErrorException
+BadDataException
+```
+
+### **2. Exception Messages**
+```csharp
+// ✅ GOOD - Specific and actionable
+"User with ID '12345' was not found"
+"Email format is invalid. Expected format: user@domain.com"
+"Password must contain at least one uppercase letter"
+
+// ❌ BAD - Vague and unhelpful
+"Error occurred"
+"Invalid data"
+"Something went wrong"
+```
+
+### **3. Inner Exceptions**
+```csharp
+// ✅ GOOD - Preserve original exception
+try
+{
+ await database.SaveAsync();
+}
+catch (SqlException ex)
+{
+ throw new DataPersistenceException("Failed to save user data", ex);
+}
+
+// ❌ BAD - Lose original exception information
+catch (SqlException ex)
+{
+ throw new DataPersistenceException("Failed to save user data");
+}
+```
+
+### **4. Exception Properties**
+```csharp
+// ✅ GOOD - Include relevant context
+public class ValidationException : Exception
+{
+ public Dictionary Errors { get; }
+ public string FieldName { get; }
+ // ... constructors
+}
+
+// ❌ BAD - No additional context
+public class ValidationException : Exception
+{
+ // Only message, no additional info
+}
+```
+
+---
+
+## 💡 Examples
+
+### **User Registration Flow**
+```csharp
+// Domain Layer
+public static User Create(string email, string password)
+{
+ // Argument validation
+ if (string.IsNullOrEmpty(email))
+ throw new ArgumentException("Email cannot be empty", nameof(email));
+
+ // Business rule validation
+ if (!IsValidEmailFormat(email))
+ throw new BusinessRuleViolationException("EmailFormat", "Email format is invalid");
+
+ if (password.Length < 8)
+ throw new BusinessRuleViolationException("PasswordLength", "Password must be at least 8 characters");
+
+ return new User(email, password);
+}
+
+// Application Layer
+public async Task CreateUserAsync(CreateUserCommand command)
+{
+ // Check if user already exists
+ var existingUser = await userRepository.GetByEmailAsync(command.Email);
+ if (existingUser != null)
+ throw new ValidationException("User with this email already exists");
+
+ // Authorization check
+ if (!currentUser.HasRole("Admin"))
+ throw new UnauthorizedException("Only admins can create users");
+
+ try
+ {
+ var user = User.Create(command.Email, command.Password);
+ await userRepository.AddAsync(user);
+ return user;
+ }
+ catch (BusinessRuleViolationException)
+ {
+ throw; // Re-throw domain exceptions as-is
+ }
+ catch (Exception ex)
+ {
+ throw new RequestProcessingException("Failed to create user", ex);
+ }
+}
+
+// Infrastructure Layer
+public async Task AddAsync(User user)
+{
+ try
+ {
+ context.Users.Add(user);
+ await context.SaveChangesAsync();
+ }
+ catch (DbUpdateException ex) when (ex.InnerException is SqlException sqlEx && sqlEx.Number == 2627)
+ {
+ throw new DataPersistenceException("User with this email already exists", ex);
+ }
+ catch (DbUpdateException ex)
+ {
+ throw new DataPersistenceException("Failed to save user to database", ex);
+ }
+}
+
+// Web API Layer
+[HttpPost]
+public async Task CreateUser(CreateUserCommand command)
+{
+ try
+ {
+ var user = await mediator.Send(command);
+ return CreatedAtAction(nameof(GetUser), new { id = user.Id }, user);
+ }
+ catch (ValidationException ex)
+ {
+ return BadRequest(new { message = ex.Message });
+ }
+ catch (UnauthorizedException ex)
+ {
+ return Unauthorized(new { message = ex.Message });
+ }
+ catch (BusinessRuleViolationException ex)
+ {
+ return BadRequest(new { rule = ex.RuleName, message = ex.Message });
+ }
+ catch (RequestProcessingException ex)
+ {
+ logger.LogError(ex, "Failed to process create user request");
+ return StatusCode(500, new { message = "Internal server error" });
+ }
+}
+```
+
+### **Global Exception Handler**
+```csharp
+public class GlobalExceptionMiddleware
+{
+ public async Task InvokeAsync(HttpContext context)
+ {
+ try
+ {
+ await next(context);
+ }
+ catch (Exception ex)
+ {
+ await HandleExceptionAsync(context, ex);
+ }
+ }
+
+ private static async Task HandleExceptionAsync(HttpContext context, Exception exception)
+ {
+ var response = exception switch
+ {
+ ValidationException validationEx => new ApiResponse
+ {
+ StatusCode = 400,
+ Message = "Validation failed",
+ Errors = validationEx.Errors
+ },
+ NotFoundException notFoundEx => new ApiResponse
+ {
+ StatusCode = 404,
+ Message = notFoundEx.Message
+ },
+ UnauthorizedException unauthorizedEx => new ApiResponse
+ {
+ StatusCode = 401,
+ Message = unauthorizedEx.Message
+ },
+ BusinessRuleViolationException businessEx => new ApiResponse
+ {
+ StatusCode = 400,
+ Message = businessEx.Message,
+ Details = new { Rule = businessEx.RuleName }
+ },
+ RequestProcessingException requestEx => new ApiResponse
+ {
+ StatusCode = 500,
+ Message = "Request processing failed"
+ },
+ _ => new ApiResponse
+ {
+ StatusCode = 500,
+ Message = "An unexpected error occurred"
+ }
+ };
+
+ context.Response.StatusCode = response.StatusCode;
+ await context.Response.WriteAsync(JsonSerializer.Serialize(response));
+ }
+}
+```
+
+---
+
+## 📊 Exception Decision Tree
+
+```
+Exception occurred?
+├── Is it invalid input parameter?
+│ ├── Null → ArgumentNullException
+│ ├── Wrong format → ArgumentException
+│ └── Out of range → ArgumentOutOfRangeException
+├── Is it object state issue?
+│ └── Wrong state/sequence → InvalidOperationException
+├── Is it business rule violation?
+│ └── Domain rule broken → BusinessRuleViolationException
+├── Is it validation failure?
+│ └── Input validation → ValidationException
+├── Is it authorization failure?
+│ └── Permission denied → UnauthorizedException
+├── Is it data not found?
+│ └── Entity missing → NotFoundException
+├── Is it infrastructure failure?
+│ ├── Database → DataPersistenceException
+│ ├── Event dispatch → DomainEventDispatchException
+│ └── External service → ExternalServiceException
+└── Is it request processing failure?
+ └── Pipeline error → RequestProcessingException
+```
+
+---
+
+## 🚀 Quick Reference
+
+| **Scenario** | **Exception Type** | **Layer** |
+|:-------------|:-------------------|:----------|
+| Null parameter | `ArgumentNullException` | Any |
+| Invalid parameter | `ArgumentException` | Any |
+| Wrong object state | `InvalidOperationException` | Domain/Application |
+| Business rule violation | `BusinessRuleViolationException` | Domain |
+| Input validation failure | `ValidationException` | Application |
+| Authorization failure | `UnauthorizedException` | Application |
+| Entity not found | `NotFoundException` | Application |
+| Database operation failure | `DataPersistenceException` | Infrastructure |
+| Event dispatch failure | `DomainEventDispatchException` | Infrastructure |
+| Request processing failure | `RequestProcessingException` | Application |
+
+---
+
+## 💡 Pro Tips
+
+1. **🎯 Be Specific**: Use the most specific exception type available
+2. **📝 Include Context**: Add relevant properties and detailed messages
+3. **🔗 Preserve Stack Trace**: Always include inner exceptions
+4. **🏗️ Layer Appropriate**: Use exceptions appropriate for each layer
+5. **📊 Centralize Handling**: Use global exception handlers in Web API
+6. **🔍 Log Strategically**: Log at the appropriate level (Error/Warning/Info)
+7. **🚫 Don't Swallow**: Never catch and ignore exceptions without good reason
+8. **🎭 Custom When Needed**: Create custom exceptions for domain-specific scenarios
+
+**Remember: Exceptions should be exceptional! Use them for error conditions, not control flow.** 🎯
diff --git a/docs/AGGREGATE_EXPLAINED.md b/docs/AGGREGATE_EXPLAINED.md
new file mode 100644
index 0000000..0246e6d
--- /dev/null
+++ b/docs/AGGREGATE_EXPLAINED.md
@@ -0,0 +1,296 @@
+# 🏗️ Aggregate - Giải thích từ A-Z
+
+## 🤔 **AGGREGATE LÀ GÌ?**
+
+**Aggregate = "Nhóm các entities liên quan + business logic quản lý chúng"**
+
+### **🏠 VÍ DỤ ĐỜI THƯỜNG:**
+
+Tưởng tượng **Aggregate** như một **ngôi nhà**:
+
+```
+🏠 Ngôi nhà (UserAggregate)
+├── 👤 Chủ nhà (User entity) ← Aggregate Root
+├── 📱 Số điện thoại (Phone entity)
+├── 📧 Email (Email entity)
+├── 🏠 Địa chỉ (Address entity)
+└── 🔑 Quy tắc nhà (Business rules)
+```
+
+**Quy tắc quan trọng:**
+- ✅ **Chỉ được giao tiếp với chủ nhà** (Aggregate Root)
+- ❌ **Không được nói chuyện trực tiếp với phone/email** (Child entities)
+- 🔒 **Chủ nhà quyết định mọi thay đổi** trong nhà
+
+## 🎯 **TẠI SAO CẦN AGGREGATE?**
+
+### **❌ Không có Aggregate - Chaos:**
+```csharp
+// BAD: Ai cũng có thể sửa trực tiếp entities
+user.Email = "new@email.com"; // ← Không validate
+user.Phone = "invalid"; // ← Không check format
+user.Address.Street = ""; // ← Không check business rules
+// Result: Data inconsistent, broken business rules
+```
+
+### **✅ Có Aggregate - Controlled:**
+```csharp
+// GOOD: Chỉ được thay đổi qua Aggregate
+userAggregate.UpdateContactInfo("new@email.com", "0123456789");
+// ← Aggregate validates everything, ensures consistency
+```
+
+## 🏗️ **AGGREGATE TRONG CODE**
+
+### **📊 So sánh Entity vs Aggregate:**
+
+#### **🏛️ User Entity (Dumb Data)**
+```csharp
+// Domain/Entities/User.cs
+public sealed class User : BaseEntity
+{
+ public required string Email { get; set; }
+ public required string FullName { get; set; }
+ public required string PasswordHash { get; set; }
+ public UserRole[] Roles { get; set; } = [];
+
+ // Chỉ có basic methods
+ public bool HasRole(UserRole role) => Roles.Contains(role);
+ public bool IsActive => !IsDeleted;
+}
+```
+
+#### **🏗️ User Aggregate (Smart Orchestrator)**
+```csharp
+// Domain/Aggregates/User/UserAggregate.cs
+public sealed class UserAggregate : BaseAggregateRoot
+{
+ private readonly User _user; // ← Wraps entity
+
+ // ✅ CONTROLLED CREATION
+ public static UserAggregate Create(string email, string fullName, string password)
+ {
+ // 🛡️ Business validation
+ if (string.IsNullOrWhiteSpace(email))
+ throw new ArgumentException("Email required");
+
+ if (!IsValidEmail(email))
+ throw new ArgumentException("Invalid email format");
+
+ if (password.Length < 8)
+ throw new ArgumentException("Password too short");
+
+ // 🏗️ Create entity
+ var user = new User
+ {
+ Email = email.ToLowerInvariant(), // ← Normalize
+ FullName = fullName.Trim(), // ← Clean
+ PasswordHash = HashPassword(password), // ← Hash
+ Roles = [UserRole.User] // ← Default
+ };
+
+ var aggregate = new UserAggregate(user);
+
+ // 📢 Raise domain event
+ aggregate.AddDomainEvent(new UserCreatedEvent(user.Id, user.Email));
+
+ return aggregate;
+ }
+
+ // ✅ CONTROLLED UPDATES
+ public void UpdateContactInfo(string newEmail, string newPhone)
+ {
+ // 🛡️ Business rules
+ if (_user.IsDeleted)
+ throw new InvalidOperationException("Cannot update deleted user");
+
+ if (!IsValidEmail(newEmail))
+ throw new ArgumentException("Invalid email");
+
+ // 🔄 Update entity
+ _user.Email = newEmail.ToLowerInvariant();
+ _user.PhoneNumber = newPhone;
+ _user.UpdatedAt = DateTime.UtcNow;
+
+ // 📢 Raise event
+ AddDomainEvent(new UserContactUpdatedEvent(_user.Id, newEmail, newPhone));
+ }
+
+ // ✅ CONTROLLED DEACTIVATION
+ public void Deactivate(string reason)
+ {
+ // 🛡️ Business rules
+ if (HasActiveSubscriptions())
+ throw new InvalidOperationException("Cannot deactivate user with active subscriptions");
+
+ if (_user.Roles.Contains(UserRole.Admin) && IsLastAdmin())
+ throw new InvalidOperationException("Cannot deactivate last admin");
+
+ // 🔄 Soft delete
+ _user.IsDeleted = true;
+ _user.DeletedAt = DateTime.UtcNow;
+
+ // 📢 Raise event
+ AddDomainEvent(new UserDeactivatedEvent(_user.Id, reason));
+ }
+}
+```
+
+## 🎯 **AGGREGATE BOUNDARIES**
+
+### **🔍 Câu hỏi quan trọng: "Entities nào thuộc cùng 1 Aggregate?"**
+
+#### **✅ GOOD: Tight Cohesion**
+```
+UserAggregate:
+├── User (root) ← Chính
+├── UserProfile ← Belongs to User
+├── UserSettings ← Belongs to User
+└── UserPreferences ← Belongs to User
+
+ConversationAggregate:
+├── Conversation (root) ← Chính
+├── Message ← Belongs to Conversation
+├── Attachment ← Belongs to Message
+└── MessageReaction ← Belongs to Message
+```
+
+#### **❌ BAD: Too Big**
+```
+GiantAggregate: (DON'T DO THIS!)
+├── User
+├── Conversation
+├── Message
+├── Order
+├── Payment
+├── Notification
+└── Everything... ← TOO MUCH!
+```
+
+### **🎯 Quy tắc vàng:**
+- **1 transaction = 1 aggregate**
+- **Thay đổi cùng lúc = cùng aggregate**
+- **Independent lifecycle = separate aggregates**
+
+## 🔄 **AGGREGATE LIFECYCLE**
+
+### **📍 1. Creation**
+```csharp
+// ❌ DON'T: Direct entity creation
+var user = new User { Email = "test@test.com" }; // No validation!
+
+// ✅ DO: Through aggregate
+var userAggregate = UserAggregate.Create("test@test.com", "John Doe", "password123");
+// ↑ Validates, normalizes, applies business rules, raises events
+```
+
+### **📍 2. Retrieval**
+```csharp
+// Repository returns entity, wrap in aggregate for business operations
+var user = await _userRepository.GetByIdAsync(userId);
+var userAggregate = new UserAggregate(user);
+```
+
+### **📍 3. Modification**
+```csharp
+// ❌ DON'T: Direct property changes
+user.Email = "new@email.com"; // No validation!
+
+// ✅ DO: Through aggregate methods
+userAggregate.UpdateContactInfo("new@email.com", "0123456789");
+// ↑ Validates, applies rules, raises events
+```
+
+### **📍 4. Persistence**
+```csharp
+// Save entity (not aggregate)
+await _userRepository.UpdateAsync(userAggregate.GetUser());
+// Events automatically dispatched by DomainEventBehavior
+```
+
+## 💡 **VÍ DỤ THỰC TẾ: CONVERSATION AGGREGATE**
+
+```csharp
+public sealed class ConversationAggregate : BaseAggregateRoot
+{
+ private readonly Conversation _conversation;
+ private readonly List _messages = [];
+
+ // ✅ Business rule: Only owner or participants can add messages
+ public void AddMessage(Guid senderId, string content, MessageType type = MessageType.Text)
+ {
+ // 🛡️ Validation
+ if (_conversation.Status != ConversationStatus.Active)
+ throw new InvalidOperationException("Cannot add messages to inactive conversations");
+
+ if (!CanUserSendMessage(senderId))
+ throw new UnauthorizedException("User not allowed to send messages");
+
+ if (string.IsNullOrWhiteSpace(content))
+ throw new ArgumentException("Message content required");
+
+ // 🏗️ Create message
+ var message = new Message
+ {
+ ConversationId = _conversation.Id,
+ SenderId = senderId,
+ Content = content.Trim(),
+ Type = type,
+ CreatedAt = DateTime.UtcNow
+ };
+
+ _messages.Add(message);
+
+ // 📊 Update conversation stats
+ _conversation.LastMessageAt = DateTime.UtcNow;
+ _conversation.MessageCount++;
+
+ // 📢 Raise events
+ AddDomainEvent(new MessageAddedEvent(_conversation.Id, message.Id, senderId));
+
+ // 🔔 Notify if mention
+ if (content.Contains("@"))
+ {
+ var mentions = ExtractMentions(content);
+ AddDomainEvent(new UsersMentionedEvent(_conversation.Id, message.Id, mentions));
+ }
+ }
+}
+```
+
+## 🎯 **TÓM TẮT: AGGREGATE**
+
+### **📊 Entity vs Aggregate**
+
+| **Aspect** | **Entity** | **Aggregate** |
+|------------|------------|---------------|
+| **Purpose** | 🏛️ Data container | 🏗️ Business orchestrator |
+| **Methods** | Simple getters/setters | Complex business operations |
+| **Validation** | Basic format checks | Full business rules |
+| **Events** | None | Domain events |
+| **Usage** | Repository, mapping | Business operations |
+
+### **🔑 Key Points:**
+
+1. **Aggregate Root** = Entry point, only way to access child entities
+2. **Consistency Boundary** = All changes in 1 transaction
+3. **Business Logic** = Complex operations, validation, rules
+4. **Domain Events** = Communication with other parts of system
+5. **Encapsulation** = Hide complexity, provide clean interface
+
+### **🎯 When to use Aggregates:**
+
+✅ **Use Aggregates for:**
+- Complex business operations
+- Multi-entity transactions
+- Business rule enforcement
+- State changes that need events
+
+❌ **Don't use Aggregates for:**
+- Simple CRUD operations
+- Read-only queries
+- Cross-aggregate operations
+
+---
+
+**🎯 Think of Aggregate as "Smart Controller" for a group of related entities!**
diff --git a/docs/BEHAVIORS_GUIDE.md b/docs/BEHAVIORS_GUIDE.md
new file mode 100644
index 0000000..4a62eda
--- /dev/null
+++ b/docs/BEHAVIORS_GUIDE.md
@@ -0,0 +1,237 @@
+# 🔄 MediatR Behaviors - Cross-Cutting Concerns
+
+**Behaviors là "middleware" cho MediatR pipeline - xử lý các concerns chung cho tất cả requests.**
+
+## 🎯 **TỔNG QUAN**
+
+### **Behaviors = Middleware cho Commands/Queries**
+```
+Request → ValidationBehavior → AuthorizationBehavior → LoggingBehavior → Handler → Response
+```
+
+**Mỗi behavior là 1 layer xử lý cross-cutting concerns trước/sau khi handler chạy.**
+
+## 📋 **DANH SÁCH BEHAVIORS**
+
+### **1. 🛡️ ValidationBehavior**
+- **Mục đích**: Validate input data trước khi xử lý
+- **Khi nào chạy**: Trước handler
+- **Input**: FluentValidation validators
+- **Output**: Throw ValidationException nếu invalid
+
+```csharp
+// Example: CreateUserCommand validation
+public class CreateUserCommandValidator : AbstractValidator
+{
+ public CreateUserCommandValidator()
+ {
+ RuleFor(x => x.Email).NotEmpty().EmailAddress();
+ RuleFor(x => x.FullName).NotEmpty().MinimumLength(2);
+ RuleFor(x => x.Password).NotEmpty().MinimumLength(8);
+ }
+}
+
+// ValidationBehavior tự động chạy validator này
+// Nếu fail → throw ValidationException
+```
+
+### **2. 🔐 AuthorizationBehavior**
+- **Mục đích**: Kiểm tra quyền truy cập
+- **Khi nào chạy**: Sau validation, trước handler
+- **Input**: IAuthorizedRequest interface
+- **Output**: Throw UnauthorizedException/ForbiddenException
+
+```csharp
+// Command yêu cầu authorization
+public record CreateUserCommand : ICommand>,
+ IAuthorizedRequest
+{
+ public AuthorizationRequirement AuthorizationRequirement => new()
+ {
+ Roles = ["Admin"], // Cần role Admin
+ Permissions = ["users.create"], // Cần permission users.create
+ RequireAuthentication = true
+ };
+}
+
+// AuthorizationBehavior sẽ check:
+// - User có authenticated không?
+// - User có role Admin không?
+// - User có permission users.create không?
+```
+
+### **3. 📊 LoggingBehavior**
+- **Mục đích**: Log tất cả requests/responses
+- **Khi nào chạy**: Bao quanh handler (before + after)
+- **Input**: Request name
+- **Output**: Logs với timing
+
+```csharp
+// Logs:
+// "Starting request CreateUserCommand"
+// "Completed request CreateUserCommand in 150ms"
+// "Request CreateUserCommand failed after 200ms" (nếu exception)
+```
+
+### **4. ⚡ PerformanceBehavior**
+- **Mục đích**: Monitor performance, cảnh báo slow requests
+- **Khi nào chạy**: Bao quanh handler
+- **Threshold**: 5 seconds (configurable)
+- **Output**: Warning log cho slow requests
+
+```csharp
+// Nếu request > 5 giây:
+// "Slow request detected: CreateUserCommand took 7500ms"
+```
+
+### **5. 💾 CachingBehavior**
+- **Mục đích**: Cache kết quả queries để tăng performance
+- **Khi nào chạy**: Chỉ cho queries implement ICacheableQuery
+- **Storage**: MemoryCache
+- **TTL**: Configurable per query
+
+```csharp
+// Query với caching
+public record GetUserByIdQuery(Guid UserId) : IQuery,
+ ICacheableQuery
+{
+ public string CacheKey => $"user-{UserId}";
+ public TimeSpan? CacheExpiration => TimeSpan.FromMinutes(10);
+}
+
+// Flow:
+// 1. Check cache → nếu có → return cached result
+// 2. Nếu không → execute handler → cache result → return
+```
+
+### **6. 🔄 TransactionBehavior**
+- **Mục đích**: Wrap commands trong database transaction
+- **Khi nào chạy**: Chỉ cho commands implement ITransactionalCommand
+- **Rollback**: Automatic nếu có exception
+
+```csharp
+// Command cần transaction
+public record CreateUserCommand : ICommand>,
+ ITransactionalCommand
+{
+ // Properties...
+}
+
+// TransactionBehavior sẽ:
+// 1. Begin transaction
+// 2. Execute handler
+// 3. Commit nếu success / Rollback nếu exception
+```
+
+### **7. 📢 DomainEventBehavior**
+- **Mục đích**: Dispatch domain events sau khi command thành công
+- **Khi nào chạy**: Sau handler (chỉ cho commands)
+- **Process**: Lấy events từ aggregates → dispatch qua MediatR
+
+```csharp
+// Flow:
+// 1. Handler tạo User → UserAggregate raises UserCreatedEvent
+// 2. DomainEventBehavior lấy events từ aggregates
+// 3. Dispatch events → UserCreatedEventHandler.Handle()
+```
+
+## 🔄 **PIPELINE EXECUTION ORDER**
+
+```
+📥 Request comes in
+ ↓
+🛡️ ValidationBehavior (validate input)
+ ↓
+🔐 AuthorizationBehavior (check permissions)
+ ↓
+📊 LoggingBehavior (start logging)
+ ↓
+⚡ PerformanceBehavior (start timing)
+ ↓
+💾 CachingBehavior (check cache)
+ ↓
+🔄 TransactionBehavior (begin transaction)
+ ↓
+🎯 HANDLER EXECUTION
+ ↓
+🔄 TransactionBehavior (commit/rollback)
+ ↓
+💾 CachingBehavior (save to cache)
+ ↓
+⚡ PerformanceBehavior (check if slow)
+ ↓
+📊 LoggingBehavior (log completion)
+ ↓
+📢 DomainEventBehavior (dispatch events)
+ ↓
+📤 Response returned
+```
+
+## 🎯 **CÁCH SỬ DỤNG**
+
+### **1. Validation**
+```csharp
+// Tạo validator
+public class CreateUserCommandValidator : AbstractValidator
+{
+ public CreateUserCommandValidator()
+ {
+ RuleFor(x => x.Email).NotEmpty().EmailAddress();
+ }
+}
+
+// ValidationBehavior tự động chạy
+```
+
+### **2. Authorization**
+```csharp
+// Implement IAuthorizedRequest
+public record DeleteUserCommand : ICommand, IAuthorizedRequest
+{
+ public AuthorizationRequirement AuthorizationRequirement => new()
+ {
+ Roles = ["Admin"],
+ RequireAuthentication = true
+ };
+}
+```
+
+### **3. Caching**
+```csharp
+// Implement ICacheableQuery
+public record GetUsersQuery : IQuery>, ICacheableQuery
+{
+ public string CacheKey => "all-users";
+ public TimeSpan? CacheExpiration => TimeSpan.FromMinutes(5);
+}
+```
+
+### **4. Transaction**
+```csharp
+// Implement ITransactionalCommand
+public record CreateUserCommand : ICommand, ITransactionalCommand
+{
+ // Command được wrap trong transaction tự động
+}
+```
+
+## ✅ **LỢI ÍCH**
+
+| **Không có Behaviors** | **Có Behaviors** |
+|------------------------|-------------------|
+| ❌ Code lặp lại validation | ✅ Validation tự động |
+| ❌ Authorization scattered | ✅ Centralized authorization |
+| ❌ Khó debug performance | ✅ Automatic performance monitoring |
+| ❌ Manual transaction management | ✅ Automatic transactions |
+| ❌ Inconsistent logging | ✅ Consistent logging |
+
+## 🚫 **LƯU Ý**
+
+- **Order matters**: Behaviors chạy theo thứ tự đăng ký
+- **Performance**: Behaviors thêm overhead, nhưng benefits > costs
+- **Testing**: Có thể bypass behaviors trong unit tests
+- **Conditional**: Behaviors chỉ chạy khi request implement interface tương ứng
+
+---
+
+**🎯 Behaviors = "Aspect-Oriented Programming" cho Clean Architecture!**
diff --git a/docs/CI_CD_EXPLAINED.md b/docs/CI_CD_EXPLAINED.md
new file mode 100644
index 0000000..74f66b1
--- /dev/null
+++ b/docs/CI_CD_EXPLAINED.md
@@ -0,0 +1,497 @@
+# 🚀 CI/CD Pipeline Explained
+
+## 📖 **Mục đích tài liệu**
+Giải thích chi tiết CI/CD pipeline của Legal Assistant project, giúp team hiểu rõ quy trình tự động hóa build, test và deploy.
+
+---
+
+## 🔄 **CI/CD là gì?**
+
+### **🔧 CI (Continuous Integration)**
+- **Tự động build** và **test** code mỗi khi có thay đổi
+- **Phát hiện lỗi sớm** trước khi merge vào main branch
+- **Đảm bảo code quality** thông qua automated checks
+
+### **🚀 CD (Continuous Deployment)**
+- **Tự động deploy** code sau khi pass tất cả tests
+- **Giảm thời gian** từ development đến production
+- **Đảm bảo consistency** giữa các environments
+
+---
+
+## 📋 **Pipeline Overview**
+
+```mermaid
+graph LR
+ A[Code Push] --> B[Trigger CI/CD]
+ B --> C[Restore Dependencies]
+ C --> D[Build Solution]
+ D --> E[Run Tests]
+ E --> F[Code Analysis]
+ F --> G[Publish Artifacts]
+ G --> H[Upload to GitHub]
+```
+
+---
+
+## 🎯 **Triggers - Khi nào pipeline chạy?**
+
+### **1. 📝 Push to main branch**
+```yaml
+push:
+ branches:
+ - main
+```
+- **Mục đích**: Đảm bảo main branch luôn stable
+- **Khi nào**: Mỗi khi merge PR hoặc push trực tiếp
+
+### **2. 🔀 Pull Requests**
+```yaml
+pull_request:
+ branches:
+ - main
+ types:
+ - opened # Khi tạo PR mới
+ - synchronize # Khi push thêm commits
+ - reopened # Khi reopen PR
+ - ready_for_review # Khi mark ready từ draft
+```
+- **Mục đích**: Kiểm tra code trước khi merge
+- **Lợi ích**: Ngăn broken code vào main branch
+
+### **3. 🔧 Manual Trigger**
+```yaml
+workflow_dispatch:
+```
+- **Mục đích**: Chạy pipeline thủ công khi cần
+- **Sử dụng**: Testing, debugging, emergency builds
+
+---
+
+## 🏗️ **Build Matrix Strategy**
+
+```yaml
+strategy:
+ matrix:
+ dotnet-version: ['8.x']
+ os: [ubuntu-latest]
+ configuration: [Release]
+```
+
+### **🎯 Lợi ích:**
+- **Flexibility**: Dễ dàng test multiple versions/OS
+- **Scalability**: Có thể mở rộng test trên Windows, macOS
+- **Future-proof**: Sẵn sàng cho .NET 9, 10...
+
+### **💡 Ví dụ mở rộng:**
+```yaml
+matrix:
+ dotnet-version: ['8.x', '9.x']
+ os: [ubuntu-latest, windows-latest, macos-latest]
+ configuration: [Debug, Release]
+```
+
+---
+
+## ⚙️ **Chi tiết các Steps**
+
+### **1. 📦 Checkout Code**
+```yaml
+- uses: actions/checkout@v4
+```
+- **Mục đích**: Download source code về runner
+- **Version**: v4 (latest, secure)
+
+### **2. 🛠️ Setup .NET**
+```yaml
+- name: Setup .NET
+ uses: actions/setup-dotnet@v4
+ with:
+ dotnet-version: ${{ env.DOTNET_VERSION }}
+```
+- **Mục đích**: Cài đặt .NET 8 SDK
+- **Environment**: `DOTNET_VERSION: "8.x"`
+
+### **3. 💾 Cache NuGet Packages**
+```yaml
+- name: Cache NuGet packages
+ uses: actions/cache@v4
+ with:
+ path: ~/.nuget/packages
+ key: ${{ runner.os }}-nuget-${{ hashFiles('**/*.csproj') }}
+ restore-keys: |
+ ${{ runner.os }}-nuget-
+```
+
+#### **🚀 Lợi ích của Caching:**
+- **Tăng tốc build**: Không cần download packages đã có
+- **Tiết kiệm bandwidth**: Giảm tải cho NuGet servers
+- **Cache invalidation**: Tự động refresh khi .csproj thay đổi
+
+#### **📊 Performance Impact:**
+- **Không cache**: ~2-3 phút restore
+- **Có cache**: ~10-30 giây restore
+
+### **4. 📥 Restore Dependencies**
+```yaml
+- name: Restore
+ run: dotnet restore LegalAssistant.AppService.sln
+```
+- **Mục đích**: Download NuGet packages
+- **Input**: Solution file (tất cả projects)
+
+### **5. 🔨 Build Solution**
+```yaml
+- name: Build
+ run: dotnet build LegalAssistant.AppService.sln --configuration ${{ matrix.configuration }} --no-restore
+```
+
+#### **🎯 Build Parameters:**
+- `--configuration Release`: Optimized build
+- `--no-restore`: Skip restore (đã làm ở step trước)
+
+#### **📦 Build Order (Dependencies):**
+```
+1. Domain.dll (no dependencies)
+2. Application.dll (depends on Domain)
+3. Infrastructure.dll (depends on Domain + Application)
+4. Web.Api.dll (depends on all above)
+```
+
+### **6. 🧪 Run Tests**
+```yaml
+- name: Test
+ run: dotnet test LegalAssistant.AppService.sln --configuration ${{ matrix.configuration }} --no-restore --no-build --verbosity normal --collect:"XPlat Code Coverage"
+```
+
+#### **🔍 Test Parameters:**
+- `--no-restore`: Skip restore
+- `--no-build`: Use existing build
+- `--verbosity normal`: Detailed output
+- `--collect:"XPlat Code Coverage"`: Generate coverage report
+
+#### **📊 Coverage Benefits:**
+- **Track code quality**: Phần trăm code được test
+- **Identify gaps**: Code nào chưa có test
+- **Trend analysis**: Coverage tăng/giảm theo thời gian
+
+### **7. 🔍 Code Analysis**
+```yaml
+- name: Code Analysis
+ run: dotnet build LegalAssistant.AppService.sln --configuration ${{ matrix.configuration }} --verbosity normal /p:TreatWarningsAsErrors=true
+```
+
+#### **🛡️ Quality Gates:**
+- `TreatWarningsAsErrors=true`: Warning = build failure
+- **Static analysis**: Detect potential issues
+- **Code style**: Enforce consistent formatting
+
+#### **📋 Checks thực hiện:**
+- **Security**: CA5387 (password iterations)
+- **Performance**: CA1851 (multiple enumerations)
+- **Style**: IDE0008 (explicit vs var)
+- **Maintainability**: Complexity warnings
+
+### **8. 📦 Publish Application**
+```yaml
+- name: Publish Web API
+ run: dotnet publish src/Web.Api/Web.Api.csproj --configuration ${{ matrix.configuration }} --no-restore --no-build --output ./publish
+```
+
+#### **🎯 Publish Benefits:**
+- **Self-contained**: Tất cả dependencies included
+- **Optimized**: Trimmed, compressed
+- **Deployment-ready**: Có thể run trực tiếp
+
+#### **📁 Output Structure:**
+```
+./publish/
+├── Web.Api.dll
+├── Web.Api.exe (Windows)
+├── appsettings.json
+├── wwwroot/
+└── runtimes/ (dependencies)
+```
+
+### **9. ☁️ Upload Artifacts**
+```yaml
+- name: Upload Build Artifacts
+ uses: actions/upload-artifact@v4
+ with:
+ name: published-app
+ path: ./publish
+```
+
+#### **💾 Artifact Benefits:**
+- **Deployment**: Download và deploy lên server
+- **Rollback**: Giữ lại previous versions
+- **Testing**: QA có thể test exact build
+- **Distribution**: Share build với team
+
+---
+
+## 🔄 **Workflow Execution Flow**
+
+### **📝 Scenario 1: Developer tạo Pull Request**
+
+```
+1. 👨💻 Developer: git push origin feature/new-auth
+2. 🌐 GitHub: Tạo PR từ feature/new-auth → main
+3. 🤖 CI/CD: Trigger pipeline automatically
+4. ⚙️ Runner: Checkout → Setup → Cache → Restore → Build → Test → Analyze
+5. ✅ Status: All checks passed ✅
+6. 👨💼 Reviewer: Approve và merge PR
+7. 🚀 Main branch: Trigger deployment pipeline
+```
+
+### **❌ Scenario 2: Build failure**
+
+```
+1. 👨💻 Developer: Push code với compilation error
+2. 🤖 CI/CD: Build step fails
+3. 🔴 Status: ❌ Build failed - compilation error
+4. 📧 Notification: Developer được notify qua email/Slack
+5. 🔧 Developer: Fix lỗi và push lại
+6. 🔄 CI/CD: Re-run pipeline
+7. ✅ Status: All checks passed ✅
+```
+
+### **🧪 Scenario 3: Test failure**
+
+```
+1. 👨💻 Developer: Push code break existing test
+2. ✅ Build: Success
+3. ❌ Test: 5/10 tests failed
+4. 🔴 Status: ❌ Tests failed
+5. 📊 Report: Detailed test results available
+6. 🔧 Developer: Analyze failures, fix code
+7. 🔄 Repeat: Until all tests pass
+```
+
+---
+
+## 📊 **Performance Metrics**
+
+### **⏱️ Typical Pipeline Duration:**
+
+| Step | First Run | Cached Run |
+|------|-----------|------------|
+| Checkout | 10s | 10s |
+| Setup .NET | 30s | 5s |
+| Cache/Restore | 2m | 20s |
+| Build | 1m | 45s |
+| Test | 30s | 30s |
+| Analysis | 20s | 15s |
+| Publish | 45s | 30s |
+| Upload | 15s | 10s |
+| **Total** | **~5m** | **~2m45s** |
+
+### **💾 Storage Usage:**
+- **Artifacts**: ~50MB per build
+- **Cache**: ~200MB NuGet packages
+- **Retention**: 90 days (configurable)
+
+---
+
+## 🛡️ **Security Considerations**
+
+### **🔐 Secrets Management:**
+```yaml
+# ❌ NEVER do this:
+- name: Deploy
+ run: echo "ConnectionString=Server=prod;Password=123456"
+
+# ✅ Correct way:
+- name: Deploy
+ env:
+ CONNECTION_STRING: ${{ secrets.PROD_CONNECTION_STRING }}
+ run: echo "Using secure connection"
+```
+
+### **🛡️ Security Features đã áp dụng:**
+- **No hardcoded secrets**: Sử dụng GitHub Secrets
+- **Least privilege**: Runner chỉ có quyền cần thiết
+- **Dependency scanning**: Automated security updates
+- **Code analysis**: Detect security vulnerabilities
+
+---
+
+## 🔧 **Customization Options**
+
+### **🌍 Multi-Environment Support:**
+
+```yaml
+strategy:
+ matrix:
+ environment: [dev, staging, prod]
+ include:
+ - environment: dev
+ configuration: Debug
+ deploy_target: dev-server
+ - environment: staging
+ configuration: Release
+ deploy_target: staging-server
+ - environment: prod
+ configuration: Release
+ deploy_target: prod-server
+```
+
+### **🐳 Docker Integration:**
+
+```yaml
+- name: Build Docker Image
+ run: |
+ docker build -t legal-assistant:${{ github.sha }} .
+ docker tag legal-assistant:${{ github.sha }} legal-assistant:latest
+
+- name: Push to Registry
+ run: |
+ docker push legal-assistant:${{ github.sha }}
+ docker push legal-assistant:latest
+```
+
+### **📱 Notification Setup:**
+
+```yaml
+- name: Notify Slack on Success
+ if: success()
+ uses: rtCamp/action-slack-notify@v2
+ env:
+ SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK }}
+ SLACK_MESSAGE: '✅ Build ${{ github.sha }} deployed successfully!'
+
+- name: Notify Slack on Failure
+ if: failure()
+ uses: rtCamp/action-slack-notify@v2
+ env:
+ SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK }}
+ SLACK_MESSAGE: '❌ Build ${{ github.sha }} failed!'
+```
+
+---
+
+## 📈 **Monitoring & Insights**
+
+### **📊 GitHub Actions Dashboard:**
+- **Build success rate**: Target 95%+
+- **Average build time**: Monitor trends
+- **Resource usage**: Optimize costs
+- **Cache hit rate**: Optimize performance
+
+### **🔍 Debugging Failed Builds:**
+
+1. **📋 Check logs**: Click vào failed step
+2. **🔄 Re-run job**: Temporary issues
+3. **🐛 Local reproduction**:
+ ```bash
+ dotnet restore
+ dotnet build --configuration Release
+ dotnet test --configuration Release
+ ```
+4. **💬 Team discussion**: Complex issues
+
+---
+
+## 🚀 **Best Practices**
+
+### **✅ DO:**
+- **Small, frequent commits**: Easier to debug failures
+- **Descriptive commit messages**: Help understand context
+- **Keep pipeline fast**: < 5 minutes target
+- **Monitor trends**: Build time, success rate
+- **Use caching**: Significant speed improvement
+- **Version pinning**: `@v4` instead of `@latest`
+
+### **❌ DON'T:**
+- **Skip tests**: Always run full test suite
+- **Hardcode secrets**: Use GitHub Secrets
+- **Ignore warnings**: Fix them early
+- **Run on every branch**: Only main + PRs
+- **Complex inline scripts**: Use separate files
+
+---
+
+## 🔮 **Future Enhancements**
+
+### **🎯 Roadmap:**
+
+1. **📊 Code Coverage Reports**
+ - Integration với SonarCloud
+ - Coverage trends tracking
+ - Quality gates enforcement
+
+2. **🐳 Container Registry**
+ - Docker image building
+ - Multi-arch support (ARM64, x86)
+ - Security scanning
+
+3. **🌍 Multi-Environment Deployment**
+ - Automated dev deployment
+ - Manual staging approval
+ - Blue-green production deployment
+
+4. **🔍 Advanced Testing**
+ - Integration tests với database
+ - End-to-end UI testing
+ - Performance benchmarking
+
+5. **📱 Enhanced Notifications**
+ - Slack/Teams integration
+ - Email reports
+ - Dashboard widgets
+
+---
+
+## 🆘 **Troubleshooting Guide**
+
+### **❌ Common Issues:**
+
+#### **1. Build Timeout**
+```
+Error: The job running on runner GitHub Actions X has exceeded the maximum execution time of 6 hours.
+```
+**Solution:**
+- Check for infinite loops
+- Optimize restore/build steps
+- Use more caching
+
+#### **2. Test Failures**
+```
+Error: Test run failed with 3 failed tests
+```
+**Steps:**
+1. Download test results artifact
+2. Analyze failed test details
+3. Run tests locally with same configuration
+4. Fix failing tests
+
+#### **3. Package Restore Failure**
+```
+Error: Unable to restore package 'Microsoft.AspNetCore.App'
+```
+**Solution:**
+- Check NuGet.org status
+- Clear cache and retry
+- Verify package versions in .csproj
+
+#### **4. Disk Space Issues**
+```
+Error: No space left on device
+```
+**Solution:**
+- Clean up old artifacts
+- Optimize Docker layers
+- Use smaller base images
+
+---
+
+## 📚 **Additional Resources**
+
+- **🔗 [GitHub Actions Documentation](https://docs.github.com/en/actions)**
+- **🔗 [.NET CLI Reference](https://docs.microsoft.com/en-us/dotnet/core/tools/)**
+- **🔗 [ASP.NET Core Deployment](https://docs.microsoft.com/en-us/aspnet/core/host-and-deploy/)**
+- **🔗 [Security Best Practices](https://docs.github.com/en/actions/security-guides)**
+
+---
+
+**🎯 Mục tiêu: Đảm bảo code quality cao và deployment tự động, an toàn cho Legal Assistant project!**
diff --git a/docs/COMMANDS.md b/docs/COMMANDS.md
new file mode 100644
index 0000000..3c7a8ed
--- /dev/null
+++ b/docs/COMMANDS.md
@@ -0,0 +1,447 @@
+# 🛠️ Common Commands - Legal Assistant Project
+
+Danh sách các commands thường dùng để develop, build, test và deploy Legal Assistant application.
+
+---
+
+## 🏗️ **Build Commands**
+
+### **Build toàn bộ solution**
+```bash
+# Build entire solution
+dotnet build LegalAssistant.AppService.sln
+
+# Build specific configuration
+dotnet build LegalAssistant.AppService.sln --configuration Release
+dotnet build LegalAssistant.AppService.sln --configuration Debug
+```
+
+### **Build từng project riêng**
+```bash
+# Build Domain layer
+dotnet build src/Domain/Domain.csproj
+
+# Build Application layer
+dotnet build src/Application/Application.csproj
+
+# Build Infrastructure layer
+dotnet build src/Infrastructure/Infrastructure.csproj
+
+# Build Web API
+dotnet build src/Web.Api/Web.Api.csproj
+```
+
+### **Clean và Rebuild**
+```bash
+# Clean solution
+dotnet clean LegalAssistant.AppService.sln
+
+# Clean và build lại
+dotnet clean && dotnet build LegalAssistant.AppService.sln
+```
+
+---
+
+## 🚀 **Run Commands**
+
+### **Chạy Web API**
+```bash
+# Run Web API (Production mode)
+dotnet run --project src/Web.Api/Web.Api.csproj
+
+# Run với environment cụ thể
+dotnet run --project src/Web.Api/Web.Api.csproj --environment Development
+dotnet run --project src/Web.Api/Web.Api.csproj --environment Staging
+dotnet run --project src/Web.Api/Web.Api.csproj --environment Production
+```
+
+### **Watch mode (auto-reload khi code thay đổi)**
+```bash
+# Auto-reload khi save file
+dotnet watch run --project src/Web.Api/Web.Api.csproj
+
+# Watch với hot reload (C# 6+)
+dotnet watch --project src/Web.Api/Web.Api.csproj
+```
+
+### **Chạy với URLs cụ thể**
+```bash
+# Chạy trên port khác
+dotnet run --project src/Web.Api/Web.Api.csproj --urls "https://localhost:5001;http://localhost:5000"
+
+# Chạy trên tất cả interfaces
+dotnet run --project src/Web.Api/Web.Api.csproj --urls "https://*:5001;http://*:5000"
+```
+
+---
+
+## 🧪 **Test Commands**
+
+### **Chạy tests**
+```bash
+# Run tất cả tests
+dotnet test LegalAssistant.AppService.sln
+
+# Run tests với detailed output
+dotnet test LegalAssistant.AppService.sln --verbosity normal
+
+# Run tests với code coverage
+dotnet test LegalAssistant.AppService.sln --collect:"XPlat Code Coverage"
+```
+
+### **Test từng project**
+```bash
+# Khi có test projects (future)
+# dotnet test tests/Application.Tests/Application.Tests.csproj
+# dotnet test tests/Infrastructure.Tests/Infrastructure.Tests.csproj
+# dotnet test tests/Web.Api.Tests/Web.Api.Tests.csproj
+```
+
+### **Test với filters**
+```bash
+# Run tests theo category
+dotnet test --filter Category=Unit
+dotnet test --filter Category=Integration
+
+# Run tests theo class name
+dotnet test --filter ClassName=UserServiceTests
+
+# Run tests theo method name
+dotnet test --filter MethodName=CreateUser_ShouldReturnSuccess
+```
+
+---
+
+## 📦 **Package Management**
+
+### **Restore packages**
+```bash
+# Restore tất cả packages
+dotnet restore LegalAssistant.AppService.sln
+
+# Restore cho project cụ thể
+dotnet restore src/Web.Api/Web.Api.csproj
+```
+
+### **Add/Remove packages**
+```bash
+# Add package vào project cụ thể
+dotnet add src/Web.Api/Web.Api.csproj package Microsoft.EntityFrameworkCore
+dotnet add src/Infrastructure/Infrastructure.csproj package BCrypt.Net-Next
+
+# Remove package
+dotnet remove src/Web.Api/Web.Api.csproj package OldPackageName
+
+# Update package
+dotnet add src/Web.Api/Web.Api.csproj package Microsoft.EntityFrameworkCore --version 8.0.0
+```
+
+### **List packages**
+```bash
+# List packages trong project
+dotnet list src/Web.Api/Web.Api.csproj package
+
+# List outdated packages
+dotnet list package --outdated
+
+# List vulnerable packages
+dotnet list package --vulnerable
+```
+
+---
+
+## 🎨 **Code Formatting**
+
+### **Check format theo .editorconfig**
+```bash
+# Check format cho toàn bộ solution (sẽ fail nếu có file không đúng format)
+dotnet format --verify-no-changes LegalAssistant.AppService.sln
+
+# Check format cho project cụ thể
+dotnet format --verify-no-changes src/Web.Api/Web.Api.csproj
+```
+
+### **Fix formatting issues**
+```bash
+# Auto-fix format issues
+dotnet format LegalAssistant.AppService.sln
+
+# Fix format cho project cụ thể
+dotnet format src/Web.Api/Web.Api.csproj
+
+# Fix với options
+dotnet format LegalAssistant.AppService.sln --include-generated
+```
+
+### **Style analysis**
+```bash
+# Run code analysis
+dotnet build LegalAssistant.AppService.sln /p:TreatWarningsAsErrors=true
+
+# Run với specific analyzers
+dotnet build --verbosity normal /p:RunAnalyzersDuringBuild=true
+```
+
+---
+
+## 🗄️ **Database Commands**
+
+### **Entity Framework migrations**
+```bash
+# Add migration (khi có DbContext)
+dotnet ef migrations add InitialCreate --project src/Infrastructure --startup-project src/Web.Api
+
+# Update database
+dotnet ef database update --project src/Infrastructure --startup-project src/Web.Api
+
+# Drop database
+dotnet ef database drop --project src/Infrastructure --startup-project src/Web.Api
+
+# Generate SQL script
+dotnet ef migrations script --project src/Infrastructure --startup-project src/Web.Api
+```
+
+### **Database tools**
+```bash
+# Install EF tools globally (one time)
+dotnet tool install --global dotnet-ef
+
+# Update EF tools
+dotnet tool update --global dotnet-ef
+
+# Check EF tools version
+dotnet ef --version
+```
+
+---
+
+## 📦 **Publish & Deploy**
+
+### **Publish application**
+```bash
+# Publish Web API
+dotnet publish src/Web.Api/Web.Api.csproj --configuration Release --output ./publish
+
+# Publish self-contained
+dotnet publish src/Web.Api/Web.Api.csproj -c Release -r win-x64 --self-contained true
+
+# Publish for Linux
+dotnet publish src/Web.Api/Web.Api.csproj -c Release -r linux-x64 --self-contained true
+```
+
+### **Docker commands**
+```bash
+# Build Docker image (nếu có Dockerfile)
+docker build -t legal-assistant:latest .
+
+# Run Docker container
+docker run -p 5000:8080 legal-assistant:latest
+
+# Run với environment variables
+docker run -p 5000:8080 -e ASPNETCORE_ENVIRONMENT=Production legal-assistant:latest
+```
+
+---
+
+## 🔍 **Development Tools**
+
+### **Code generation**
+```bash
+# Create new controller
+dotnet new controller -n ConversationsController -o src/Web.Api/Controllers/V1
+
+# Create new class
+dotnet new class -n UserService -o src/Application/Services
+```
+
+### **Security scanning**
+```bash
+# Scan for vulnerable packages
+dotnet list package --vulnerable --include-transitive
+
+# Audit packages
+dotnet restore --use-lock-file
+```
+
+### **Performance profiling**
+```bash
+# Run with diagnostic tools
+dotnet run --project src/Web.Api/Web.Api.csproj --configuration Release
+
+# Memory usage analysis
+dotnet-counters monitor --process-id [PID]
+```
+
+---
+
+## 🔧 **Debugging Commands**
+
+### **Logging & Diagnostics**
+```bash
+# Run với specific log level
+dotnet run --project src/Web.Api/Web.Api.csproj -- --Logging:LogLevel:Default=Debug
+
+# Enable detailed errors
+dotnet run --project src/Web.Api/Web.Api.csproj --environment Development
+```
+
+### **Development certificates**
+```bash
+# Trust development certificate
+dotnet dev-certs https --trust
+
+# Clean và regenerate certificates
+dotnet dev-certs https --clean
+dotnet dev-certs https --trust
+```
+
+---
+
+## 🌍 **Environment Management**
+
+### **Environment variables**
+```bash
+# Windows
+set ASPNETCORE_ENVIRONMENT=Development
+dotnet run --project src/Web.Api/Web.Api.csproj
+
+# Linux/Mac
+export ASPNETCORE_ENVIRONMENT=Development
+dotnet run --project src/Web.Api/Web.Api.csproj
+
+# PowerShell
+$env:ASPNETCORE_ENVIRONMENT="Development"
+dotnet run --project src/Web.Api/Web.Api.csproj
+```
+
+### **Configuration management**
+```bash
+# Override appsettings
+dotnet run --project src/Web.Api/Web.Api.csproj -- --ConnectionStrings:DefaultConnection="YourConnectionString"
+
+# Use specific settings file
+dotnet run --project src/Web.Api/Web.Api.csproj --environment Staging
+# Will use appsettings.Staging.json
+```
+
+---
+
+## 📊 **Monitoring & Health Checks**
+
+### **Health checks**
+```bash
+# Kiểm tra health endpoint
+curl https://localhost:5001/health
+
+# Với detailed information
+curl https://localhost:5001/health?detailed=true
+```
+
+### **Application URLs**
+```bash
+# Swagger API documentation
+# https://localhost:5001/swagger
+
+# Health checks
+# https://localhost:5001/health
+
+# API endpoints
+# https://localhost:5001/api/v1/auth/login
+# https://localhost:5001/api/v1/users
+```
+
+---
+
+## 🚀 **Quick Start Commands**
+
+### **Development setup (first time)**
+```bash
+# 1. Clone repository
+git clone [repository-url]
+cd LegalAssistant.AppService
+
+# 2. Restore packages
+dotnet restore
+
+# 3. Trust development certificates
+dotnet dev-certs https --trust
+
+# 4. Run application
+dotnet run --project src/Web.Api/Web.Api.csproj
+
+# 5. Open browser
+# Navigate to: https://localhost:5001/swagger
+```
+
+### **Daily development workflow**
+```bash
+# 1. Pull latest changes
+git pull origin main
+
+# 2. Restore any new packages
+dotnet restore
+
+# 3. Build to check for errors
+dotnet build
+
+# 4. Run with auto-reload
+dotnet watch run --project src/Web.Api/Web.Api.csproj
+
+# 5. Run tests before committing
+dotnet test
+
+# 6. Format code
+dotnet format
+
+# 7. Commit changes
+git add .
+git commit -m "Your commit message"
+git push
+```
+
+---
+
+## 🆘 **Troubleshooting Commands**
+
+### **Common issues**
+```bash
+# Port already in use
+netstat -ano | findstr :5001
+# Kill process: taskkill /PID [process_id] /F
+
+# Clear NuGet cache
+dotnet nuget locals all --clear
+
+# Reset tool path
+dotnet tool uninstall -g dotnet-ef
+dotnet tool install -g dotnet-ef
+
+# Check .NET versions
+dotnet --list-sdks
+dotnet --list-runtimes
+
+# Verbose build output
+dotnet build --verbosity diagnostic
+```
+
+### **Clean everything**
+```bash
+# Nuclear option: clean everything và rebuild
+dotnet clean
+rm -rf bin obj # hoặc rmdir /s bin obj trên Windows
+dotnet restore
+dotnet build
+```
+
+---
+
+## 📚 **Useful References**
+
+- **🔗 [.NET CLI Reference](https://docs.microsoft.com/en-us/dotnet/core/tools/)**
+- **🔗 [Entity Framework Core CLI](https://docs.microsoft.com/en-us/ef/core/cli/dotnet)**
+- **🔗 [ASP.NET Core Environment](https://docs.microsoft.com/en-us/aspnet/core/fundamentals/environments)**
+- **🔗 [Docker with .NET](https://docs.microsoft.com/en-us/dotnet/architecture/microservices/docker-application-development-process/)**
+
+---
+
+**💡 Tip: Bookmark trang này và sử dụng Ctrl+F để tìm command cần thiết nhanh chóng!**
diff --git a/docs/DI_BEHAVIORS_EXAMPLE.md b/docs/DI_BEHAVIORS_EXAMPLE.md
new file mode 100644
index 0000000..34d320c
--- /dev/null
+++ b/docs/DI_BEHAVIORS_EXAMPLE.md
@@ -0,0 +1,177 @@
+# 🔗 Dependency Injection cho MediatR Behaviors
+
+## 🎯 **REGISTRATION PROCESS**
+
+### **Step 1: Register Generic Behaviors**
+```csharp
+// Trong ApplicationServiceExtensions.cs
+services.AddScoped(typeof(IPipelineBehavior<,>), typeof(ValidationBehavior<,>));
+```
+
+**Open Generic Type Registration:**
+- `IPipelineBehavior` = Interface
+- `ValidationBehavior` = Implementation
+
+### **Step 2: MediatR Request đến**
+```csharp
+// User gửi request:
+var command = new CreateUserCommand
+{
+ Email = "user@example.com",
+ FullName = "John Doe",
+ Password = "password123"
+};
+
+await _mediator.Send(command); // ← MediatR xử lý request này
+```
+
+### **Step 3: MediatR tự động resolve Generic Types**
+```csharp
+// MediatR phân tích:
+// TRequest = CreateUserCommand
+// TResponse = Result
+
+// Resolve thành:
+IPipelineBehavior>
+ ↓
+ValidationBehavior>
+```
+
+### **Step 4: Constructor Injection**
+```csharp
+// MediatR tạo instance:
+var validators = serviceProvider.GetServices>();
+var validationBehavior = new ValidationBehavior>(validators);
+```
+
+### **Step 5: Pipeline Execution**
+```csharp
+// Chạy pipeline:
+public async Task> Handle(
+ CreateUserCommand request,
+ RequestHandlerDelegate> next,
+ CancellationToken cancellationToken)
+{
+ // Validation logic...
+
+ var context = new ValidationContext(request);
+ var validationResults = await Task.WhenAll(
+ validators.Select(v => v.ValidateAsync(context, cancellationToken)));
+
+ // Nếu có lỗi → throw ValidationException
+ // Nếu OK → await next() (chuyển sang behavior tiếp theo)
+}
+```
+
+## 🔄 **FLOW CHI TIẾT**
+
+### **1. User gửi CreateUserCommand**
+```
+POST /api/users
+{
+ "email": "user@example.com",
+ "fullName": "John Doe",
+ "password": "password123"
+}
+```
+
+### **2. MediatR nhận request**
+```csharp
+await _mediator.Send(createUserCommand);
+```
+
+### **3. MediatR build Pipeline từ registered Behaviors**
+```csharp
+// Từ registration order:
+1. LoggingBehavior>
+2. PerformanceBehavior>
+3. AuthorizationBehavior>
+4. ValidationBehavior> ← ĐÚNG CHỖ NÀY!
+5. CachingBehavior>
+6. TransactionBehavior>
+7. DomainEventBehavior>
+8. CreateUserCommandHandler.Handle()
+```
+
+### **4. ValidationBehavior được execute**
+```csharp
+public async Task> Handle(
+ CreateUserCommand request, // ← Đây là concrete type
+ RequestHandlerDelegate> next,
+ CancellationToken cancellationToken)
+{
+ // DI container đã inject validators:
+ // IEnumerable> validators
+
+ if (!validators.Any())
+ {
+ return await next(); // Không có validator → skip
+ }
+
+ // Chạy tất cả validators cho CreateUserCommand
+ var context = new ValidationContext(request);
+ var validationResults = await Task.WhenAll(
+ validators.Select(v => v.ValidateAsync(context, cancellationToken)));
+
+ var failures = validationResults
+ .Where(r => r.Errors.Count != 0)
+ .SelectMany(r => r.Errors)
+ .ToList();
+
+ if (failures.Count != 0)
+ {
+ throw new ValidationException(failures); // ← Lỗi validation
+ }
+
+ return await next(); // ← OK → chuyển sang behavior tiếp theo
+}
+```
+
+## 🎯 **TẠI SAO DÙNG OPEN GENERIC TYPES?**
+
+### **❌ Nếu đăng ký từng type cụ thể:**
+```csharp
+// Phải đăng ký cho từng command riêng:
+services.AddScoped>,
+ ValidationBehavior>>();
+
+services.AddScoped>,
+ ValidationBehavior>>();
+
+services.AddScoped,
+ ValidationBehavior>();
+
+// ... và hàng trăm commands khác
+```
+
+### **✅ Với Open Generic Types:**
+```csharp
+// 1 dòng duy nhất cho TẤT CẢ commands:
+services.AddScoped(typeof(IPipelineBehavior<,>), typeof(ValidationBehavior<,>));
+
+// MediatR tự động resolve cho:
+// - CreateUserCommand, UpdateUserCommand, DeleteUserCommand
+// - GetUserQuery, GetUsersQuery, SearchUsersQuery
+// - Và tất cả requests khác!
+```
+
+## 💡 **MAGIC HAPPENS Ở ĐÂY:**
+
+1. **Registration time**: Đăng ký generic types
+2. **Runtime**: MediatR substitute concrete types
+3. **Execution**: ValidationBehavior nhận đúng validators cho request type
+
+```csharp
+// Registration:
+IPipelineBehavior<,> → ValidationBehavior<,>
+
+// Runtime cho CreateUserCommand:
+IPipelineBehavior>
+ → ValidationBehavior>
+
+// Runtime cho GetUserQuery:
+IPipelineBehavior
+ → ValidationBehavior
+```
+
+**🎯 Đây chính là sức mạnh của Generic Dependency Injection!**
diff --git a/docs/NAMING_CONVENTIONS.md b/docs/NAMING_CONVENTIONS.md
new file mode 100644
index 0000000..ff0ab33
--- /dev/null
+++ b/docs/NAMING_CONVENTIONS.md
@@ -0,0 +1,247 @@
+# 📝 Domain Layer Naming Conventions
+
+## 🎯 **TẠI SAO CẦN NAMING CONVENTIONS?**
+
+**Trước khi fix:**
+```
+❌ Domain/Entities/User.cs ← Entity
+❌ Domain/Aggregates/User/User.cs ← Aggregate (CONFUSING!)
+```
+
+**Sau khi fix:**
+```
+✅ Domain/Entities/User.cs ← Entity
+✅ Domain/Aggregates/User/UserAggregate.cs ← Aggregate (CLEAR!)
+```
+
+## 📋 **NAMING RULES**
+
+### **1. 🏛️ Entities - Pure Data Objects**
+```
+📁 Domain/Entities/
+├── User.cs ← class User : BaseEntity
+├── Conversation.cs ← class Conversation : BaseEntity
+├── Message.cs ← class Message : BaseEntity
+├── Attachment.cs ← class Attachment : BaseEntity
+└── LegalDocument.cs ← class LegalDocument : BaseEntity
+```
+
+**Convention:**
+- **Class name**: Singular noun (User, Conversation, Message)
+- **Namespace**: `Domain.Entities`
+- **Purpose**: Data + basic behavior
+- **Inheritance**: `BaseEntity`
+
+### **2. 🏗️ Aggregates - Business Logic Orchestrators**
+```
+📁 Domain/Aggregates/
+├── User/
+│ └── UserAggregate.cs ← class UserAggregate : BaseAggregateRoot
+├── Conversation/
+│ └── ConversationAggregate.cs ← class ConversationAggregate : BaseAggregateRoot
+└── LegalCase/
+ └── LegalCaseAggregate.cs ← class LegalCaseAggregate : BaseAggregateRoot
+```
+
+**Convention:**
+- **Class name**: `[Entity]Aggregate` (UserAggregate, ConversationAggregate)
+- **File name**: `[Entity]Aggregate.cs`
+- **Folder**: `[Entity]/` (singular)
+- **Namespace**: `Domain.Aggregates.[Entity]`
+- **Purpose**: Complex business operations
+- **Inheritance**: `BaseAggregateRoot`
+
+### **3. 📢 Events - Past Tense Actions**
+```
+📁 Domain/Events/
+├── User/
+│ ├── UserCreatedEvent.cs ← class UserCreatedEvent : IDomainEvent
+│ ├── UserUpdatedEvent.cs ← class UserUpdatedEvent : IDomainEvent
+│ └── UserDeactivatedEvent.cs ← class UserDeactivatedEvent : IDomainEvent
+└── Conversation/
+ ├── ConversationCreatedEvent.cs
+ ├── MessageAddedEvent.cs
+ └── ConversationClosedEvent.cs
+```
+
+**Convention:**
+- **Class name**: `[Entity][Action]Event` (UserCreatedEvent)
+- **Tense**: Past tense (Created, Updated, Deleted)
+- **Namespace**: `Domain.Events.[Entity]`
+
+### **4. 💎 Value Objects - Immutable Values**
+```
+📁 Domain/ValueObjects/
+├── Email.cs ← class Email : ValueObject
+├── Money.cs ← class Money : ValueObject
+├── Address.cs ← class Address : ValueObject
+└── PhoneNumber.cs ← class PhoneNumber : ValueObject
+```
+
+**Convention:**
+- **Class name**: Singular noun (Email, Money)
+- **Inheritance**: `ValueObject`
+- **Purpose**: Immutable value with validation
+
+### **5. 🔍 Specifications - Query Logic**
+```
+📁 Domain/Specifications/
+├── UserSpecifications.cs ← static class UserSpecifications
+├── ConversationSpecifications.cs
+└── MessageSpecifications.cs
+```
+
+**Convention:**
+- **Class name**: `[Entity]Specifications` (plural)
+- **Type**: Static class with static methods
+- **Methods**: `ActiveUsers()`, `RecentConversations()`
+
+### **6. 🛠️ Domain Services - Cross-Entity Logic**
+```
+📁 Domain/Services/
+├── UserDomainService.cs ← class UserDomainService
+├── ConversationDomainService.cs
+└── LegalAdviceDomainService.cs
+```
+
+**Convention:**
+- **Class name**: `[Domain]DomainService`
+- **Interface**: `I[Domain]DomainService`
+- **Purpose**: Logic that doesn't belong to single entity
+
+## 🎯 **COMPARISON: ENTITY vs AGGREGATE**
+
+### **📊 Side-by-Side Example**
+
+#### **🏛️ User Entity (Domain/Entities/User.cs)**
+```csharp
+namespace Domain.Entities;
+
+public sealed class User : BaseEntity
+{
+ public required string Email { get; set; }
+ public required string FullName { get; set; }
+ public UserRole[] Roles { get; set; } = [];
+
+ // Simple methods - basic behavior
+ public bool HasRole(UserRole role) => Roles.Contains(role);
+ public bool IsActive => !IsDeleted;
+}
+```
+
+#### **🏗️ User Aggregate (Domain/Aggregates/User/UserAggregate.cs)**
+```csharp
+namespace Domain.Aggregates.User;
+
+public sealed class UserAggregate : BaseAggregateRoot
+{
+ private readonly Entities.User _user; // ← Wraps entity
+
+ // Complex business operations
+ public static UserAggregate Create(string email, string fullName, ...)
+ {
+ // ✅ Business validation
+ // ✅ Domain events
+ // ✅ Complex logic
+ }
+
+ public void Deactivate()
+ {
+ // ✅ Business rules
+ // ✅ Domain events
+ // ✅ Cross-entity coordination
+ }
+}
+```
+
+## 🔄 **USAGE PATTERNS**
+
+### **In Application Layer:**
+```csharp
+// ❌ DON'T use entities directly
+public class CreateUserCommandHandler
+{
+ public async Task Handle(CreateUserCommand command)
+ {
+ var user = new User { Email = command.Email }; // ❌ Direct entity creation
+ await _repository.SaveAsync(user);
+ }
+}
+
+// ✅ DO use aggregates
+public class CreateUserCommandHandler
+{
+ public async Task Handle(CreateUserCommand command)
+ {
+ var userAggregate = UserAggregate.Create(command.Email, command.FullName); // ✅ Aggregate
+ await _repository.SaveAsync(userAggregate.GetUser());
+ }
+}
+```
+
+### **In Infrastructure Layer:**
+```csharp
+// Repository works with entities
+public class UserRepository : IUserRepository
+{
+ public async Task GetByIdAsync(Guid id) // ← Entity
+ {
+ return await _context.Users.FindAsync(id);
+ }
+
+ public async Task SaveAsync(User user) // ← Entity
+ {
+ _context.Users.Add(user);
+ await _context.SaveChangesAsync();
+ }
+}
+```
+
+## 📁 **FOLDER STRUCTURE SUMMARY**
+
+```
+Domain/
+├── Entities/ # 🏛️ Pure data objects
+│ ├── User.cs
+│ ├── Conversation.cs
+│ └── Message.cs
+│
+├── Aggregates/ # 🏗️ Business logic orchestrators
+│ ├── User/
+│ │ └── UserAggregate.cs
+│ └── Conversation/
+│ └── ConversationAggregate.cs
+│
+├── Events/ # 📢 Domain events (past tense)
+│ ├── User/
+│ │ ├── UserCreatedEvent.cs
+│ │ └── UserUpdatedEvent.cs
+│ └── Conversation/
+│ └── ConversationCreatedEvent.cs
+│
+├── ValueObjects/ # 💎 Immutable values
+│ ├── Email.cs
+│ └── Money.cs
+│
+├── Specifications/ # 🔍 Query logic
+│ ├── UserSpecifications.cs
+│ └── ConversationSpecifications.cs
+│
+└── Services/ # 🛠️ Domain services
+ ├── UserDomainService.cs
+ └── ConversationDomainService.cs
+```
+
+## ✅ **BENEFITS OF CLEAR NAMING**
+
+| **Aspect** | **Before** | **After** |
+|------------|------------|-----------|
+| **Clarity** | ❌ User vs User? | ✅ User vs UserAggregate |
+| **IDE Navigation** | ❌ Confusing autocomplete | ✅ Clear suggestions |
+| **Code Reviews** | ❌ Which User class? | ✅ Obviously different |
+| **Refactoring** | ❌ Risk of wrong changes | ✅ Safe refactoring |
+| **Onboarding** | ❌ Developers confused | ✅ Self-documenting |
+
+---
+
+**🎯 Remember: Good naming is documentation that never lies!**
diff --git a/docs/README.md b/docs/README.md
index 1f8ab2e..4db4754 100644
--- a/docs/README.md
+++ b/docs/README.md
@@ -22,20 +22,54 @@
- Multiple Event Handlers
- Side effects và decoupling
+### 🔄 **3. BEHAVIORS_GUIDE.md**
+- **🎯 Mục đích**: Hiểu MediatR Pipeline Behaviors (Cross-cutting concerns)
+- **👥 Đối tượng**: Developer cần hiểu Validation, Authorization, Caching, etc.
+- **📋 Nội dung**:
+ - 7 loại Behaviors: Validation, Authorization, Logging, Performance, Caching, Transaction, DomainEvent
+ - Pipeline execution order
+ - Cách implement và sử dụng từng loại
+
+### 🚀 **4. CI_CD_EXPLAINED.md**
+- **🎯 Mục đích**: Hiểu CI/CD pipeline và automation workflow
+- **👥 Đối tượng**: DevOps, Developer, Tech Lead
+- **📋 Nội dung**:
+ - CI/CD concepts và benefits
+ - Pipeline triggers và execution flow
+ - Build matrix strategy và caching
+ - Security considerations và best practices
+ - Troubleshooting guide và performance metrics
+
+### 🛠️ **5. COMMANDS.md**
+- **🎯 Mục đích**: Danh sách commands thường dùng cho development
+- **👥 Đối tượng**: Tất cả developers
+- **📋 Nội dung**:
+ - Build, run, test commands
+ - Package management và formatting
+ - Database migrations và publishing
+ - Troubleshooting và quick start guide
+
## 🚀 **Cách sử dụng tài liệu**
### **🆕 Người mới:**
1. 📖 Đọc **ARCHITECTURE_OVERVIEW.md** trước
-2. 🔍 Trace 1 luồng CreateUser từ đầu đến cuối
-3. 📚 Đọc **EVENT_HANDLER_FLOW.md** để hiểu Events
+2. 🛠️ Đọc **COMMANDS.md** để setup development environment
+3. 🔍 Trace 1 luồng CreateUser từ đầu đến cuối
+4. 📚 Đọc **EVENT_HANDLER_FLOW.md** để hiểu Events
+5. 🔄 Đọc **BEHAVIORS_GUIDE.md** để hiểu Cross-cutting concerns
+6. 🚀 Đọc **CI_CD_EXPLAINED.md** để hiểu automation workflow
### **🔧 Developer có kinh nghiệm:**
1. 🏗️ Review **ARCHITECTURE_OVERVIEW.md** để hiểu patterns
-2. 🔄 Đọc **EVENT_HANDLER_FLOW.md** để implement Events
+2. 🛠️ Bookmark **COMMANDS.md** để reference nhanh
+3. 🔄 Đọc **EVENT_HANDLER_FLOW.md** để implement Events
+4. 🔄 Đọc **BEHAVIORS_GUIDE.md** để implement Pipeline Behaviors
+5. 🚀 Đọc **CI_CD_EXPLAINED.md** để setup automation
### **👨💼 Tech Lead/Architect:**
-1. 📊 Review cả 2 docs để hiểu design decisions
+1. 📊 Review cả 5 docs để hiểu design decisions
2. 🎯 Sử dụng làm onboarding material cho team
+3. 🛠️ Share **COMMANDS.md** với team cho consistency
## 🧭 **Navigation Map**
@@ -43,13 +77,17 @@
📁 docs/
├── 📖 README.md ← Bạn đang ở đây
├── 🏗️ ARCHITECTURE_OVERVIEW.md ← Bắt đầu từ đây
-└── 🔄 EVENT_HANDLER_FLOW.md ← Sau khi hiểu kiến trúc
+├── 🛠️ COMMANDS.md ← Commands reference
+├── 🔄 EVENT_HANDLER_FLOW.md ← Hiểu Domain Events
+├── 🔄 BEHAVIORS_GUIDE.md ← Hiểu Pipeline Behaviors
+└── 🚀 CI_CD_EXPLAINED.md ← Hiểu Automation
```
## 📞 **Hỗ trợ**
- **❓ Có thắc mắc về architecture**: Xem ARCHITECTURE_OVERVIEW.md
-- **🔄 Không hiểu Events**: Xem EVENT_HANDLER_FLOW.md
+- **🔄 Không hiểu Events**: Xem EVENT_HANDLER_FLOW.md
+- **🔄 Không hiểu Behaviors**: Xem BEHAVIORS_GUIDE.md
- **🆘 Vẫn chưa rõ**: Hỏi team hoặc tạo issue
---
diff --git a/src/Application/Common/Behaviors/AuthorizationBehavior.cs b/src/Application/Common/Behaviors/AuthorizationBehavior.cs
index 90ce6f0..bf5a539 100644
--- a/src/Application/Common/Behaviors/AuthorizationBehavior.cs
+++ b/src/Application/Common/Behaviors/AuthorizationBehavior.cs
@@ -12,7 +12,7 @@ public sealed record AuthorizationRequirement
///
/// Required roles (user must have at least one)
///
- public string[] Roles { get; init; } = [];
+ public List Roles { get; init; } = [];
///
/// Required permissions (user must have all)
@@ -54,12 +54,12 @@ public interface ICurrentUser
///
/// User roles
///
- string[] Roles { get; }
+ List Roles { get; }
///
/// User permissions
///
- string[] Permissions { get; }
+ List Permissions { get; }
}
///
@@ -90,7 +90,7 @@ public async Task Handle(TRequest request, RequestHandlerDelegate 0)
+ if (requirement.Roles.Count > 0)
{
var hasRequiredRole = requirement.Roles.Any(role =>
currentUser.Roles.Contains(role, StringComparer.OrdinalIgnoreCase));
@@ -106,12 +106,12 @@ public async Task Handle(TRequest request, RequestHandlerDelegate 0)
{
- var hasAllPermissions = requirement.Permissions.All(permission =>
+ var hasAllPermissions = requirement.Permissions.ToList().TrueForAll(permission =>
currentUser.Permissions.Contains(permission, StringComparer.OrdinalIgnoreCase));
if (!hasAllPermissions)
{
- var missingPermissions = requirement.Permissions.Except(currentUser.Permissions).ToArray();
+ var missingPermissions = requirement.Permissions.Except(currentUser.Permissions).ToList();
logger.LogWarning("Access denied for user {UserId} to {RequestName}. Missing permissions: {MissingPermissions}",
currentUser.UserId, typeof(TRequest).Name, missingPermissions);
throw new ForbiddenException($"Missing permissions: {string.Join(", ", missingPermissions)}");
diff --git a/src/Application/Common/Behaviors/LoggingBehavior.cs b/src/Application/Common/Behaviors/LoggingBehavior.cs
index 8f8f3fa..a564004 100644
--- a/src/Application/Common/Behaviors/LoggingBehavior.cs
+++ b/src/Application/Common/Behaviors/LoggingBehavior.cs
@@ -1,3 +1,4 @@
+using Application.Common.Exceptions;
using MediatR;
using Microsoft.Extensions.Logging;
using System.Diagnostics;
@@ -35,7 +36,9 @@ public async Task Handle(TRequest request, RequestHandlerDelegate Handle(TRequest request, RequestHandlerDelegate
+/// Exception thrown when request processing fails in MediatR pipeline
+///
+public sealed class RequestProcessingException : Exception
+{
+ public RequestProcessingException(string message) : base(message)
+ {
+ }
+
+ public RequestProcessingException(string message, Exception innerException) : base(message, innerException)
+ {
+ }
+}
diff --git a/src/Application/Features/Auth/GetProfile/GetProfileQuery.cs b/src/Application/Features/Auth/GetProfile/GetProfileQuery.cs
index 3c3ab21..3551462 100644
--- a/src/Application/Features/Auth/GetProfile/GetProfileQuery.cs
+++ b/src/Application/Features/Auth/GetProfile/GetProfileQuery.cs
@@ -47,7 +47,7 @@ public sealed record GetProfileResponse
///
/// User roles
///
- public required string[] Roles { get; init; }
+ public required List Roles { get; init; }
///
/// Created date
diff --git a/src/Application/Features/Auth/Login/LoginCommand.cs b/src/Application/Features/Auth/Login/LoginCommand.cs
index 0af1b7c..cf53392 100644
--- a/src/Application/Features/Auth/Login/LoginCommand.cs
+++ b/src/Application/Features/Auth/Login/LoginCommand.cs
@@ -73,5 +73,5 @@ public sealed record UserInfo
///
/// User roles
///
- public required string[] Roles { get; init; }
+ public required List Roles { get; init; }
}
diff --git a/src/Application/Features/Auth/Login/LoginCommandHandler.cs b/src/Application/Features/Auth/Login/LoginCommandHandler.cs
index 831711d..9674c77 100644
--- a/src/Application/Features/Auth/Login/LoginCommandHandler.cs
+++ b/src/Application/Features/Auth/Login/LoginCommandHandler.cs
@@ -37,7 +37,7 @@ public async Task> Handle(LoginCommand request, Cancellati
Id = user.Id,
Email = user.Email,
FullName = user.FullName,
- Roles = user.Roles
+ Roles = [.. user.Roles.Select(r => r.ToString())]
};
var accessToken = tokenService.GenerateAccessToken(userInfo);
diff --git a/src/Application/Features/User/CreateUser/CreateUserCommand.cs b/src/Application/Features/User/CreateUser/CreateUserCommand.cs
index c479804..1713cac 100644
--- a/src/Application/Features/User/CreateUser/CreateUserCommand.cs
+++ b/src/Application/Features/User/CreateUser/CreateUserCommand.cs
@@ -33,7 +33,7 @@ public sealed record CreateUserCommand : ICommand>,
///
/// User roles
///
- public string[] Roles { get; init; } = ["User"];
+ public List Roles { get; init; } = ["User"];
// IAuthorizedRequest implementation
public AuthorizationRequirement AuthorizationRequirement => new()
diff --git a/src/Application/Features/User/CreateUser/CreateUserCommandHandler.cs b/src/Application/Features/User/CreateUser/CreateUserCommandHandler.cs
index d6166b8..15da024 100644
--- a/src/Application/Features/User/CreateUser/CreateUserCommandHandler.cs
+++ b/src/Application/Features/User/CreateUser/CreateUserCommandHandler.cs
@@ -23,7 +23,7 @@ public async Task> Handle(CreateUserCommand request,
}
// Create new user
- var user = Domain.Aggregates.User.User.Create(
+ var user = UserAggregate.Create(
request.Email,
request.FullName,
passwordHasher.HashPassword(request.Password),
@@ -31,7 +31,7 @@ public async Task> Handle(CreateUserCommand request,
try
{
- var createdUser = await userRepository.CreateAsync(user, cancellationToken);
+ var createdUser = await userRepository.CreateAsync(user.GetUser(), cancellationToken);
var response = new CreateUserResponse
{
diff --git a/src/Application/Features/User/CreateUser/CreateUserCommandValidator.cs b/src/Application/Features/User/CreateUser/CreateUserCommandValidator.cs
index c1f9a9b..e2aa742 100644
--- a/src/Application/Features/User/CreateUser/CreateUserCommandValidator.cs
+++ b/src/Application/Features/User/CreateUser/CreateUserCommandValidator.cs
@@ -39,7 +39,7 @@ public CreateUserCommandValidator()
RuleFor(x => x.Roles)
.NotEmpty()
.WithMessage("At least one role is required")
- .Must(roles => roles.All(role => !string.IsNullOrWhiteSpace(role)))
+ .Must(roles => roles.ToList().TrueForAll(role => !string.IsNullOrWhiteSpace(role)))
.WithMessage("Role names cannot be empty");
}
}
diff --git a/src/Application/Features/User/GetUsers/GetUsersQuery.cs b/src/Application/Features/User/GetUsers/GetUsersQuery.cs
index d8aafec..178bf1a 100644
--- a/src/Application/Features/User/GetUsers/GetUsersQuery.cs
+++ b/src/Application/Features/User/GetUsers/GetUsersQuery.cs
@@ -61,7 +61,7 @@ public sealed record UserSummary : AuditableEntity
///
/// User roles
///
- public required string[] Roles { get; init; }
+ public required List Roles { get; init; }
///
/// Is user active
diff --git a/src/Application/Interfaces/IUserRepository.cs b/src/Application/Interfaces/IUserRepository.cs
index c233c15..c3ab403 100644
--- a/src/Application/Interfaces/IUserRepository.cs
+++ b/src/Application/Interfaces/IUserRepository.cs
@@ -1,4 +1,4 @@
-using Domain.Aggregates.User;
+using Domain.Entities;
namespace Application.Interfaces;
diff --git a/src/Domain/Aggregates/Conversation/Conversation.cs b/src/Domain/Aggregates/Conversation/ConversationAggregate.cs
similarity index 93%
rename from src/Domain/Aggregates/Conversation/Conversation.cs
rename to src/Domain/Aggregates/Conversation/ConversationAggregate.cs
index 7da105b..6706088 100644
--- a/src/Domain/Aggregates/Conversation/Conversation.cs
+++ b/src/Domain/Aggregates/Conversation/ConversationAggregate.cs
@@ -1,7 +1,6 @@
using Domain.Common;
using Domain.Entities;
using Domain.Events.Conversation;
-using Domain.Enums;
namespace Domain.Aggregates.Conversation;
@@ -73,7 +72,7 @@ public static ConversationAggregate Create(Guid ownerId, string title, bool isPr
///
/// Add message to conversation
///
- public void AddMessage(Guid senderId, string content, MessageType type = MessageType.Text, bool isFromBot = false)
+ public void AddMessage(Guid senderId, string content, Entities.MessageType type = Entities.MessageType.Text, bool isFromBot = false)
{
// Business rules validation
if (_conversation.Status != ConversationStatus.Active)
@@ -140,10 +139,10 @@ public void UpdateTitle(string newTitle)
///
/// Change privacy settings
///
- public void SetPrivacy(bool isPrivate, UserRole userRole)
+ public void SetPrivacy(bool isPrivate, string userRole)
{
// Business rule: Only owner or admins can change privacy
- if (userRole != UserRole.Admin && _conversation.OwnerId != Id)
+ if (userRole != Constants.UserRoles.Admin && _conversation.OwnerId != Id)
{
throw new UnauthorizedAccessException("Only owner or admin can change privacy settings");
}
@@ -183,9 +182,9 @@ public void AddTags(params string[] newTags)
///
/// Check if user can access this conversation
///
- public bool CanBeAccessedBy(Guid userId, UserRole[] userRoles)
+ public bool CanBeAccessedBy(Guid userId, string[] userRoles)
{
- return _conversation.IsAccessibleBy(userId, userRoles.Select(r => r.ToString()).ToArray());
+ return _conversation.IsAccessibleBy(userId, userRoles);
}
// Private helper methods
diff --git a/src/Domain/Aggregates/User/User.cs b/src/Domain/Aggregates/User/UserAggregate.cs
similarity index 80%
rename from src/Domain/Aggregates/User/User.cs
rename to src/Domain/Aggregates/User/UserAggregate.cs
index a660dd5..50b5361 100644
--- a/src/Domain/Aggregates/User/User.cs
+++ b/src/Domain/Aggregates/User/UserAggregate.cs
@@ -1,7 +1,7 @@
using Domain.Common;
+using Domain.Constants;
using Domain.Entities;
using Domain.Events.User;
-using Domain.Enums;
using Domain.ValueObjects;
namespace Domain.Aggregates.User;
@@ -24,7 +24,7 @@ public UserAggregate(Entities.User user)
public string FullName => _user.FullName;
public string PasswordHash => _user.PasswordHash;
public string? PhoneNumber => _user.PhoneNumber;
- public UserRole[] Roles => _user.Roles;
+ public List Roles => _user.Roles;
public bool IsActive => _user.IsActive;
// Get the underlying entity
@@ -33,7 +33,7 @@ public UserAggregate(Entities.User user)
///
/// Create a new user aggregate
///
- public static UserAggregate Create(string email, string fullName, string passwordHash, UserRole[]? roles = null)
+ public static UserAggregate Create(string email, string fullName, string passwordHash, List? roles = null)
{
// Validate business rules
if (string.IsNullOrWhiteSpace(email))
@@ -57,13 +57,13 @@ public static UserAggregate Create(string email, string fullName, string passwor
Email = email,
FullName = fullName,
PasswordHash = passwordHash,
- Roles = roles ?? [UserRole.User]
+ Roles = roles?.ToList() ?? [UserRoles.User]
};
var aggregate = new UserAggregate(user);
// Raise domain event
- aggregate.AddDomainEvent(new UserCreatedEvent(user.Id, user.Email, user.FullName, user.Roles.Select(r => r.ToString()).ToArray()));
+ aggregate.AddDomainEvent(new UserCreatedEvent(user.Id, user.Email, user.FullName, user.Roles));
return aggregate;
}
@@ -109,16 +109,14 @@ public void UpdateInfo(string fullName, string? phoneNumber = null)
///
/// Add role to user
///
- public void AddRole(UserRole role)
+ public void AddRole(string role)
{
- if (_user.Roles.Contains(role))
+ if (string.IsNullOrWhiteSpace(role))
{
- return;
+ throw new ArgumentException("Role cannot be empty", nameof(role));
}
- var rolesList = _user.Roles.ToList();
- rolesList.Add(role);
- _user.Roles = rolesList.ToArray();
+ _user.AddRole(role);
_user.UpdatedAt = DateTime.UtcNow;
UpdatedAt = DateTime.UtcNow;
}
@@ -126,20 +124,20 @@ public void AddRole(UserRole role)
///
/// Remove role from user
///
- public void RemoveRole(UserRole role)
+ public void RemoveRole(string role)
{
- if (!_user.Roles.Contains(role))
+ if (string.IsNullOrWhiteSpace(role))
{
- return;
+ throw new ArgumentException("Role cannot be empty", nameof(role));
}
// Business rule: User must have at least one role
- if (_user.Roles.Length <= 1)
+ if (_user.Roles.Count <= 1)
{
throw new InvalidOperationException("User must have at least one role");
}
- _user.Roles = _user.Roles.Where(r => r != role).ToArray();
+ _user.RemoveRole(role);
_user.UpdatedAt = DateTime.UtcNow;
UpdatedAt = DateTime.UtcNow;
}
@@ -154,17 +152,19 @@ public bool CanPerformAction(string action)
{
"CreateConversation" => IsActive,
"EditProfile" => IsActive,
- "ManageUsers" => _user.HasRole(UserRole.Admin),
- "AccessLegalDatabase" => _user.HasAnyRole(UserRole.Admin, UserRole.LegalExpert),
+ "ManageUsers" => _user.HasRole(UserRoles.Admin),
+ "AccessLegalDatabase" => _user.HasAnyRole(UserRoles.Admin, UserRoles.LegalExpert),
_ => false
};
}
// Private helper methods
+ private const bool DefaultActiveConversationStatus = false;
+
private bool HasActiveConversations()
{
// This would typically be checked via a domain service
- // For now, assume false
- return false;
+ // For now, return constant as an example
+ return DefaultActiveConversationStatus;
}
}
diff --git a/src/Domain/Constants/UserRoles.cs b/src/Domain/Constants/UserRoles.cs
new file mode 100644
index 0000000..dd2451f
--- /dev/null
+++ b/src/Domain/Constants/UserRoles.cs
@@ -0,0 +1,71 @@
+namespace Domain.Constants;
+
+///
+/// User role constants - type-safe role strings
+///
+public static class UserRoles
+{
+ ///
+ /// Regular user with basic access
+ ///
+ public const string User = "User";
+
+ ///
+ /// Premium user with extended features
+ ///
+ public const string Premium = "Premium";
+
+ ///
+ /// Legal expert with consultation capabilities
+ ///
+ public const string LegalExpert = "LegalExpert";
+
+ ///
+ /// Manager with team oversight capabilities
+ ///
+ public const string Manager = "Manager";
+
+ ///
+ /// Administrator with full system access
+ ///
+ public const string Admin = "Admin";
+
+ ///
+ /// System user for automated processes
+ ///
+ public const string System = "System";
+
+ ///
+ /// All available roles
+ ///
+ public static readonly string[] All =
+ [
+ User,
+ Premium,
+ LegalExpert,
+ Manager,
+ Admin,
+ System
+ ];
+
+ ///
+ /// Elevated privilege roles
+ ///
+ public static readonly string[] Elevated =
+ [
+ LegalExpert,
+ Manager,
+ Admin,
+ System
+ ];
+
+ ///
+ /// Check if role is valid
+ ///
+ public static bool IsValid(string role) => All.Contains(role, StringComparer.OrdinalIgnoreCase);
+
+ ///
+ /// Check if role has elevated privileges
+ ///
+ public static bool IsElevated(string role) => Elevated.Contains(role, StringComparer.OrdinalIgnoreCase);
+}
diff --git a/src/Domain/Entities/User.cs b/src/Domain/Entities/User.cs
index 6273d73..525aa87 100644
--- a/src/Domain/Entities/User.cs
+++ b/src/Domain/Entities/User.cs
@@ -1,5 +1,4 @@
using Domain.Common;
-using Domain.Enums;
namespace Domain.Entities;
@@ -12,7 +11,7 @@ public sealed class User : BaseEntity
public required string FullName { get; set; }
public required string PasswordHash { get; set; }
public string? PhoneNumber { get; set; }
- public UserRole[] Roles { get; set; } = [];
+ public List Roles { get; set; } = [];
// Navigation properties for related entities
public ICollection OwnedConversations { get; set; } = [];
@@ -21,12 +20,36 @@ public sealed class User : BaseEntity
///
/// Check if user has specific role
///
- public bool HasRole(UserRole role) => Roles.Contains(role);
+ public bool HasRole(string role) => Roles.Contains(role, StringComparer.OrdinalIgnoreCase);
///
/// Check if user has any of the specified roles
///
- public bool HasAnyRole(params UserRole[] roles) => roles.Any(role => Roles.Contains(role));
+ public bool HasAnyRole(params string[] roles) => roles.Any(role => Roles.Contains(role, StringComparer.OrdinalIgnoreCase));
+
+ ///
+ /// Add role to user
+ ///
+ public void AddRole(string role)
+ {
+ if (!HasRole(role))
+ {
+ var rolesList = Roles.ToList();
+ rolesList.Add(role);
+ Roles = rolesList.ToList();
+ }
+ }
+
+ ///
+ /// Remove role from user
+ ///
+ public void RemoveRole(string role)
+ {
+ if (HasRole(role))
+ {
+ Roles = Roles.Where(r => !string.Equals(r, role, StringComparison.OrdinalIgnoreCase)).ToList();
+ }
+ }
///
/// Validate email format (basic validation in entity)
diff --git a/src/Domain/Enums/UserRole.cs b/src/Domain/Enums/UserRole.cs
deleted file mode 100644
index 5553aec..0000000
--- a/src/Domain/Enums/UserRole.cs
+++ /dev/null
@@ -1,62 +0,0 @@
-namespace Domain.Enums;
-
-///
-/// User roles enumeration
-///
-public enum UserRole
-{
- ///
- /// Regular user
- ///
- User = 0,
-
- ///
- /// Administrator
- ///
- Admin = 1,
-
- ///
- /// Manager
- ///
- Manager = 2,
-
- ///
- /// Legal expert
- ///
- LegalExpert = 3,
-
- ///
- /// Premium user with extended features
- ///
- Premium = 4,
-
- ///
- /// System user (for automated processes)
- ///
- System = 5
-}
-
-///
-/// Extension methods for UserRole enum
-///
-public static class UserRoleExtensions
-{
- ///
- /// Get display name for role
- ///
- public static string GetDisplayName(this UserRole role) => role switch
- {
- UserRole.User => "User",
- UserRole.Admin => "Administrator",
- UserRole.Manager => "Manager",
- UserRole.LegalExpert => "Legal Expert",
- UserRole.Premium => "Premium User",
- UserRole.System => "System",
- _ => "Unknown"
- };
-
- ///
- /// Check if role has elevated privileges
- ///
- public static bool IsElevated(this UserRole role) => role is UserRole.LegalExpert or UserRole.Manager or UserRole.Admin or UserRole.System;
-}
diff --git a/src/Domain/Events/Examples/DomainEventsUsageExamples.cs b/src/Domain/Events/Examples/DomainEventsUsageExamples.cs
index 20b4482..79a59cf 100644
--- a/src/Domain/Events/Examples/DomainEventsUsageExamples.cs
+++ b/src/Domain/Events/Examples/DomainEventsUsageExamples.cs
@@ -16,7 +16,10 @@ public static class DomainEventsUsageExamples
public static void UserRegistrationExample()
{
// 1. Tạo user aggregate
- var user = Domain.Aggregates.User.User.Create("john@example.com", "John Doe", "hashedPassword", ["User"]);
+ var userAggregate = UserAggregate.Create("john@example.com", "John Doe", "hashedPassword", ["User"]);
+
+ // Use the aggregate for demonstration
+ Console.WriteLine($"User created: {userAggregate.Email}");
// 2. Domain event sẽ được raise automatically trong User.Create()
// UserCreatedEvent sẽ trigger:
@@ -121,6 +124,9 @@ public static void BestPracticesExample()
roles: ["User"]
);
+ // Use the event for demonstration
+ Console.WriteLine($"Event created: {userCreated.GetType().Name}");
+
// ❌ BAD: Mutable, confusing name
// var createUserEvent = new CreateUserEvent { ... };
}
@@ -139,7 +145,10 @@ public static class LegalAssistantDomainEvents
/// - UserSubscriptionChangedEvent
/// - UserTierUpgradedEvent
///
- public static void UserEvents() { }
+ public static void UserEvents()
+ {
+ // Example method for documentation purposes - lists available user domain events
+ }
///
/// Conversation Domain Events:
@@ -149,7 +158,10 @@ public static void UserEvents() { }
/// - ExpertAssignedEvent
/// - ConversationRatedEvent
///
- public static void ConversationEvents() { }
+ public static void ConversationEvents()
+ {
+ // Example method for documentation purposes - lists available conversation domain events
+ }
///
/// Legal Service Domain Events:
@@ -159,7 +171,10 @@ public static void ConversationEvents() { }
/// - LegalAdviceProvidedEvent
/// - CaseFileCreatedEvent
///
- public static void LegalServiceEvents() { }
+ public static void LegalServiceEvents()
+ {
+ // Example method for documentation purposes - lists available legal service domain events
+ }
///
/// Payment Domain Events:
@@ -169,5 +184,8 @@ public static void LegalServiceEvents() { }
/// - RefundProcessedEvent
/// - InvoiceGeneratedEvent
///
- public static void PaymentEvents() { }
+ public static void PaymentEvents()
+ {
+ // Example method for documentation purposes - lists available payment domain events
+ }
}
diff --git a/src/Domain/Events/User/UserCreatedEvent.cs b/src/Domain/Events/User/UserCreatedEvent.cs
index cca2511..014ed81 100644
--- a/src/Domain/Events/User/UserCreatedEvent.cs
+++ b/src/Domain/Events/User/UserCreatedEvent.cs
@@ -10,10 +10,10 @@ public sealed record UserCreatedEvent : IDomainEvent
public Guid UserId { get; }
public string Email { get; }
public string FullName { get; }
- public string[] Roles { get; }
+ public List Roles { get; }
public DateTime OccurredOn { get; }
- public UserCreatedEvent(Guid userId, string email, string fullName, string[] roles)
+ public UserCreatedEvent(Guid userId, string email, string fullName, List roles)
{
UserId = userId;
Email = email;
diff --git a/src/Domain/GlobalSuppressions.cs b/src/Domain/GlobalSuppressions.cs
new file mode 100644
index 0000000..2e88af7
--- /dev/null
+++ b/src/Domain/GlobalSuppressions.cs
@@ -0,0 +1,8 @@
+// This file is used by Code Analysis to maintain SuppressMessage
+// attributes that are applied to this project.
+// Project-level suppressions either have no target or are given
+// a specific target and scoped to a namespace, type, member, etc.
+
+using System.Diagnostics.CodeAnalysis;
+
+[assembly: SuppressMessage("Major Code Smell", "S2589:Boolean expressions should not be gratuitous", Justification = "", Scope = "member", Target = "~M:Domain.Specifications.Examples.SpecificationUsageExamples.DynamicQueryBuilding")]
diff --git a/src/Domain/Services/UserDomainService.cs b/src/Domain/Services/UserDomainService.cs
index 5c3edfb..0a9e174 100644
--- a/src/Domain/Services/UserDomainService.cs
+++ b/src/Domain/Services/UserDomainService.cs
@@ -8,10 +8,10 @@ public sealed class UserDomainService
///
/// Check if user can be assigned to specific role
///
- public bool CanAssignRole(Domain.Aggregates.User.User user, string newRole)
+ public bool CanAssignRole(Domain.Aggregates.User.UserAggregate userAggregate, string newRole)
{
// Business rule: Admin can't be demoted if they're the last admin
- if (user.Roles.Contains("Admin") && newRole != "Admin")
+ if (userAggregate.Roles.Contains("Admin") && newRole != "Admin")
{
// This would require checking other admins - complex business logic
// that doesn't belong to User entity alone
diff --git a/src/Domain/Specifications/Examples/SpecificationUsageExamples.cs b/src/Domain/Specifications/Examples/SpecificationUsageExamples.cs
index 6f8f820..88ba17c 100644
--- a/src/Domain/Specifications/Examples/SpecificationUsageExamples.cs
+++ b/src/Domain/Specifications/Examples/SpecificationUsageExamples.cs
@@ -1,4 +1,5 @@
using Domain.Aggregates.User;
+using Domain.Entities;
namespace Domain.Specifications.Examples;
@@ -16,9 +17,11 @@ public static void SimpleUsage()
// Find active users
var activeUsers = users.Where(UserSpecs.Active.ToExpression().Compile());
+ Console.WriteLine($"Found {activeUsers.Count()} active users");
// Find admin users
var adminUsers = users.Where(UserSpecs.Admin.ToExpression().Compile());
+ Console.WriteLine($"Found {adminUsers.Count()} admin users");
}
///
@@ -31,14 +34,17 @@ public static void CombinedSpecifications()
// Active AND Admin users
var activeAdmins = UserSpecs.Active & UserSpecs.Admin;
var result1 = users.Where(activeAdmins.ToExpression().Compile());
+ Console.WriteLine($"Found {result1.Count()} active admins");
// Users with legal access OR admin role
var legalOrAdmin = UserSpecs.LegalAccess | UserSpecs.Admin;
var result2 = users.Where(legalOrAdmin.ToExpression().Compile());
+ Console.WriteLine($"Found {result2.Count()} legal/admin users");
// NOT deleted users (same as active)
var notDeleted = !new UserHasRoleSpecification("Deleted");
var result3 = users.Where(notDeleted.ToExpression().Compile());
+ Console.WriteLine($"Found {result3.Count()} non-deleted users");
}
///
@@ -54,6 +60,7 @@ public static void ComplexBusinessRules()
UserSpecs.CreatedAfter(DateTime.UtcNow.AddMonths(-6));
var result = users.Where(recentActiveLegalExperts.ToExpression().Compile());
+ Console.WriteLine($"Found {result.Count()} recent legal experts");
// Business rule: "Company users excluding admins"
var companyNonAdmins = UserSpecs.EmailDomain("company.com") &
@@ -61,6 +68,7 @@ public static void ComplexBusinessRules()
!UserSpecs.Admin;
var result2 = users.Where(companyNonAdmins.ToExpression().Compile());
+ Console.WriteLine($"Found {result2.Count()} company non-admins");
}
///
@@ -98,12 +106,16 @@ public static void DynamicQueryBuilding()
if (requireLegalAccess)
{
querySpec = (ActiveUserSpecification)(querySpec & UserSpecs.LegalAccess);
+ Console.WriteLine("Added legal access requirement");
}
+#pragma warning disable S2583 // Conditionally executed code should be reachable
if (excludeAdmins)
{
querySpec = (ActiveUserSpecification)(querySpec & !UserSpecs.Admin);
+ Console.WriteLine("Excluded admin users");
}
+#pragma warning restore S2583 // Conditionally executed code should be reachable
if (!string.IsNullOrEmpty(emailDomain))
{
@@ -111,6 +123,7 @@ public static void DynamicQueryBuilding()
}
var result = users.Where(querySpec.ToExpression().Compile());
+ Console.WriteLine($"Dynamic query returned {result.Count()} users");
}
// Helper methods
diff --git a/src/Domain/Specifications/UserSpecifications.cs b/src/Domain/Specifications/UserSpecifications.cs
index 8f0d924..130aab8 100644
--- a/src/Domain/Specifications/UserSpecifications.cs
+++ b/src/Domain/Specifications/UserSpecifications.cs
@@ -1,5 +1,6 @@
using System.Linq.Expressions;
using Domain.Aggregates.User;
+using Domain.Entities;
namespace Domain.Specifications;
@@ -21,7 +22,7 @@ public sealed class UserHasRoleSpecification(string role) : Specification
{
public override Expression> ToExpression()
{
- return user => user.Roles.Contains(role);
+ return user => user.HasRole(role);
}
}
@@ -54,7 +55,7 @@ public sealed class AdminUserSpecification : Specification
{
public override Expression> ToExpression()
{
- return user => user.Roles.Contains("Admin");
+ return user => user.HasRole("Admin");
}
}
@@ -65,9 +66,9 @@ public sealed class LegalAccessUserSpecification : Specification
{
public override Expression> ToExpression()
{
- return user => user.Roles.Contains("Admin") ||
- user.Roles.Contains("LegalExpert") ||
- user.Roles.Contains("Manager");
+ return user => user.HasRole("Admin") ||
+ user.HasRole("LegalExpert") ||
+ user.HasRole("Manager");
}
}
diff --git a/src/Infrastructure/Configuration/EmailSettings.cs b/src/Infrastructure/Configuration/EmailSettings.cs
new file mode 100644
index 0000000..e7d67d5
--- /dev/null
+++ b/src/Infrastructure/Configuration/EmailSettings.cs
@@ -0,0 +1,17 @@
+namespace Infrastructure.Configuration;
+
+///
+/// Email service configuration settings
+///
+public sealed class EmailSettings
+{
+ public const string SectionName = "EmailSettings";
+
+ public string SmtpServer { get; set; } = string.Empty;
+ public int SmtpPort { get; set; } = 587;
+ public string Username { get; set; } = string.Empty;
+ public string Password { get; set; } = string.Empty;
+ public string FromEmail { get; set; } = string.Empty;
+ public string FromName { get; set; } = string.Empty;
+ public bool EnableSsl { get; set; } = true;
+}
diff --git a/src/Infrastructure/Configuration/FileStorageSettings.cs b/src/Infrastructure/Configuration/FileStorageSettings.cs
new file mode 100644
index 0000000..5bdcea5
--- /dev/null
+++ b/src/Infrastructure/Configuration/FileStorageSettings.cs
@@ -0,0 +1,16 @@
+namespace Infrastructure.Configuration;
+
+///
+/// File storage configuration settings
+///
+public sealed class FileStorageSettings
+{
+ public const string SectionName = "FileStorageSettings";
+
+ public string Provider { get; set; } = "Local"; // Local, Azure, AWS
+ public string ConnectionString { get; set; } = string.Empty;
+ public string ContainerName { get; set; } = "files";
+ public string BasePath { get; set; } = "uploads";
+ public long MaxFileSizeBytes { get; set; } = 10 * 1024 * 1024; // 10MB
+ public string[] AllowedExtensions { get; set; } = [".pdf", ".docx", ".png", ".jpg"];
+}
diff --git a/src/Infrastructure/Configuration/JwtSettings.cs b/src/Infrastructure/Configuration/JwtSettings.cs
new file mode 100644
index 0000000..e132722
--- /dev/null
+++ b/src/Infrastructure/Configuration/JwtSettings.cs
@@ -0,0 +1,15 @@
+namespace Infrastructure.Configuration;
+
+///
+/// JWT authentication configuration settings
+///
+public sealed class JwtSettings
+{
+ public const string SectionName = "JwtSettings";
+
+ public string SecretKey { get; set; } = string.Empty;
+ public string Issuer { get; set; } = string.Empty;
+ public string Audience { get; set; } = string.Empty;
+ public int ExpirationHours { get; set; } = 1;
+ public int RefreshTokenExpirationDays { get; set; } = 7;
+}
diff --git a/src/Infrastructure/Data/Contexts/DataContext.cs b/src/Infrastructure/Data/Contexts/DataContext.cs
index 5725473..07e15a6 100644
--- a/src/Infrastructure/Data/Contexts/DataContext.cs
+++ b/src/Infrastructure/Data/Contexts/DataContext.cs
@@ -24,8 +24,6 @@ protected override void OnModelCreating(ModelBuilder modelBuilder)
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
- base.OnConfiguring(optionsBuilder);
-
// Cấu hình logging cho Entity Framework (chỉ trong Development)
#if DEBUG
optionsBuilder.EnableSensitiveDataLogging();
diff --git a/src/Infrastructure/Exceptions/DomainEventDispatchException.cs b/src/Infrastructure/Exceptions/DomainEventDispatchException.cs
new file mode 100644
index 0000000..ab4c568
--- /dev/null
+++ b/src/Infrastructure/Exceptions/DomainEventDispatchException.cs
@@ -0,0 +1,15 @@
+namespace Infrastructure.Exceptions;
+
+///
+/// Exception thrown when domain event dispatching fails
+///
+public sealed class DomainEventDispatchException : Exception
+{
+ public DomainEventDispatchException(string message) : base(message)
+ {
+ }
+
+ public DomainEventDispatchException(string message, Exception innerException) : base(message, innerException)
+ {
+ }
+}
diff --git a/src/Infrastructure/Extensions/ServiceCollectionExtensions.cs b/src/Infrastructure/Extensions/ServiceCollectionExtensions.cs
new file mode 100644
index 0000000..cfc53de
--- /dev/null
+++ b/src/Infrastructure/Extensions/ServiceCollectionExtensions.cs
@@ -0,0 +1,171 @@
+using Application.Common;
+using Application.Interfaces;
+using Infrastructure.Configuration;
+using Infrastructure.Data.Contexts;
+using Infrastructure.Repositories;
+using Infrastructure.Services;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.Extensions.Configuration;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Diagnostics.HealthChecks;
+
+namespace Infrastructure.Extensions;
+
+///
+/// Infrastructure service registration extensions
+///
+public static class ServiceCollectionExtensions
+{
+ ///
+ /// Add Infrastructure layer services
+ ///
+ public static IServiceCollection AddInfrastructureServices(
+ this IServiceCollection services,
+ IConfiguration configuration)
+ {
+ // Database
+ services.AddDatabase(configuration);
+
+ // Repositories
+ services.AddRepositories();
+
+ // Services
+ services.AddInfrastructureApplicationServices();
+
+ // External Services
+ services.AddExternalServices(configuration);
+
+ return services;
+ }
+
+ ///
+ /// Add database services
+ ///
+ private static IServiceCollection AddDatabase(
+ this IServiceCollection services,
+ IConfiguration configuration)
+ {
+ var connectionString = configuration.GetConnectionString("DefaultConnection")
+ ?? "Server=(localdb)\\mssqllocaldb;Database=LegalAssistantDb;Trusted_Connection=true;MultipleActiveResultSets=true";
+
+ services.AddDbContext(options =>
+ {
+ options.UseSqlServer(connectionString, sqlOptions =>
+ sqlOptions.EnableRetryOnFailure(
+ maxRetryCount: 3,
+ maxRetryDelay: TimeSpan.FromSeconds(30),
+ errorNumbersToAdd: null));
+
+#if DEBUG
+ options.EnableSensitiveDataLogging();
+ options.EnableDetailedErrors();
+#endif
+ });
+
+ // Register as interface
+ services.AddScoped(provider => provider.GetRequiredService());
+
+ return services;
+ }
+
+ ///
+ /// Add repository implementations
+ ///
+ private static IServiceCollection AddRepositories(this IServiceCollection services)
+ {
+ services.AddScoped();
+
+ // TODO: Add other repositories here
+ // services.AddScoped();
+ // services.AddScoped();
+
+ return services;
+ }
+
+ ///
+ /// Add Infrastructure implementations of Application interfaces
+ ///
+ private static IServiceCollection AddInfrastructureApplicationServices(this IServiceCollection services)
+ {
+ // Authentication & Security
+ services.AddScoped();
+ services.AddScoped();
+
+ // Domain Events
+ services.AddScoped();
+
+ // TODO: Add other application services
+ // services.AddScoped();
+ // services.AddScoped();
+ // services.AddScoped();
+
+ return services;
+ }
+
+ ///
+ /// Add external service integrations
+ ///
+ private static IServiceCollection AddExternalServices(
+ this IServiceCollection services,
+ IConfiguration configuration)
+ {
+ // Email Service
+ services.Configure(configuration.GetSection("EmailSettings"));
+
+ // File Storage
+ services.Configure(configuration.GetSection("FileStorageSettings"));
+
+ // JWT Settings
+ services.Configure(configuration.GetSection("JwtSettings"));
+
+ // TODO: Add external service clients
+ // services.AddHttpClient();
+
+ return services;
+ }
+
+ ///
+ /// Add caching services
+ ///
+ public static IServiceCollection AddCaching(
+ this IServiceCollection services,
+ IConfiguration configuration)
+ {
+ // Memory Cache (already added in Application layer)
+
+ // Redis Cache (optional)
+ var redisConnectionString = configuration.GetConnectionString("Redis");
+ if (!string.IsNullOrEmpty(redisConnectionString))
+ {
+ services.AddStackExchangeRedisCache(options => options.Configuration = redisConnectionString);
+ }
+
+ return services;
+ }
+
+ ///
+ /// Add health checks
+ ///
+ public static IServiceCollection AddHealthChecks(
+ this IServiceCollection services,
+ IConfiguration configuration)
+ {
+ var healthChecks = services.AddHealthChecks();
+
+ // Database health check
+ var connectionString = configuration.GetConnectionString("DefaultConnection");
+ if (!string.IsNullOrEmpty(connectionString))
+ {
+ healthChecks.AddSqlServer(connectionString, name: "database");
+ }
+
+ // Redis health check (if configured)
+ var redisConnectionString = configuration.GetConnectionString("Redis");
+ if (!string.IsNullOrEmpty(redisConnectionString))
+ {
+ healthChecks.AddRedis(redisConnectionString, name: "redis");
+ }
+
+ return services;
+ }
+}
diff --git a/src/Infrastructure/Infrastructure.csproj b/src/Infrastructure/Infrastructure.csproj
index 663d7cb..b92a4b1 100644
--- a/src/Infrastructure/Infrastructure.csproj
+++ b/src/Infrastructure/Infrastructure.csproj
@@ -17,6 +17,12 @@
all
runtime; build; native; contentfiles; analyzers; buildtransitive
+
+
+
+
+
+
diff --git a/src/Infrastructure/Repositories/UserRepository.cs b/src/Infrastructure/Repositories/UserRepository.cs
new file mode 100644
index 0000000..22744da
--- /dev/null
+++ b/src/Infrastructure/Repositories/UserRepository.cs
@@ -0,0 +1,63 @@
+using Application.Interfaces;
+using Domain.Entities;
+using Infrastructure.Data.Contexts;
+using Microsoft.EntityFrameworkCore;
+
+namespace Infrastructure.Repositories;
+
+///
+/// User repository implementation - connects to database
+///
+public sealed class UserRepository : IUserRepository
+{
+ private readonly DataContext _context;
+
+ public UserRepository(DataContext context)
+ {
+ _context = context;
+ }
+
+ ///
+ /// Get user by email from database
+ ///
+ public async Task GetByEmailAsync(string email, CancellationToken cancellationToken = default)
+ {
+ return await _context.Set()
+ .Where(u => u.Email == email && !u.IsDeleted)
+ .FirstOrDefaultAsync(cancellationToken);
+ }
+
+ ///
+ /// Get user by ID from database
+ ///
+ public async Task GetByIdAsync(Guid id, CancellationToken cancellationToken = default)
+ {
+ return await _context.Set()
+ .Where(u => u.Id == id && !u.IsDeleted)
+ .FirstOrDefaultAsync(cancellationToken);
+ }
+
+ ///
+ /// Update user in database
+ ///
+ public async Task UpdateAsync(User user, CancellationToken cancellationToken = default)
+ {
+ _context.Set().Update(user);
+ await _context.SaveChangesAsync(cancellationToken);
+ }
+
+ ///
+ /// Create new user in database
+ ///
+ public async Task CreateAsync(User user, CancellationToken cancellationToken = default)
+ {
+ // Add to context
+ var entityEntry = await _context.Set().AddAsync(user, cancellationToken);
+
+ // Save to database
+ await _context.SaveChangesAsync(cancellationToken);
+
+ // Return created user with generated ID
+ return entityEntry.Entity;
+ }
+}
diff --git a/src/Infrastructure/Services/DomainEventDispatcher.cs b/src/Infrastructure/Services/DomainEventDispatcher.cs
index 2e4a9ac..0613f3e 100644
--- a/src/Infrastructure/Services/DomainEventDispatcher.cs
+++ b/src/Infrastructure/Services/DomainEventDispatcher.cs
@@ -1,5 +1,6 @@
using Application.Common;
using Domain.Common;
+using Infrastructure.Exceptions;
using MediatR;
using Microsoft.Extensions.Logging;
@@ -45,7 +46,7 @@ public async Task DispatchEventAsync(IDomainEvent domainEvent, CancellationToken
catch (Exception ex)
{
logger.LogError(ex, "Error dispatching domain event: {EventType}", domainEvent.GetType().Name);
- throw;
+ throw new DomainEventDispatchException($"Failed to dispatch domain event of type {domainEvent.GetType().Name}", ex);
}
}
}
diff --git a/src/Infrastructure/Services/PasswordHasher.cs b/src/Infrastructure/Services/PasswordHasher.cs
new file mode 100644
index 0000000..f83ecfc
--- /dev/null
+++ b/src/Infrastructure/Services/PasswordHasher.cs
@@ -0,0 +1,94 @@
+using Application.Interfaces;
+using System.Security.Cryptography;
+using System.Text;
+
+namespace Infrastructure.Services;
+
+///
+/// Password hashing service using BCrypt-like approach
+///
+public sealed class PasswordHasher : IPasswordHasher
+{
+ private const int SaltSize = 16;
+ private const int HashSize = 32;
+ private const int Iterations = 100000; // Security requirement: min 100,000 iterations
+
+ ///
+ /// Hash password with salt
+ ///
+ public string HashPassword(string password)
+ {
+ if (string.IsNullOrWhiteSpace(password))
+ {
+ throw new ArgumentException("Password cannot be empty", nameof(password));
+ }
+
+ // Generate salt
+ using var rng = RandomNumberGenerator.Create();
+ var salt = new byte[SaltSize];
+ rng.GetBytes(salt);
+
+ // Hash password with salt
+ using var pbkdf2 = new Rfc2898DeriveBytes(
+ Encoding.UTF8.GetBytes(password),
+ salt,
+ Iterations,
+ HashAlgorithmName.SHA256);
+
+ var hash = pbkdf2.GetBytes(HashSize);
+
+ // Combine salt + hash
+ var result = new byte[SaltSize + HashSize];
+ Array.Copy(salt, 0, result, 0, SaltSize);
+ Array.Copy(hash, 0, result, SaltSize, HashSize);
+
+ return Convert.ToBase64String(result);
+ }
+
+ ///
+ /// Verify password against hash
+ ///
+ public bool VerifyPassword(string password, string hash)
+ {
+ if (string.IsNullOrWhiteSpace(password) || string.IsNullOrWhiteSpace(hash))
+ {
+ return false;
+ }
+
+ try
+ {
+ var hashBytes = Convert.FromBase64String(hash);
+
+ if (hashBytes.Length != SaltSize + HashSize)
+ {
+ return false;
+ }
+
+ // Extract salt
+ var salt = new byte[SaltSize];
+ Array.Copy(hashBytes, 0, salt, 0, SaltSize);
+
+ // Extract hash
+ var existingHash = new byte[HashSize];
+ Array.Copy(hashBytes, SaltSize, existingHash, 0, HashSize);
+
+ // Hash input password with same salt
+#pragma warning disable S2053 // Password hashing functions should use an unpredictable salt
+ using var pbkdf2 = new Rfc2898DeriveBytes(
+ Encoding.UTF8.GetBytes(password),
+ salt,
+ Iterations,
+ HashAlgorithmName.SHA256);
+#pragma warning restore S2053 // Password hashing functions should use an unpredictable salt
+
+ var newHash = pbkdf2.GetBytes(HashSize);
+
+ // Compare hashes
+ return CryptographicOperations.FixedTimeEquals(existingHash, newHash);
+ }
+ catch
+ {
+ return false;
+ }
+ }
+}
diff --git a/src/Infrastructure/Services/TokenService.cs b/src/Infrastructure/Services/TokenService.cs
new file mode 100644
index 0000000..3faf8e8
--- /dev/null
+++ b/src/Infrastructure/Services/TokenService.cs
@@ -0,0 +1,126 @@
+using Application.Interfaces;
+using Microsoft.Extensions.Configuration;
+using System.IdentityModel.Tokens.Jwt;
+using System.Security.Claims;
+using System.Security.Cryptography;
+using System.Text;
+using Microsoft.IdentityModel.Tokens;
+using Application.Features.Auth.Login;
+using System.Globalization;
+
+namespace Infrastructure.Services;
+
+///
+/// JWT token service for authentication
+///
+public sealed class TokenService : ITokenService
+{
+ private readonly IConfiguration _configuration;
+ private readonly JwtSecurityTokenHandler _tokenHandler;
+
+ public TokenService(IConfiguration configuration)
+ {
+ _configuration = configuration;
+ _tokenHandler = new JwtSecurityTokenHandler();
+ }
+
+ ///
+ /// Generate access token (JWT)
+ ///
+ public string GenerateAccessToken(UserInfo user)
+ {
+ var jwtSettings = _configuration.GetSection("JwtSettings");
+ var secretKey = jwtSettings["SecretKey"] ?? "YourSuperSecretKeyThatIsAtLeast32Characters!!";
+ var issuer = jwtSettings["Issuer"] ?? "LegalAssistant";
+ var audience = jwtSettings["Audience"] ?? "LegalAssistantUsers";
+ var expirationHours = int.Parse(jwtSettings["ExpirationHours"] ?? "1", CultureInfo.InvariantCulture);
+
+ var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(secretKey));
+ var credentials = new SigningCredentials(key, SecurityAlgorithms.HmacSha256);
+
+ var claims = new List
+ {
+ new(ClaimTypes.NameIdentifier, user.Id.ToString()),
+ new(ClaimTypes.Email, user.Email),
+ new(ClaimTypes.Name, user.FullName),
+ new(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()),
+ new(JwtRegisteredClaimNames.Iat,
+ DateTimeOffset.UtcNow.ToUnixTimeSeconds().ToString(CultureInfo.InvariantCulture),
+ ClaimValueTypes.Integer64)
+ };
+
+ // Add roles
+ foreach (var role in user.Roles)
+ {
+ claims.Add(new Claim(ClaimTypes.Role, role));
+ }
+
+ var tokenDescriptor = new SecurityTokenDescriptor
+ {
+ Subject = new ClaimsIdentity(claims),
+ Expires = DateTime.UtcNow.AddHours(expirationHours),
+ Issuer = issuer,
+ Audience = audience,
+ SigningCredentials = credentials
+ };
+
+ var token = _tokenHandler.CreateToken(tokenDescriptor);
+ return _tokenHandler.WriteToken(token);
+ }
+
+ ///
+ /// Generate refresh token (random string)
+ ///
+ public string GenerateRefreshToken()
+ {
+ var randomBytes = new byte[32];
+ using var rng = RandomNumberGenerator.Create();
+ rng.GetBytes(randomBytes);
+ return Convert.ToBase64String(randomBytes);
+ }
+
+ public Task?> GetClaimsFromTokenAsync(string token)
+ {
+ throw new NotImplementedException();
+ }
+
+ public Task ValidateAccessTokenAsync(string token)
+ {
+ throw new NotImplementedException();
+ }
+
+ ///
+ /// Validate token and extract claims
+ ///
+ public ClaimsPrincipal? ValidateToken(string token)
+ {
+ try
+ {
+ var jwtSettings = _configuration.GetSection("JwtSettings");
+ var secretKey = jwtSettings["SecretKey"] ?? "YourSuperSecretKeyThatIsAtLeast32Characters!!";
+ var issuer = jwtSettings["Issuer"] ?? "LegalAssistant";
+ var audience = jwtSettings["Audience"] ?? "LegalAssistantUsers";
+
+ var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(secretKey));
+
+ var validationParameters = new TokenValidationParameters
+ {
+ ValidateIssuerSigningKey = true,
+ IssuerSigningKey = key,
+ ValidateIssuer = true,
+ ValidIssuer = issuer,
+ ValidateAudience = true,
+ ValidAudience = audience,
+ ValidateLifetime = true,
+ ClockSkew = TimeSpan.Zero
+ };
+
+ var principal = _tokenHandler.ValidateToken(token, validationParameters, out _);
+ return principal;
+ }
+ catch
+ {
+ return null;
+ }
+ }
+}
diff --git a/src/Web.Api/Controllers/V1/AuthController.cs b/src/Web.Api/Controllers/V1/AuthController.cs
index ee26040..e567a9b 100644
--- a/src/Web.Api/Controllers/V1/AuthController.cs
+++ b/src/Web.Api/Controllers/V1/AuthController.cs
@@ -29,6 +29,6 @@ public async Task Login([FromBody] LoginCommand command)
return Ok(ApiResponse.CreateSuccess(result.Value!, "Login successful"));
}
- return BadRequest(ApiResponse
diff --git a/src/Web.Api/appsettings.json b/src/Web.Api/appsettings.json
index 10f68b8..2fbf78c 100644
--- a/src/Web.Api/appsettings.json
+++ b/src/Web.Api/appsettings.json
@@ -5,5 +5,33 @@
"Microsoft.AspNetCore": "Warning"
}
},
- "AllowedHosts": "*"
+ "AllowedHosts": "*",
+ "ConnectionStrings": {
+ "DefaultConnection": "Server=(localdb)\\mssqllocaldb;Database=LegalAssistantDb;Trusted_Connection=true;MultipleActiveResultSets=true;",
+ "Redis": ""
+ },
+ "JwtSettings": {
+ "SecretKey": "YourSuperSecretKeyThatIsAtLeast32CharactersLong!!",
+ "Issuer": "LegalAssistant",
+ "Audience": "LegalAssistantUsers",
+ "ExpirationHours": 1,
+ "RefreshTokenExpirationDays": 7
+ },
+ "EmailSettings": {
+ "SmtpServer": "smtp.gmail.com",
+ "SmtpPort": 587,
+ "Username": "",
+ "Password": "",
+ "FromEmail": "noreply@legalassistant.com",
+ "FromName": "Legal Assistant",
+ "EnableSsl": true
+ },
+ "FileStorageSettings": {
+ "Provider": "Local",
+ "ConnectionString": "",
+ "ContainerName": "files",
+ "BasePath": "uploads",
+ "MaxFileSizeBytes": 10485760,
+ "AllowedExtensions": [".pdf", ".docx", ".png", ".jpg", ".jpeg"]
+ }
}