diff --git a/.gitignore b/.gitignore
index 3584a60f..b54e9b9b 100644
--- a/.gitignore
+++ b/.gitignore
@@ -83,3 +83,7 @@ infra/traefik/certs/*
*storybook.log
storybook-static
+
+# MAUI
+obj
+bin
\ No newline at end of file
diff --git a/apps/app-client-base/App.xaml b/apps/app-client-base/App.xaml
new file mode 100644
index 00000000..3666c886
--- /dev/null
+++ b/apps/app-client-base/App.xaml
@@ -0,0 +1,14 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/app-client-base/App.xaml.cs b/apps/app-client-base/App.xaml.cs
new file mode 100644
index 00000000..28d0258d
--- /dev/null
+++ b/apps/app-client-base/App.xaml.cs
@@ -0,0 +1,26 @@
+namespace AppClientBase;
+
+///
+/// EN: Main application class.
+/// VI: Lớp ứng dụng chính.
+///
+public partial class App : Application
+{
+ ///
+ /// EN: Constructor - initializes XAML components.
+ /// VI: Constructor - khởi tạo các thành phần XAML.
+ ///
+ public App()
+ {
+ InitializeComponent();
+ }
+
+ ///
+ /// EN: Creates the main window with AppShell.
+ /// VI: Tạo cửa sổ chính với AppShell.
+ ///
+ protected override Window CreateWindow(IActivationState? activationState)
+ {
+ return new Window(new AppShell());
+ }
+}
\ No newline at end of file
diff --git a/apps/app-client-base/AppClientBase.csproj b/apps/app-client-base/AppClientBase.csproj
new file mode 100644
index 00000000..488a97d8
--- /dev/null
+++ b/apps/app-client-base/AppClientBase.csproj
@@ -0,0 +1,66 @@
+
+
+
+ net10.0-android;net10.0-ios;net10.0-maccatalyst
+ $(TargetFrameworks);net10.0-windows10.0.19041.0
+
+
+
+
+ Exe
+ AppClientBase
+ true
+ true
+ enable
+ enable
+
+
+ AppClientBase
+
+
+ com.companyname.appclientbase
+
+
+ 1.0
+ 1
+
+
+ None
+
+ 15.0
+ 15.0
+ 21.0
+ 10.0.17763.0
+ 10.0.17763.0
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/app-client-base/AppShell.xaml b/apps/app-client-base/AppShell.xaml
new file mode 100644
index 00000000..d150121c
--- /dev/null
+++ b/apps/app-client-base/AppShell.xaml
@@ -0,0 +1,15 @@
+
+
+
+
+
+
diff --git a/apps/app-client-base/AppShell.xaml.cs b/apps/app-client-base/AppShell.xaml.cs
new file mode 100644
index 00000000..66acd4d4
--- /dev/null
+++ b/apps/app-client-base/AppShell.xaml.cs
@@ -0,0 +1,9 @@
+namespace AppClientBase;
+
+public partial class AppShell : Shell
+{
+ public AppShell()
+ {
+ InitializeComponent();
+ }
+}
diff --git a/apps/app-client-base/MainPage.xaml b/apps/app-client-base/MainPage.xaml
new file mode 100644
index 00000000..4ecb2106
--- /dev/null
+++ b/apps/app-client-base/MainPage.xaml
@@ -0,0 +1,38 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/app-client-base/MainPage.xaml.cs b/apps/app-client-base/MainPage.xaml.cs
new file mode 100644
index 00000000..6b4090cc
--- /dev/null
+++ b/apps/app-client-base/MainPage.xaml.cs
@@ -0,0 +1,23 @@
+namespace AppClientBase;
+
+public partial class MainPage : ContentPage
+{
+ int count = 0;
+
+ public MainPage()
+ {
+ InitializeComponent();
+ }
+
+ private void OnCounterClicked(object sender, EventArgs e)
+ {
+ count++;
+
+ if (count == 1)
+ CounterBtn.Text = $"Clicked {count} time";
+ else
+ CounterBtn.Text = $"Clicked {count} times";
+
+ SemanticScreenReader.Announce(CounterBtn.Text);
+ }
+}
diff --git a/apps/app-client-base/MauiProgram.cs b/apps/app-client-base/MauiProgram.cs
new file mode 100644
index 00000000..8551aac3
--- /dev/null
+++ b/apps/app-client-base/MauiProgram.cs
@@ -0,0 +1,31 @@
+using CommunityToolkit.Maui;
+using Microsoft.Extensions.Logging;
+
+namespace AppClientBase;
+
+///
+/// EN: Main entry point for MAUI application configuration.
+/// VI: Điểm khởi đầu chính để cấu hình ứng dụng MAUI.
+///
+public static class MauiProgram
+{
+ public static MauiApp CreateMauiApp()
+ {
+ var builder = MauiApp.CreateBuilder();
+
+ builder
+ .UseMauiApp()
+ .UseMauiCommunityToolkit()
+ .ConfigureFonts(fonts =>
+ {
+ fonts.AddFont("OpenSans-Regular.ttf", "OpenSansRegular");
+ fonts.AddFont("OpenSans-Semibold.ttf", "OpenSansSemibold");
+ });
+
+#if DEBUG
+ builder.Logging.AddDebug();
+#endif
+
+ return builder.Build();
+ }
+}
diff --git a/apps/app-client-base/Platforms/Android/AndroidManifest.xml b/apps/app-client-base/Platforms/Android/AndroidManifest.xml
new file mode 100644
index 00000000..bdec9b59
--- /dev/null
+++ b/apps/app-client-base/Platforms/Android/AndroidManifest.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/apps/app-client-base/Platforms/Android/MainActivity.cs b/apps/app-client-base/Platforms/Android/MainActivity.cs
new file mode 100644
index 00000000..f1fe9c72
--- /dev/null
+++ b/apps/app-client-base/Platforms/Android/MainActivity.cs
@@ -0,0 +1,10 @@
+using Android.App;
+using Android.Content.PM;
+using Android.OS;
+
+namespace AppClientBase;
+
+[Activity(Theme = "@style/Maui.SplashTheme", MainLauncher = true, LaunchMode = LaunchMode.SingleTop, ConfigurationChanges = ConfigChanges.ScreenSize | ConfigChanges.Orientation | ConfigChanges.UiMode | ConfigChanges.ScreenLayout | ConfigChanges.SmallestScreenSize | ConfigChanges.Density)]
+public class MainActivity : MauiAppCompatActivity
+{
+}
diff --git a/apps/app-client-base/Platforms/Android/MainApplication.cs b/apps/app-client-base/Platforms/Android/MainApplication.cs
new file mode 100644
index 00000000..0ca0b8b4
--- /dev/null
+++ b/apps/app-client-base/Platforms/Android/MainApplication.cs
@@ -0,0 +1,15 @@
+using Android.App;
+using Android.Runtime;
+
+namespace AppClientBase;
+
+[Application]
+public class MainApplication : MauiApplication
+{
+ public MainApplication(IntPtr handle, JniHandleOwnership ownership)
+ : base(handle, ownership)
+ {
+ }
+
+ protected override MauiApp CreateMauiApp() => MauiProgram.CreateMauiApp();
+}
diff --git a/apps/app-client-base/Platforms/Android/Resources/values/colors.xml b/apps/app-client-base/Platforms/Android/Resources/values/colors.xml
new file mode 100644
index 00000000..5cd16049
--- /dev/null
+++ b/apps/app-client-base/Platforms/Android/Resources/values/colors.xml
@@ -0,0 +1,6 @@
+
+
+ #512BD4
+ #2B0B98
+ #2B0B98
+
\ No newline at end of file
diff --git a/apps/app-client-base/Platforms/MacCatalyst/AppDelegate.cs b/apps/app-client-base/Platforms/MacCatalyst/AppDelegate.cs
new file mode 100644
index 00000000..46a08b54
--- /dev/null
+++ b/apps/app-client-base/Platforms/MacCatalyst/AppDelegate.cs
@@ -0,0 +1,9 @@
+using Foundation;
+
+namespace AppClientBase;
+
+[Register("AppDelegate")]
+public class AppDelegate : MauiUIApplicationDelegate
+{
+ protected override MauiApp CreateMauiApp() => MauiProgram.CreateMauiApp();
+}
diff --git a/apps/app-client-base/Platforms/MacCatalyst/Entitlements.plist b/apps/app-client-base/Platforms/MacCatalyst/Entitlements.plist
new file mode 100644
index 00000000..8e87c0cb
--- /dev/null
+++ b/apps/app-client-base/Platforms/MacCatalyst/Entitlements.plist
@@ -0,0 +1,14 @@
+
+
+
+
+
+
+ com.apple.security.app-sandbox
+
+
+ com.apple.security.network.client
+
+
+
+
diff --git a/apps/app-client-base/Platforms/MacCatalyst/Info.plist b/apps/app-client-base/Platforms/MacCatalyst/Info.plist
new file mode 100644
index 00000000..cfd1c83e
--- /dev/null
+++ b/apps/app-client-base/Platforms/MacCatalyst/Info.plist
@@ -0,0 +1,40 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+ UIDeviceFamily
+
+ 2
+
+ LSApplicationCategoryType
+ public.app-category.lifestyle
+ UIRequiredDeviceCapabilities
+
+ arm64
+
+ UISupportedInterfaceOrientations
+
+ UIInterfaceOrientationPortrait
+ UIInterfaceOrientationLandscapeLeft
+ UIInterfaceOrientationLandscapeRight
+
+ UISupportedInterfaceOrientations~ipad
+
+ UIInterfaceOrientationPortrait
+ UIInterfaceOrientationPortraitUpsideDown
+ UIInterfaceOrientationLandscapeLeft
+ UIInterfaceOrientationLandscapeRight
+
+ XSAppIconAssets
+ Assets.xcassets/appicon.appiconset
+
+
diff --git a/apps/app-client-base/Platforms/MacCatalyst/Program.cs b/apps/app-client-base/Platforms/MacCatalyst/Program.cs
new file mode 100644
index 00000000..167d2c8d
--- /dev/null
+++ b/apps/app-client-base/Platforms/MacCatalyst/Program.cs
@@ -0,0 +1,15 @@
+using ObjCRuntime;
+using UIKit;
+
+namespace AppClientBase;
+
+public class Program
+{
+ // This is the main entry point of the application.
+ static void Main(string[] args)
+ {
+ // if you want to use a different Application Delegate class from "AppDelegate"
+ // you can specify it here.
+ UIApplication.Main(args, null, typeof(AppDelegate));
+ }
+}
diff --git a/apps/app-client-base/Platforms/Windows/App.xaml b/apps/app-client-base/Platforms/Windows/App.xaml
new file mode 100644
index 00000000..0ab7c30f
--- /dev/null
+++ b/apps/app-client-base/Platforms/Windows/App.xaml
@@ -0,0 +1,8 @@
+
+
+
diff --git a/apps/app-client-base/Platforms/Windows/App.xaml.cs b/apps/app-client-base/Platforms/Windows/App.xaml.cs
new file mode 100644
index 00000000..f16fb5e3
--- /dev/null
+++ b/apps/app-client-base/Platforms/Windows/App.xaml.cs
@@ -0,0 +1,24 @@
+using Microsoft.UI.Xaml;
+
+// To learn more about WinUI, the WinUI project structure,
+// and more about our project templates, see: http://aka.ms/winui-project-info.
+
+namespace AppClientBase.WinUI;
+
+///
+/// Provides application-specific behavior to supplement the default Application class.
+///
+public partial class App : MauiWinUIApplication
+{
+ ///
+ /// Initializes the singleton application object. This is the first line of authored code
+ /// executed, and as such is the logical equivalent of main() or WinMain().
+ ///
+ public App()
+ {
+ this.InitializeComponent();
+ }
+
+ protected override MauiApp CreateMauiApp() => MauiProgram.CreateMauiApp();
+}
+
diff --git a/apps/app-client-base/Platforms/Windows/Package.appxmanifest b/apps/app-client-base/Platforms/Windows/Package.appxmanifest
new file mode 100644
index 00000000..2dfa0e69
--- /dev/null
+++ b/apps/app-client-base/Platforms/Windows/Package.appxmanifest
@@ -0,0 +1,46 @@
+
+
+
+
+
+
+
+
+ $placeholder$
+ User Name
+ $placeholder$.png
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/app-client-base/Platforms/Windows/app.manifest b/apps/app-client-base/Platforms/Windows/app.manifest
new file mode 100644
index 00000000..bc7afe96
--- /dev/null
+++ b/apps/app-client-base/Platforms/Windows/app.manifest
@@ -0,0 +1,17 @@
+
+
+
+
+
+
+
+ true/PM
+ PerMonitorV2, PerMonitor
+
+ true
+
+
+
diff --git a/apps/app-client-base/Platforms/iOS/AppDelegate.cs b/apps/app-client-base/Platforms/iOS/AppDelegate.cs
new file mode 100644
index 00000000..46a08b54
--- /dev/null
+++ b/apps/app-client-base/Platforms/iOS/AppDelegate.cs
@@ -0,0 +1,9 @@
+using Foundation;
+
+namespace AppClientBase;
+
+[Register("AppDelegate")]
+public class AppDelegate : MauiUIApplicationDelegate
+{
+ protected override MauiApp CreateMauiApp() => MauiProgram.CreateMauiApp();
+}
diff --git a/apps/app-client-base/Platforms/iOS/Info.plist b/apps/app-client-base/Platforms/iOS/Info.plist
new file mode 100644
index 00000000..358337bb
--- /dev/null
+++ b/apps/app-client-base/Platforms/iOS/Info.plist
@@ -0,0 +1,32 @@
+
+
+
+
+ LSRequiresIPhoneOS
+
+ UIDeviceFamily
+
+ 1
+ 2
+
+ UIRequiredDeviceCapabilities
+
+ arm64
+
+ UISupportedInterfaceOrientations
+
+ UIInterfaceOrientationPortrait
+ UIInterfaceOrientationLandscapeLeft
+ UIInterfaceOrientationLandscapeRight
+
+ UISupportedInterfaceOrientations~ipad
+
+ UIInterfaceOrientationPortrait
+ UIInterfaceOrientationPortraitUpsideDown
+ UIInterfaceOrientationLandscapeLeft
+ UIInterfaceOrientationLandscapeRight
+
+ XSAppIconAssets
+ Assets.xcassets/appicon.appiconset
+
+
diff --git a/apps/app-client-base/Platforms/iOS/Program.cs b/apps/app-client-base/Platforms/iOS/Program.cs
new file mode 100644
index 00000000..167d2c8d
--- /dev/null
+++ b/apps/app-client-base/Platforms/iOS/Program.cs
@@ -0,0 +1,15 @@
+using ObjCRuntime;
+using UIKit;
+
+namespace AppClientBase;
+
+public class Program
+{
+ // This is the main entry point of the application.
+ static void Main(string[] args)
+ {
+ // if you want to use a different Application Delegate class from "AppDelegate"
+ // you can specify it here.
+ UIApplication.Main(args, null, typeof(AppDelegate));
+ }
+}
diff --git a/apps/app-client-base/Platforms/iOS/Resources/PrivacyInfo.xcprivacy b/apps/app-client-base/Platforms/iOS/Resources/PrivacyInfo.xcprivacy
new file mode 100644
index 00000000..1ea3a5d7
--- /dev/null
+++ b/apps/app-client-base/Platforms/iOS/Resources/PrivacyInfo.xcprivacy
@@ -0,0 +1,51 @@
+
+
+
+
+
+ NSPrivacyAccessedAPITypes
+
+
+ NSPrivacyAccessedAPIType
+ NSPrivacyAccessedAPICategoryFileTimestamp
+ NSPrivacyAccessedAPITypeReasons
+
+ C617.1
+
+
+
+ NSPrivacyAccessedAPIType
+ NSPrivacyAccessedAPICategorySystemBootTime
+ NSPrivacyAccessedAPITypeReasons
+
+ 35F9.1
+
+
+
+ NSPrivacyAccessedAPIType
+ NSPrivacyAccessedAPICategoryDiskSpace
+ NSPrivacyAccessedAPITypeReasons
+
+ E174.1
+
+
+
+
+
+
diff --git a/apps/app-client-base/Properties/launchSettings.json b/apps/app-client-base/Properties/launchSettings.json
new file mode 100644
index 00000000..f4c6c8dd
--- /dev/null
+++ b/apps/app-client-base/Properties/launchSettings.json
@@ -0,0 +1,8 @@
+{
+ "profiles": {
+ "Windows Machine": {
+ "commandName": "Project",
+ "nativeDebugging": false
+ }
+ }
+}
\ No newline at end of file
diff --git a/apps/app-client-base/README.md b/apps/app-client-base/README.md
new file mode 100644
index 00000000..36372bab
--- /dev/null
+++ b/apps/app-client-base/README.md
@@ -0,0 +1,87 @@
+# AppClientBase - .NET MAUI Enterprise Base App
+
+Base frontend application cho iOS và Android được xây dựng với .NET MAUI theo chuẩn Enterprise.
+
+## Tech Stack
+
+- **.NET 10 MAUI** - Cross-platform framework
+- **CommunityToolkit.Mvvm 8.4.0** - MVVM implementation
+- **CommunityToolkit.Maui 9.1.0** - MAUI extensions
+
+## Project Structure
+
+```
+AppClientBase/
+├── MauiProgram.cs # DI & configuration
+├── App.xaml # Application resources
+├── AppShell.xaml # Navigation shell
+├── ViewModels/ # MVVM ViewModels
+│ ├── BaseViewModel.cs # Base với IsBusy, Title
+│ └── MainViewModel.cs # Main page ViewModel
+├── Views/ # XAML Pages
+│ └── MainPage.xaml # Main page với compiled bindings
+├── Services/ # Business services
+│ ├── INavigationService.cs # Navigation interface
+│ ├── NavigationService.cs # Shell navigation
+│ ├── ISettingsService.cs # Settings interface
+│ └── SettingsService.cs # MAUI Preferences wrapper
+├── Models/ # Data models
+├── Resources/
+│ └── Styles/
+│ ├── Colors.xaml # Brand, Semantic, Surface colors
+│ ├── Typography.xaml # Font families, sizes, text styles
+│ ├── Theme.xaml # Button, Entry, Card styles
+│ └── Styles.xaml # Template implicit styles
+└── Platforms/ # Platform-specific code
+ ├── Android/
+ └── iOS/
+```
+
+## Features
+
+### MVVM + Dependency Injection
+- Constructor injection qua DI container
+- Singleton cho stateful services (NavigationService, SettingsService)
+- Transient cho ViewModels và Views
+
+### Branding System
+- **Colors.xaml**: Light/Dark colors riêng biệt
+- **Typography.xaml**: Font scale (Caption → Display)
+- **Theme.xaml**: ButtonPrimary, ButtonSecondary, CardDefault styles
+- AppThemeBinding cho Light/Dark mode support
+
+### Compiled Bindings
+- `x:DataType` cho performance tối ưu
+- Compile-time binding validation
+
+## Getting Started
+
+```bash
+# Navigate to project
+cd apps/app-client-base
+
+# Restore packages
+dotnet restore
+
+# Build for macCatalyst
+dotnet build -c Debug -f net10.0-maccatalyst
+
+# Build for iOS (requires Xcode with iOS simulator)
+dotnet build -c Debug -f net10.0-ios
+
+# Build for Android (requires Android SDK)
+dotnet build -c Debug -f net10.0-android
+```
+
+## Requirements
+
+- .NET 10 SDK
+- MAUI workload (`dotnet workload install maui`)
+- For iOS: macOS with Xcode
+- For Android: Android SDK
+
+## Related Skills
+
+Xem các MAUI Skills để hiểu thêm về patterns được sử dụng:
+- `.agent/skills/maui-enterprise-architect/SKILL.md`
+- `.agent/skills/maui-branding-expert/SKILL.md`
diff --git a/apps/app-client-base/Resources/AppIcon/appicon.svg b/apps/app-client-base/Resources/AppIcon/appicon.svg
new file mode 100644
index 00000000..5f04fcfc
--- /dev/null
+++ b/apps/app-client-base/Resources/AppIcon/appicon.svg
@@ -0,0 +1,4 @@
+
+
\ No newline at end of file
diff --git a/apps/app-client-base/Resources/AppIcon/appiconfg.svg b/apps/app-client-base/Resources/AppIcon/appiconfg.svg
new file mode 100644
index 00000000..62d66d7a
--- /dev/null
+++ b/apps/app-client-base/Resources/AppIcon/appiconfg.svg
@@ -0,0 +1,8 @@
+
+
+
\ No newline at end of file
diff --git a/apps/app-client-base/Resources/Fonts/OpenSans-Regular.ttf b/apps/app-client-base/Resources/Fonts/OpenSans-Regular.ttf
new file mode 100644
index 00000000..6c872c6c
Binary files /dev/null and b/apps/app-client-base/Resources/Fonts/OpenSans-Regular.ttf differ
diff --git a/apps/app-client-base/Resources/Fonts/OpenSans-Semibold.ttf b/apps/app-client-base/Resources/Fonts/OpenSans-Semibold.ttf
new file mode 100644
index 00000000..d9ca35e2
Binary files /dev/null and b/apps/app-client-base/Resources/Fonts/OpenSans-Semibold.ttf differ
diff --git a/apps/app-client-base/Resources/Images/dotnet_bot.png b/apps/app-client-base/Resources/Images/dotnet_bot.png
new file mode 100644
index 00000000..054167e5
Binary files /dev/null and b/apps/app-client-base/Resources/Images/dotnet_bot.png differ
diff --git a/apps/app-client-base/Resources/Raw/AboutAssets.txt b/apps/app-client-base/Resources/Raw/AboutAssets.txt
new file mode 100644
index 00000000..f22d3bfa
--- /dev/null
+++ b/apps/app-client-base/Resources/Raw/AboutAssets.txt
@@ -0,0 +1,15 @@
+Any raw assets you want to be deployed with your application can be placed in
+this directory (and child directories). Deployment of the asset to your application
+is automatically handled by the following `MauiAsset` Build Action within your `.csproj`.
+
+
+
+These files will be deployed with your package and will be accessible using Essentials:
+
+ async Task LoadMauiAsset()
+ {
+ using var stream = await FileSystem.OpenAppPackageFileAsync("AboutAssets.txt");
+ using var reader = new StreamReader(stream);
+
+ var contents = reader.ReadToEnd();
+ }
diff --git a/apps/app-client-base/Resources/Splash/splash.svg b/apps/app-client-base/Resources/Splash/splash.svg
new file mode 100644
index 00000000..62d66d7a
--- /dev/null
+++ b/apps/app-client-base/Resources/Splash/splash.svg
@@ -0,0 +1,8 @@
+
+
+
\ No newline at end of file
diff --git a/apps/app-client-base/Resources/Styles/Colors.xaml b/apps/app-client-base/Resources/Styles/Colors.xaml
new file mode 100644
index 00000000..bebe595a
--- /dev/null
+++ b/apps/app-client-base/Resources/Styles/Colors.xaml
@@ -0,0 +1,81 @@
+
+
+
+
+
+
+ #2196F3
+ #1976D2
+ #64B5F6
+
+
+
+ #FF5722
+ #E64A19
+
+
+
+
+ #4CAF50
+ #FF9800
+ #F44336
+ #2196F3
+
+
+
+
+ #FAFAFA
+ #121212
+ #FFFFFF
+ #1E1E1E
+ #00000033
+ #FFFFFF33
+
+
+
+
+ #212121
+ #FFFFFF
+ #757575
+ #B0B0B0
+ #9E9E9E
+ #616161
+ #FFFFFF
+
+
+
+
+ #E0E0E0
+ #424242
+ #2196F3
+
+
+
+
+
+
+
+
+ #2196F3
+ #1976D2
+ #FF5722
+ White
+ Black
+ #1f1f1f
+ #E1E1E1
+ #C8C8C8
+ #ACACAC
+ #919191
+ #6E6E6E
+ #404040
+ #212121
+ #141414
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/apps/app-client-base/Resources/Styles/Styles.xaml b/apps/app-client-base/Resources/Styles/Styles.xaml
new file mode 100644
index 00000000..0dce3741
--- /dev/null
+++ b/apps/app-client-base/Resources/Styles/Styles.xaml
@@ -0,0 +1,434 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/app-client-base/Resources/Styles/Theme.xaml b/apps/app-client-base/Resources/Styles/Theme.xaml
new file mode 100644
index 00000000..7c35081d
--- /dev/null
+++ b/apps/app-client-base/Resources/Styles/Theme.xaml
@@ -0,0 +1,95 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/app-client-base/Resources/Styles/Typography.xaml b/apps/app-client-base/Resources/Styles/Typography.xaml
new file mode 100644
index 00000000..71adff25
--- /dev/null
+++ b/apps/app-client-base/Resources/Styles/Typography.xaml
@@ -0,0 +1,63 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 12
+ 16
+ 18
+ 24
+ 32
+ 48
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/app-client-base/Services/INavigationService.cs b/apps/app-client-base/Services/INavigationService.cs
new file mode 100644
index 00000000..ba2b37c0
--- /dev/null
+++ b/apps/app-client-base/Services/INavigationService.cs
@@ -0,0 +1,20 @@
+namespace AppClientBase.Services;
+
+///
+/// EN: Navigation service interface for Shell-based navigation.
+/// VI: Interface dịch vụ điều hướng cho điều hướng dựa trên Shell.
+///
+public interface INavigationService
+{
+ ///
+ /// EN: Navigate to a route with optional parameters.
+ /// VI: Điều hướng đến một route với các tham số tùy chọn.
+ ///
+ Task GoToAsync(string route, IDictionary? parameters = null);
+
+ ///
+ /// EN: Navigate back.
+ /// VI: Điều hướng quay lại.
+ ///
+ Task GoBackAsync();
+}
diff --git a/apps/app-client-base/Services/ISettingsService.cs b/apps/app-client-base/Services/ISettingsService.cs
new file mode 100644
index 00000000..15e22a02
--- /dev/null
+++ b/apps/app-client-base/Services/ISettingsService.cs
@@ -0,0 +1,38 @@
+namespace AppClientBase.Services;
+
+///
+/// EN: Settings service interface for app preferences.
+/// VI: Interface dịch vụ cài đặt cho tùy chọn ứng dụng.
+///
+public interface ISettingsService
+{
+ ///
+ /// EN: Get a setting value.
+ /// VI: Lấy giá trị cài đặt.
+ ///
+ T Get(string key, T defaultValue);
+
+ ///
+ /// EN: Set a setting value.
+ /// VI: Đặt giá trị cài đặt.
+ ///
+ void Set(string key, T value);
+
+ ///
+ /// EN: Check if a setting exists.
+ /// VI: Kiểm tra xem cài đặt có tồn tại không.
+ ///
+ bool Contains(string key);
+
+ ///
+ /// EN: Remove a setting.
+ /// VI: Xóa một cài đặt.
+ ///
+ void Remove(string key);
+
+ ///
+ /// EN: Clear all settings.
+ /// VI: Xóa tất cả cài đặt.
+ ///
+ void Clear();
+}
diff --git a/apps/app-client-base/Services/NavigationService.cs b/apps/app-client-base/Services/NavigationService.cs
new file mode 100644
index 00000000..9415d4dc
--- /dev/null
+++ b/apps/app-client-base/Services/NavigationService.cs
@@ -0,0 +1,33 @@
+namespace AppClientBase.Services;
+
+///
+/// EN: Navigation service using Shell navigation.
+/// VI: Dịch vụ điều hướng sử dụng Shell navigation.
+///
+public class NavigationService : INavigationService
+{
+ ///
+ /// EN: Navigate to a route with optional parameters.
+ /// VI: Điều hướng đến một route với các tham số tùy chọn.
+ ///
+ public async Task GoToAsync(string route, IDictionary? parameters = null)
+ {
+ if (parameters != null)
+ {
+ await Shell.Current.GoToAsync(route, parameters);
+ }
+ else
+ {
+ await Shell.Current.GoToAsync(route);
+ }
+ }
+
+ ///
+ /// EN: Navigate back.
+ /// VI: Điều hướng quay lại.
+ ///
+ public async Task GoBackAsync()
+ {
+ await Shell.Current.GoToAsync("..");
+ }
+}
diff --git a/apps/app-client-base/Services/SettingsService.cs b/apps/app-client-base/Services/SettingsService.cs
new file mode 100644
index 00000000..ea08b75f
--- /dev/null
+++ b/apps/app-client-base/Services/SettingsService.cs
@@ -0,0 +1,53 @@
+namespace AppClientBase.Services;
+
+///
+/// EN: Settings service using MAUI Preferences API.
+/// VI: Dịch vụ cài đặt sử dụng MAUI Preferences API.
+///
+public class SettingsService : ISettingsService
+{
+ ///
+ /// EN: Get a setting value.
+ /// VI: Lấy giá trị cài đặt.
+ ///
+ public T Get(string key, T defaultValue)
+ {
+ return Preferences.Default.Get(key, defaultValue);
+ }
+
+ ///
+ /// EN: Set a setting value.
+ /// VI: Đặt giá trị cài đặt.
+ ///
+ public void Set(string key, T value)
+ {
+ Preferences.Default.Set(key, value);
+ }
+
+ ///
+ /// EN: Check if a setting exists.
+ /// VI: Kiểm tra xem cài đặt có tồn tại không.
+ ///
+ public bool Contains(string key)
+ {
+ return Preferences.Default.ContainsKey(key);
+ }
+
+ ///
+ /// EN: Remove a setting.
+ /// VI: Xóa một cài đặt.
+ ///
+ public void Remove(string key)
+ {
+ Preferences.Default.Remove(key);
+ }
+
+ ///
+ /// EN: Clear all settings.
+ /// VI: Xóa tất cả cài đặt.
+ ///
+ public void Clear()
+ {
+ Preferences.Default.Clear();
+ }
+}
diff --git a/apps/app-client-base/ViewModels/BaseViewModel.cs b/apps/app-client-base/ViewModels/BaseViewModel.cs
new file mode 100644
index 00000000..291a7971
--- /dev/null
+++ b/apps/app-client-base/ViewModels/BaseViewModel.cs
@@ -0,0 +1,58 @@
+using CommunityToolkit.Mvvm.ComponentModel;
+using CommunityToolkit.Mvvm.Input;
+using AppClientBase.Services;
+
+namespace AppClientBase.ViewModels;
+
+///
+/// EN: Base ViewModel class with common functionality.
+/// VI: Lớp ViewModel cơ sở với chức năng chung.
+///
+public abstract partial class BaseViewModel : ObservableObject
+{
+ ///
+ /// EN: Navigation service instance.
+ /// VI: Instance của dịch vụ điều hướng.
+ ///
+ protected readonly INavigationService NavigationService;
+
+ ///
+ /// EN: Indicates if the ViewModel is currently loading data.
+ /// VI: Cho biết ViewModel đang tải dữ liệu hay không.
+ ///
+ [ObservableProperty]
+ [NotifyPropertyChangedFor(nameof(IsNotBusy))]
+ private bool _isBusy;
+
+ ///
+ /// EN: Title of the current page.
+ /// VI: Tiêu đề của trang hiện tại.
+ ///
+ [ObservableProperty]
+ private string _title = string.Empty;
+
+ ///
+ /// EN: Indicates if the ViewModel is not busy.
+ /// VI: Cho biết ViewModel không bận.
+ ///
+ public bool IsNotBusy => !IsBusy;
+
+ ///
+ /// EN: Constructor with navigation service.
+ /// VI: Constructor với dịch vụ điều hướng.
+ ///
+ protected BaseViewModel(INavigationService navigationService)
+ {
+ NavigationService = navigationService;
+ }
+
+ ///
+ /// EN: Navigate back to previous page.
+ /// VI: Điều hướng quay lại trang trước.
+ ///
+ [RelayCommand]
+ protected virtual async Task GoBackAsync()
+ {
+ await NavigationService.GoBackAsync();
+ }
+}
diff --git a/apps/app-client-base/ViewModels/MainViewModel.cs b/apps/app-client-base/ViewModels/MainViewModel.cs
new file mode 100644
index 00000000..5cc32c70
--- /dev/null
+++ b/apps/app-client-base/ViewModels/MainViewModel.cs
@@ -0,0 +1,84 @@
+using CommunityToolkit.Mvvm.ComponentModel;
+using CommunityToolkit.Mvvm.Input;
+using AppClientBase.Services;
+
+namespace AppClientBase.ViewModels;
+
+///
+/// EN: Main page ViewModel.
+/// VI: ViewModel của trang chính.
+///
+public partial class MainViewModel : BaseViewModel
+{
+ private readonly ISettingsService _settingsService;
+
+ ///
+ /// EN: Welcome message displayed on the main page.
+ /// VI: Thông điệp chào mừng hiển thị trên trang chính.
+ ///
+ [ObservableProperty]
+ private string _welcomeMessage = "Welcome to AppClientBase!";
+
+ ///
+ /// EN: Click counter for demonstration.
+ /// VI: Bộ đếm click để minh họa.
+ ///
+ [ObservableProperty]
+ private int _clickCount;
+
+ ///
+ /// EN: Button text that updates with click count.
+ /// VI: Text nút cập nhật theo số lần click.
+ ///
+ [ObservableProperty]
+ private string _buttonText = "Click me";
+
+ ///
+ /// EN: Constructor with required services.
+ /// VI: Constructor với các services cần thiết.
+ ///
+ public MainViewModel(
+ INavigationService navigationService,
+ ISettingsService settingsService)
+ : base(navigationService)
+ {
+ _settingsService = settingsService;
+ Title = "Home";
+
+ // Defer loading settings to avoid startup crash
+ // ClickCount will be loaded on first interaction
+ }
+
+ ///
+ /// EN: Increment click counter command.
+ /// VI: Command tăng bộ đếm click.
+ ///
+ [RelayCommand]
+ private void IncrementCounter()
+ {
+ ClickCount++;
+ try
+ {
+ _settingsService.Set("ClickCount", ClickCount);
+ }
+ catch
+ {
+ // Ignore settings errors
+ }
+ UpdateButtonText();
+ }
+
+ ///
+ /// EN: Update button text based on click count.
+ /// VI: Cập nhật text nút dựa trên số lần click.
+ ///
+ private void UpdateButtonText()
+ {
+ ButtonText = ClickCount switch
+ {
+ 0 => "Click me",
+ 1 => "Clicked 1 time",
+ _ => $"Clicked {ClickCount} times"
+ };
+ }
+}
diff --git a/services/chat-service-net/docker-compose.yml b/services/chat-service-net/docker-compose.yml
deleted file mode 100644
index 8c46bc72..00000000
--- a/services/chat-service-net/docker-compose.yml
+++ /dev/null
@@ -1,73 +0,0 @@
-version: '3.8'
-
-# EN: Docker Compose for Chat Service local development
-# VI: Docker Compose cho Chat Service phát triển local
-
-services:
- chatservice-api:
- build:
- context: .
- dockerfile: Dockerfile
- container_name: chatservice-api
- ports:
- - "5003:8080"
- environment:
- - ASPNETCORE_ENVIRONMENT=Development
- - DATABASE_URL=Host=postgres;Port=5432;Database=chatservice_db;Username=postgres;Password=postgres
- - REDIS_HOST=redis
- - REDIS_PORT=6379
- depends_on:
- postgres:
- condition: service_healthy
- redis:
- condition: service_healthy
- networks:
- - chatservice-network
- healthcheck:
- test: ["CMD", "curl", "-f", "http://localhost:8080/health/live"]
- interval: 30s
- timeout: 10s
- retries: 3
- start_period: 10s
-
- postgres:
- image: postgres:16-alpine
- container_name: chatservice-postgres
- environment:
- POSTGRES_USER: postgres
- POSTGRES_PASSWORD: postgres
- POSTGRES_DB: chatservice_db
- ports:
- - "5435:5432"
- volumes:
- - postgres_data:/var/lib/postgresql/data
- networks:
- - chatservice-network
- healthcheck:
- test: ["CMD-SHELL", "pg_isready -U postgres"]
- interval: 10s
- timeout: 5s
- retries: 5
-
- redis:
- image: redis:7-alpine
- container_name: chatservice-redis
- ports:
- - "6382:6379"
- volumes:
- - redis_data:/data
- networks:
- - chatservice-network
- healthcheck:
- test: ["CMD", "redis-cli", "ping"]
- interval: 10s
- timeout: 5s
- retries: 5
-
-volumes:
- postgres_data:
- redis_data:
-
-networks:
- chatservice-network:
- driver: bridge
diff --git a/services/chat-service-net/docs/en/ARCHITECTURE.md b/services/chat-service-net/docs/en/ARCHITECTURE.md
index 03992515..62e47eb0 100644
--- a/services/chat-service-net/docs/en/ARCHITECTURE.md
+++ b/services/chat-service-net/docs/en/ARCHITECTURE.md
@@ -1,271 +1,456 @@
-# Architecture Documentation
+# Chat Service Architecture Documentation
-> Detailed architecture documentation for the .NET 10 Microservice Template.
+> Detailed architecture for Chat Service with SignalR, scalability patterns, and AI integration.
## Architecture Overview
```mermaid
graph TB
- subgraph "API Layer"
- C[Controllers]
- CMD[Commands]
- Q[Queries]
- B[Behaviors]
- V[Validations]
+ subgraph "Clients"
+ WEB[Web App]
+ MOB[Mobile App]
+ BOT[Bot Client]
end
- subgraph "Domain Layer"
- AR[Aggregate Roots]
- E[Entities]
- VO[Value Objects]
- DE[Domain Events]
- DX[Domain Exceptions]
+ subgraph "Load Balancer"
+ LB[NGINX/Traefik]
+ SS[Sticky Sessions]
end
- subgraph "Infrastructure Layer"
- DB[(PostgreSQL)]
- R[Repositories]
- CTX[DbContext]
- ID[Idempotency]
+ subgraph "Chat Service Instances"
+ CS1[Instance 1
SignalR Hub]
+ CS2[Instance 2
SignalR Hub]
+ CS3[Instance 3
SignalR Hub]
end
- C --> CMD
- C --> Q
- CMD --> B --> V
- CMD --> AR
- Q --> R
- R --> CTX --> DB
- AR --> DE
- R --> AR
+ subgraph "Backplane"
+ RD[(Redis
Pub/Sub)]
+ end
- style C fill:#4a90d9,stroke:#2d5986,color:#fff
- style AR fill:#50c878,stroke:#2d8659,color:#fff
- style DB fill:#ff6b6b,stroke:#c0392b,color:#fff
+ subgraph "Storage"
+ PG[(PostgreSQL
Messages)]
+ RC[(Redis
Cache)]
+ end
+
+ subgraph "AI Service"
+ AI[OpenAI
GPT-4]
+ end
+
+ WEB & MOB & BOT --> LB
+ LB --> SS --> CS1 & CS2 & CS3
+ CS1 & CS2 & CS3 <--> RD
+ CS1 & CS2 & CS3 --> PG
+ CS1 & CS2 & CS3 --> RC
+ CS1 & CS2 & CS3 --> AI
+
+ style LB fill:#3498db,stroke:#2980b9,color:#fff
+ style RD fill:#e74c3c,stroke:#c0392b,color:#fff
+ style PG fill:#27ae60,stroke:#1e8449,color:#fff
+ style AI fill:#9b59b6,stroke:#7d3c98,color:#fff
```
-## Layer Responsibilities
+## SignalR Hub Architecture
-### 1. Domain Layer (ChatService.Domain)
-
-The heart of the application containing pure business logic. This layer:
-- Has **ZERO** external dependencies (except MediatR.Contracts for events)
-- Contains only POCO classes
-- Implements DDD tactical patterns
-
-#### Components
-
-| Component | Purpose |
-|-----------|---------|
-| **SeedWork** | Base classes: Entity, ValueObject, Enumeration, IAggregateRoot |
-| **AggregatesModel** | Aggregate roots with their entities and value objects |
-| **Events** | Domain events for cross-aggregate communication |
-| **Exceptions** | Domain-specific exceptions for business rule violations |
-
-### 2. Infrastructure Layer (ChatService.Infrastructure)
-
-Technical implementations and external concerns:
-- Database access (EF Core)
-- Repository implementations
-- External service integrations
-
-### 3. API Layer (ChatService.API)
-
-Application entry point and CQRS implementation:
-- Controllers for HTTP handling
-- Commands for write operations
-- Queries for read operations
-- MediatR behaviors for cross-cutting concerns
-
-## CQRS Flow
+### Connection Lifecycle
```mermaid
sequenceDiagram
participant Client
- participant Controller
- participant MediatR
- participant LoggingBehavior
- participant ValidatorBehavior
- participant TransactionBehavior
- participant CommandHandler
- participant Repository
- participant DbContext
+ participant Hub as ChatHub
+ participant Groups
+ participant DB as Database
+ participant Redis
- Client->>Controller: HTTP Request
- Controller->>MediatR: Send(Command)
- MediatR->>LoggingBehavior: Handle
- LoggingBehavior->>ValidatorBehavior: Next()
- ValidatorBehavior->>TransactionBehavior: Next()
- TransactionBehavior->>CommandHandler: Next()
- CommandHandler->>Repository: Add/Update/Delete
- Repository->>DbContext: SaveEntitiesAsync()
- DbContext-->>Repository: Success
- Repository-->>CommandHandler: Result
- CommandHandler-->>Controller: Response
- Controller-->>Client: HTTP Response
-```
-
-## Domain Events
-
-```mermaid
-graph LR
- AR[Aggregate Root] -->|Raises| DE[Domain Event]
- DE -->|Dispatched by| CTX[DbContext]
- CTX -->|Publishes to| M[MediatR]
- M -->|Handled by| H1[Handler 1]
- M -->|Handled by| H2[Handler 2]
+ Client->>Hub: Connect (JWT Token)
+ Hub->>Hub: OnConnectedAsync()
+ Hub->>DB: Load user rooms
+ Hub->>Groups: AddToGroupAsync(roomId)
+ Hub->>Redis: Publish(UserOnline)
+ Hub-->>Client: Connected
- style AR fill:#50c878,stroke:#2d8659,color:#fff
- style DE fill:#f39c12,stroke:#d68910,color:#fff
- style M fill:#9b59b6,stroke:#7d3c98,color:#fff
-```
-
-## Database Schema
-
-### Sample Aggregate
-
-```mermaid
-erDiagram
- samples {
- uuid id PK
- varchar(200) name
- varchar(1000) description
- int status_id FK
- timestamp created_at
- timestamp updated_at
- }
+ Note over Client,Hub: User is now online
- sample_statuses {
- int id PK
- varchar(50) name
- }
-
- samples ||--o{ sample_statuses : has
+ Client->>Hub: SendMessage(roomId, content)
+ Hub->>DB: Save message
+ Hub->>Redis: Publish(NewMessage)
+ Redis-->>Hub: Broadcast to all instances
+ Hub->>Groups: Clients.Group(roomId)
+ Hub-->>Client: ReceiveMessage
```
-## MediatR Pipeline
+### Hub Implementation
-```
-Request → LoggingBehavior → ValidatorBehavior → TransactionBehavior → Handler → Response
- │ │ │
- ▼ ▼ ▼
- Log start/end Validate Begin/Commit
- + timing with Transaction
- FluentValidation
-```
-
-### Behavior Order
-
-1. **LoggingBehavior** - Logs request handling with timing
-2. **ValidatorBehavior** - Validates request using FluentValidation
-3. **TransactionBehavior** - Wraps command handlers in database transactions
-
-## Error Handling
-
-### Exception Hierarchy
-
-```
-Exception
-└── DomainException
- └── SampleDomainException
-```
-
-### Problem Details (RFC 7807)
-
-All errors are returned in Problem Details format:
-
-```json
+```csharp
+public class ChatHub : Hub
{
- "type": "https://tools.ietf.org/html/rfc7807",
- "title": "Validation Error",
- "status": 400,
- "detail": "One or more validation errors occurred.",
- "errors": {
- "Name": ["Name is required"]
- }
+ private readonly IChatRoomRepository _roomRepository;
+ private readonly IMessageRepository _messageRepository;
+ private readonly IAIService _aiService;
+
+ public async Task JoinRoom(Guid roomId)
+ {
+ var userId = Context.UserIdentifier;
+
+ // Add to SignalR Group
+ await Groups.AddToGroupAsync(Context.ConnectionId, roomId.ToString());
+
+ // Notify other members
+ await Clients.Group(roomId.ToString())
+ .UserJoined(userId, roomId);
+
+ // Persist state to DB
+ await _roomRepository.AddParticipantAsync(roomId, userId);
+ }
+
+ public async Task SendMessage(Guid roomId, string content)
+ {
+ var message = new Message(roomId, Context.UserIdentifier, content);
+ await _messageRepository.AddAsync(message);
+
+ // Broadcast message
+ await Clients.Group(roomId.ToString())
+ .ReceiveMessage(message.ToDto());
+
+ // Check AI trigger
+ if (content.StartsWith("@gpt "))
+ {
+ await StreamAIResponse(roomId, content[5..]);
+ }
+ }
+
+ public async IAsyncEnumerable StreamAIResponse(
+ Guid roomId,
+ string prompt,
+ [EnumeratorCancellation] CancellationToken ct = default)
+ {
+ // Get chat history for context
+ var history = await _messageRepository.GetHistoryAsync(roomId, 20);
+
+ await foreach (var chunk in _aiService.StreamAsync(prompt, history, ct))
+ {
+ yield return chunk;
+ }
+ }
}
```
-## Health Checks
+## Scalability Architecture
+
+### Redis Backplane
```mermaid
-graph TD
- HC[Health Check Endpoint]
- HC --> |/health/live| L[Liveness]
- HC --> |/health/ready| R[Readiness]
- HC --> |/health| F[Full Status]
+graph LR
+ subgraph "Instance 1"
+ C1[Client A] --> H1[Hub 1]
+ end
- R --> PG[(PostgreSQL)]
- R --> RD[(Redis)]
+ subgraph "Instance 2"
+ C2[Client B] --> H2[Hub 2]
+ end
- style HC fill:#3498db,stroke:#2980b9,color:#fff
- style L fill:#2ecc71,stroke:#27ae60,color:#fff
- style R fill:#f39c12,stroke:#d68910,color:#fff
+ subgraph "Redis"
+ CH[Channel: chat.messages]
+ end
+
+ H1 -->|Publish| CH
+ CH -->|Subscribe| H2
+ H2 -->|Deliver| C2
+
+ style CH fill:#e74c3c,stroke:#c0392b,color:#fff
+```
+
+### Scaling Options
+
+| Option | Pros | Cons |
+|--------|------|------|
+| **Redis Backplane** | Simple, on-premise | Requires Redis cluster management |
+| **Azure SignalR** | Serverless, no sticky sessions | Vendor lock-in, cost |
+| **Sticky Sessions** | Simplest | Imperfect for failover |
+
+```csharp
+// Redis Backplane Configuration
+builder.Services.AddSignalR()
+ .AddStackExchangeRedis(options =>
+ {
+ options.Configuration.ChannelPrefix =
+ RedisChannel.Literal("ChatService");
+ options.Configuration.AbortOnConnectFail = false;
+ });
+
+// Azure SignalR Service
+builder.Services.AddSignalR()
+ .AddAzureSignalR(options =>
+ {
+ options.ConnectionString = azureSignalRConnectionString;
+ options.ServerStickyMode = ServerStickyMode.Required;
+ });
+```
+
+## User & Group Management
+
+### User Mapping
+
+```mermaid
+graph TB
+ subgraph "User Mapping"
+ U1[User ID: user-123]
+ C1[Connection 1
Phone]
+ C2[Connection 2
Laptop]
+ C3[Connection 3
Tablet]
+ end
+
+ U1 --> C1 & C2 & C3
+
+ style U1 fill:#3498db,stroke:#2980b9,color:#fff
+```
+
+### IUserIdProvider
+
+```csharp
+public class ClaimsUserIdProvider : IUserIdProvider
+{
+ public string? GetUserId(HubConnectionContext connection)
+ {
+ // Get User ID from JWT Claims
+ return connection.User?
+ .FindFirst(ClaimTypes.NameIdentifier)?.Value;
+ }
+}
+
+// Registration
+builder.Services.AddSingleton();
+```
+
+### Group State Management
+
+```csharp
+// Problem: SignalR Groups don't persist on restart
+// Solution: Store in database
+
+public class GroupStateService
+{
+ private readonly IDistributedCache _cache;
+ private readonly IChatRoomRepository _repository;
+
+ public async Task RestoreUserGroups(string userId, string connectionId)
+ {
+ var rooms = await _repository.GetUserRoomsAsync(userId);
+
+ foreach (var room in rooms)
+ {
+ await _hubContext.Groups.AddToGroupAsync(
+ connectionId,
+ room.Id.ToString()
+ );
+ }
+ }
+}
+```
+
+## AI Integration Flow
+
+```mermaid
+sequenceDiagram
+ participant User
+ participant Hub as ChatHub
+ participant AI as AIService
+ participant OpenAI
+ participant DB as Database
+
+ User->>Hub: "@gpt Explain DDD"
+ Hub->>DB: Load history (20 messages)
+ Hub->>AI: StreamAsync(prompt, history)
+ AI->>OpenAI: ChatCompletion (stream: true)
+
+ loop Streaming
+ OpenAI-->>AI: Token chunk
+ AI-->>Hub: yield chunk
+ Hub-->>User: ReceiveAIChunk
+ end
+
+ Hub->>DB: Save AI response
+ Hub-->>User: AIResponseComplete
+```
+
+## Domain Model
+
+### Aggregate Roots
+
+```mermaid
+erDiagram
+ ChatRoom ||--o{ Message : contains
+ ChatRoom ||--o{ Participant : has
+ ChatRoom {
+ uuid id PK
+ string name
+ enum type
+ uuid owner_id
+ timestamp created_at
+ }
+
+ Message {
+ uuid id PK
+ uuid room_id FK
+ uuid sender_id
+ string content
+ enum type
+ timestamp sent_at
+ }
+
+ Participant {
+ uuid id PK
+ uuid room_id FK
+ uuid user_id
+ enum role
+ timestamp joined_at
+ timestamp last_read
+ }
+```
+
+### Domain Events
+
+| Event | Trigger | Handler |
+|-------|---------|---------|
+| `MessageSentEvent` | New message | Update last_activity, broadcast |
+| `UserJoinedRoomEvent` | User joins room | Notify members |
+| `UserLeftRoomEvent` | User leaves room | Notify members |
+| `RoomCreatedEvent` | Create new room | Add creator as admin |
+
+## MessagePack Protocol
+
+### JSON vs MessagePack Comparison
+
+| Metric | JSON | MessagePack |
+|--------|------|-------------|
+| Size | 100% | ~50-70% |
+| Parse time | 1x | 0.5-0.8x |
+| Type safety | Weak | Strong |
+
+### Configuration
+
+```csharp
+// Server
+builder.Services.AddSignalR()
+ .AddMessagePackProtocol(options =>
+ {
+ options.SerializerOptions =
+ MessagePackSerializerOptions.Standard
+ .WithSecurity(MessagePackSecurity.UntrustedData)
+ .WithCompression(MessagePackCompression.Lz4BlockArray);
+ });
+```
+
+## Resiliency Patterns
+
+### Automatic Reconnect
+
+```mermaid
+stateDiagram-v2
+ [*] --> Connected
+ Connected --> Disconnected: Connection lost
+ Disconnected --> Reconnecting: Auto retry
+ Reconnecting --> Connected: Success
+ Reconnecting --> Reconnecting: Retry with backoff
+ Reconnecting --> Disconnected: Max retries exceeded
+ Disconnected --> [*]: User logout
+```
+
+### Stateful Reconnect (.NET 8+)
+
+```csharp
+// Server configuration
+builder.Services.AddSignalR(options =>
+{
+ options.StatefulReconnectBufferSize = 32 * 1024; // 32KB
+});
+
+// With UseStatefulReconnect
+app.MapHub("/chatHub", options =>
+{
+ options.AllowStatefulReconnects = true;
+});
```
## Deployment Architecture
-### Docker Compose (Local)
-
-```yaml
-services:
- myservice-api:
- build: .
- ports: ["5000:8080"]
- depends_on:
- - postgres
- - redis
-
- postgres:
- image: postgres:16-alpine
-
- redis:
- image: redis:7-alpine
-```
-
-### Kubernetes (Production)
+### Kubernetes with Sticky Sessions
```yaml
apiVersion: apps/v1
kind: Deployment
metadata:
- name: myservice-api
+ name: chatservice
spec:
replicas: 3
template:
spec:
containers:
- - name: api
- image: myservice:latest
+ - name: chatservice
+ image: chatservice:latest
ports:
- containerPort: 8080
- livenessProbe:
- httpGet:
- path: /health/live
- port: 8080
- readinessProbe:
- httpGet:
- path: /health/ready
- port: 8080
+ env:
+ - name: REDIS_URL
+ valueFrom:
+ secretKeyRef:
+ name: chat-secrets
+ key: redis-url
+---
+apiVersion: v1
+kind: Service
+metadata:
+ name: chatservice
+ annotations:
+ service.beta.kubernetes.io/aws-load-balancer-sticky-sessions: "true"
+spec:
+ type: LoadBalancer
+ sessionAffinity: ClientIP
+ sessionAffinityConfig:
+ clientIP:
+ timeoutSeconds: 3600
+ ports:
+ - port: 80
+ targetPort: 8080
```
-## Security Considerations
+### Ingress with WebSocket Support
-1. **Authentication**: JWT Bearer token (configure in production)
-2. **Authorization**: Role-based access control
-3. **Input Validation**: FluentValidation on all requests
-4. **SQL Injection**: EF Core parameterized queries
-5. **Secrets**: Environment variables, never in code
+```yaml
+apiVersion: networking.k8s.io/v1
+kind: Ingress
+metadata:
+ name: chatservice
+ annotations:
+ nginx.ingress.kubernetes.io/proxy-read-timeout: "3600"
+ nginx.ingress.kubernetes.io/proxy-send-timeout: "3600"
+ nginx.ingress.kubernetes.io/upstream-hash-by: "$request_uri"
+ nginx.ingress.kubernetes.io/affinity: "cookie"
+spec:
+ rules:
+ - host: chat.goodgo.io
+ http:
+ paths:
+ - path: /
+ pathType: Prefix
+ backend:
+ service:
+ name: chatservice
+ port:
+ number: 80
+```
-## Performance Optimization
+## Health Checks
-1. **Connection Pooling**: EF Core with Npgsql connection resilience
-2. **Async/Await**: All I/O operations are async
-3. **Response Caching**: Add caching headers for queries
-4. **Database Indexes**: Configure in EntityConfigurations
+```csharp
+builder.Services.AddHealthChecks()
+ .AddNpgSql(connectionString, name: "postgresql")
+ .AddRedis(redisConnectionString, name: "redis")
+ .AddSignalRHub(name: "signalr-hub");
+```
## References
-- [eShopOnContainers](https://github.com/dotnet-architecture/eShopOnContainers)
-- [.NET Microservices Architecture Guide](https://docs.microsoft.com/en-us/dotnet/architecture/microservices/)
-- [Domain-Driven Design](https://martinfowler.com/bliki/DomainDrivenDesign.html)
-- [CQRS Pattern](https://docs.microsoft.com/en-us/azure/architecture/patterns/cqrs)
+- [ASP.NET Core SignalR](https://docs.microsoft.com/en-us/aspnet/core/signalr/)
+- [SignalR Scale-out with Redis](https://docs.microsoft.com/en-us/aspnet/core/signalr/redis-backplane)
+- [Stateful Reconnect](https://learn.microsoft.com/en-us/aspnet/core/signalr/configuration)
+- [Azure SignalR Service](https://docs.microsoft.com/en-us/azure/azure-signalr/)
diff --git a/services/chat-service-net/docs/en/README.md b/services/chat-service-net/docs/en/README.md
index a4bc6cd1..4e7a0cf7 100644
--- a/services/chat-service-net/docs/en/README.md
+++ b/services/chat-service-net/docs/en/README.md
@@ -1,228 +1,209 @@
-# .NET 10 Microservice Template
+# Chat Service
-> Enterprise-grade .NET 10 microservice template following DDD, CQRS, and Clean Architecture patterns.
+> Real-time chat service for GoodGo platform, built on ASP.NET Core SignalR.
## Overview
-This template provides a production-ready structure for .NET microservices based on the eShopOnContainers reference architecture with:
+Chat Service provides real-time communication capabilities in the microservices system with:
-- **Domain-Driven Design (DDD)** - Aggregates, Entities, Value Objects, Domain Events
-- **CQRS Pattern** - Separate Commands (write) and Queries (read) with MediatR
-- **Clean Architecture** - Domain, Infrastructure, API layered separation
-- **EF Core 10** - PostgreSQL with connection resilience
-- **FluentValidation** - Request validation
-- **API Versioning** - URL segment versioning
-- **Health Checks** - Kubernetes-ready probes
-- **Structured Logging** - Serilog with console and Seq
+- **Real-time Communication** - ASP.NET Core SignalR with WebSockets/SSE/Long Polling
+- **Scalability** - Redis Backplane or Azure SignalR Service
+- **User & Group Management** - Chat rooms, cross-device user mapping
+- **AI Integration** - Smart chatbot with streaming response
+- **High Performance** - MessagePack protocol
+- **Resiliency** - Auto reconnect, Stateful Reconnect
-## Prerequisites
+## Requirements
| Requirement | Version |
|-------------|---------|
| .NET SDK | 10.0.101+ |
| Docker | 24.0+ |
-| PostgreSQL | 15+ (or use Docker) |
-
-```bash
-# Check .NET version
-dotnet --version
-# Should output: 10.0.xxx
-```
+| PostgreSQL | 16+ |
+| Redis | 7+ |
## Quick Start
-### 1. Create New Service
-
-```bash
-# Copy template to new service
-cp -r services/_template_dot_net services/your-service-name
-
-# Navigate to service directory
-cd services/your-service-name
-
-# Rename all occurrences of "ChatService" to "YourService"
-find . -type f -name "*.cs" -exec sed -i '' 's/ChatService/YourService/g' {} +
-find . -type f -name "*.csproj" -exec sed -i '' 's/ChatService/YourService/g' {} +
-```
-
-### 2. Configure Environment
+### 1. Configure Environment
```bash
# Copy environment template
cp .env.example .env
-# Edit with your configuration
+# Edit configuration
nano .env
```
-### 3. Run with Docker
+### 2. Run with Docker
```bash
-# Start all services (API + PostgreSQL + Redis)
+# Start all services
docker-compose up -d
# View logs
-docker-compose logs -f myservice-api
+docker-compose logs -f chatservice-api
```
-### 4. Run Locally
+### 3. Run Locally
```bash
-# Restore dependencies
dotnet restore
-
-# Build all projects
dotnet build
-
-# Run the API
dotnet run --project src/ChatService.API
```
-## Project Structure
+## Feature Details
+### A. Real-time Communication
+
+| Feature | Description |
+|---------|-------------|
+| **SignalR Hub** | Central hub handling all real-time connections |
+| **Multi-Transport** | Auto-select WebSockets → SSE → Long Polling |
+| **Streaming** | IAsyncEnumerable support for streaming AI responses |
+
+```csharp
+// Send message to group
+await Clients.Group(roomId).SendAsync("ReceiveMessage", message);
+
+// Streaming AI response
+public async IAsyncEnumerable StreamAIResponse(string prompt)
+{
+ await foreach (var chunk in _aiService.StreamAsync(prompt))
+ yield return chunk;
+}
```
-_template_dot_net/
-├── src/
-│ ├── ChatService.API/ # Presentation Layer (Controllers, CQRS)
-│ │ ├── Controllers/ # API endpoints
-│ │ ├── Application/ # CQRS Implementation
-│ │ │ ├── Commands/ # Write operations (MediatR)
-│ │ │ ├── Queries/ # Read operations
-│ │ │ ├── Behaviors/ # MediatR pipeline behaviors
-│ │ │ └── Validations/ # FluentValidation validators
-│ │ ├── Middleware/ # Custom middleware
-│ │ └── Program.cs # Application entry point
-│ │
-│ ├── ChatService.Domain/ # Domain Layer (Pure business logic)
-│ │ ├── AggregatesModel/ # Aggregate roots and entities
-│ │ ├── Events/ # Domain events
-│ │ ├── Exceptions/ # Domain exceptions
-│ │ └── SeedWork/ # Base classes (Entity, ValueObject, etc.)
-│ │
-│ └── ChatService.Infrastructure/ # Infrastructure Layer (Data access)
-│ ├── EntityConfigurations/ # EF Core Fluent API configurations
-│ ├── Repositories/ # Repository implementations
-│ ├── Idempotency/ # Request idempotency handling
-│ └── ChatServiceContext.cs # DbContext with Unit of Work
-│
-├── tests/
-│ ├── ChatService.UnitTests/ # Unit tests (Domain, Application)
-│ └── ChatService.FunctionalTests/ # Integration tests (API endpoints)
-│
-├── Dockerfile # Multi-stage Docker build
-├── docker-compose.yml # Local development setup
-├── global.json # .NET SDK version pinning
-└── Directory.Build.props # Common MSBuild properties
+
+### B. Scalability
+
+| Solution | Use Case |
+|----------|----------|
+| **Redis Backplane** | On-premise, multi-instance deployment |
+| **Azure SignalR Service** | Azure cloud, serverless scenarios |
+| **Sticky Sessions** | Fallback when not using Azure SignalR |
+
+```csharp
+// Configure Redis Backplane
+builder.Services.AddSignalR()
+ .AddStackExchangeRedis(connectionString, options => {
+ options.Configuration.ChannelPrefix = RedisChannel.Literal("ChatService");
+ });
+```
+
+### C. User & Group Management
+
+| Feature | Description |
+|---------|-------------|
+| **Groups** | Group connections by chat room |
+| **User Mapping** | Map ConnectionId → UserId via Claims |
+| **Persistent State** | Save group state to database |
+
+```csharp
+// Custom User ID Provider
+public class CustomUserIdProvider : IUserIdProvider
+{
+ public string? GetUserId(HubConnectionContext connection)
+ => connection.User?.FindFirst(ClaimTypes.NameIdentifier)?.Value;
+}
+```
+
+### D. AI Integration
+
+| Feature | Description |
+|---------|-------------|
+| **AI Assistant** | In-group chatbot (trigger: @gpt) |
+| **Streaming Response** | Push response chunks in real-time |
+| **Context History** | Save chat history for AI context |
+
+### E. MessagePack Protocol
+
+```csharp
+// Server configuration
+builder.Services.AddSignalR()
+ .AddMessagePackProtocol();
+
+// Client configuration (JavaScript)
+const connection = new signalR.HubConnectionBuilder()
+ .withUrl("/chatHub")
+ .withHubProtocol(new signalR.protocols.msgpack.MessagePackHubProtocol())
+ .build();
+```
+
+### F. Resiliency
+
+| Feature | Description |
+|---------|-------------|
+| **Auto Reconnect** | Client auto-reconnects |
+| **Stateful Reconnect** | Buffer data during interruption (.NET 8+) |
+
+```csharp
+// Server: Enable Stateful Reconnect
+builder.Services.AddSignalR(options => {
+ options.EnableDetailedErrors = true;
+ options.StatefulReconnectBufferSize = 32 * 1024; // 32KB buffer
+});
+
+// Client: Configure reconnect
+connection.WithAutomaticReconnect([0, 2000, 10000, 30000]);
```
## API Endpoints
+### HTTP REST APIs
+
| Method | Endpoint | Description |
|--------|----------|-------------|
-| `GET` | `/api/v1/samples` | Get all samples |
-| `GET` | `/api/v1/samples/{id}` | Get sample by ID |
-| `POST` | `/api/v1/samples` | Create new sample |
-| `PUT` | `/api/v1/samples/{id}` | Update sample |
-| `DELETE` | `/api/v1/samples/{id}` | Delete sample |
-| `PATCH` | `/api/v1/samples/{id}/status` | Change status |
+| `GET` | `/api/v1/rooms` | Get chat room list |
+| `POST` | `/api/v1/rooms` | Create new chat room |
+| `GET` | `/api/v1/rooms/{id}/messages` | Get message history |
+| `POST` | `/api/v1/rooms/{id}/participants` | Add participant |
+
+### SignalR Hub Methods
+
+| Method | Direction | Description |
+|--------|-----------|-------------|
+| `SendMessage` | Client → Server | Send message |
+| `JoinRoom` | Client → Server | Join room |
+| `LeaveRoom` | Client → Server | Leave room |
+| `ReceiveMessage` | Server → Client | Receive message |
+| `UserJoined` | Server → Client | New user notification |
### Health Endpoints
| Endpoint | Purpose |
|----------|---------|
-| `/health` | Full health status |
+| `/health` | Full status |
| `/health/live` | Liveness probe |
| `/health/ready` | Readiness probe |
-## CQRS Pattern
-
-### Commands (Write Operations)
-
-```csharp
-// Define command
-public record CreateSampleCommand(string Name, string? Description)
- : IRequest;
-
-// Handle command
-public class CreateSampleCommandHandler : IRequestHandler
-{
- public async Task Handle(CreateSampleCommand request, CancellationToken ct)
- {
- var sample = new Sample(request.Name, request.Description);
- _repository.Add(sample);
- await _repository.UnitOfWork.SaveEntitiesAsync(ct);
- return new CreateSampleCommandResult(sample.Id);
- }
-}
-```
-
-### Queries (Read Operations)
-
-```csharp
-// Define query
-public record GetSampleQuery(Guid SampleId) : IRequest;
-```
-
-## Domain Model
-
-### Aggregate Root
-
-```csharp
-public class Sample : Entity, IAggregateRoot
-{
- public string Name => _name;
- public SampleStatus Status => _status;
-
- public Sample(string name, string? description) {
- // Business logic validation
- if (string.IsNullOrWhiteSpace(name))
- throw new SampleDomainException("Sample name cannot be empty");
-
- // Domain event
- AddDomainEvent(new SampleCreatedDomainEvent(this));
- }
-
- public void Activate() {
- if (_status != SampleStatus.Draft)
- throw new SampleDomainException("Only draft samples can be activated");
- // State transition
- }
-}
-```
-
-## Testing
-
-```bash
-# Run all tests
-dotnet test
-
-# Run with coverage
-dotnet test /p:CollectCoverage=true /p:CoverageReportFormat=cobertura
-
-# Run specific test project
-dotnet test tests/ChatService.UnitTests
-```
-
## Configuration
### Environment Variables
| Variable | Description | Default |
|----------|-------------|---------|
-| `ASPNETCORE_ENVIRONMENT` | Environment name | `Development` |
-| `DATABASE_URL` | PostgreSQL connection string | - |
-| `REDIS_URL` | Redis connection string | - |
-| `JWT_SECRET` | JWT signing secret (min 32 chars) | - |
+| `ASPNETCORE_ENVIRONMENT` | Environment | `Development` |
+| `DATABASE_URL` | PostgreSQL connection | - |
+| `REDIS_URL` | Redis connection | - |
+| `AZURE_SIGNALR_CONNECTION` | Azure SignalR (optional) | - |
+| `OPENAI_API_KEY` | OpenAI API key (optional) | - |
### appsettings.json
```json
{
"ConnectionStrings": {
- "DefaultConnection": "Host=localhost;Database=myservice;Username=postgres;Password=postgres"
+ "DefaultConnection": "Host=localhost;Database=chatservice;Username=postgres;Password=postgres",
+ "Redis": "localhost:6379"
},
- "Serilog": {
- "MinimumLevel": "Information"
+ "SignalR": {
+ "EnableMessagePack": true,
+ "StatefulReconnectBufferSize": 32768
+ },
+ "AI": {
+ "Provider": "OpenAI",
+ "Model": "gpt-4",
+ "MaxHistoryMessages": 20
}
}
```
@@ -232,33 +213,20 @@ dotnet test tests/ChatService.UnitTests
### Docker Build
```bash
-# Build Docker image
-docker build -t myservice:latest .
-
-# Run container
-docker run -p 5000:8080 --env-file .env myservice:latest
+docker build -t chatservice:latest .
+docker run -p 5000:8080 --env-file .env chatservice:latest
```
### Kubernetes
-See [ARCHITECTURE.md](./ARCHITECTURE.md) for Kubernetes deployment manifests.
+See [ARCHITECTURE.md](./ARCHITECTURE.md) for detailed Kubernetes configuration with Sticky Sessions.
-## What's New in .NET 10
+## References
-- **C# 14** language features
-- Improved **Native AOT** support
-- Better **async/await** performance
-- Enhanced **JSON serialization**
-- Performance improvements across the board
-- 3-year **LTS** support (until November 2028)
-
-## Resources
-
-- [eShopOnContainers](https://github.com/dotnet-architecture/eShopOnContainers) - Reference architecture
-- [.NET 10 Documentation](https://docs.microsoft.com/en-us/dotnet/core/whats-new/dotnet-10)
-- [DDD with .NET](https://docs.microsoft.com/en-us/dotnet/architecture/microservices/microservice-ddd-cqrs-patterns/)
-- [MediatR](https://github.com/jbogard/MediatR) - CQRS library
-- [FluentValidation](https://docs.fluentvalidation.net/) - Validation library
+- [ASP.NET Core SignalR](https://docs.microsoft.com/en-us/aspnet/core/signalr/)
+- [Redis Backplane](https://docs.microsoft.com/en-us/aspnet/core/signalr/redis-backplane)
+- [Azure SignalR Service](https://docs.microsoft.com/en-us/azure/azure-signalr/)
+- [MessagePack Protocol](https://docs.microsoft.com/en-us/aspnet/core/signalr/messagepackhubprotocol)
## License
diff --git a/services/chat-service-net/docs/vi/ARCHITECTURE.md b/services/chat-service-net/docs/vi/ARCHITECTURE.md
index 69b9dabe..85a15c8d 100644
--- a/services/chat-service-net/docs/vi/ARCHITECTURE.md
+++ b/services/chat-service-net/docs/vi/ARCHITECTURE.md
@@ -1,271 +1,456 @@
-# Tài Liệu Kiến Trúc
+# Tài Liệu Kiến Trúc Chat Service
-> Tài liệu kiến trúc chi tiết cho Template Microservice .NET 10.
+> Kiến trúc chi tiết cho Chat Service với SignalR, scalability patterns, và AI integration.
## Tổng Quan Kiến Trúc
```mermaid
graph TB
- subgraph "Lớp API"
- C[Controllers]
- CMD[Commands]
- Q[Queries]
- B[Behaviors]
- V[Validations]
+ subgraph "Clients"
+ WEB[Web App]
+ MOB[Mobile App]
+ BOT[Bot Client]
end
- subgraph "Lớp Domain"
- AR[Aggregate Roots]
- E[Entities]
- VO[Value Objects]
- DE[Domain Events]
- DX[Domain Exceptions]
+ subgraph "Load Balancer"
+ LB[NGINX/Traefik]
+ SS[Sticky Sessions]
end
- subgraph "Lớp Infrastructure"
- DB[(PostgreSQL)]
- R[Repositories]
- CTX[DbContext]
- ID[Idempotency]
+ subgraph "Chat Service Instances"
+ CS1[Instance 1
SignalR Hub]
+ CS2[Instance 2
SignalR Hub]
+ CS3[Instance 3
SignalR Hub]
end
- C --> CMD
- C --> Q
- CMD --> B --> V
- CMD --> AR
- Q --> R
- R --> CTX --> DB
- AR --> DE
- R --> AR
+ subgraph "Backplane"
+ RD[(Redis
Pub/Sub)]
+ end
- style C fill:#4a90d9,stroke:#2d5986,color:#fff
- style AR fill:#50c878,stroke:#2d8659,color:#fff
- style DB fill:#ff6b6b,stroke:#c0392b,color:#fff
+ subgraph "Storage"
+ PG[(PostgreSQL
Messages)]
+ RC[(Redis
Cache)]
+ end
+
+ subgraph "AI Service"
+ AI[OpenAI
GPT-4]
+ end
+
+ WEB & MOB & BOT --> LB
+ LB --> SS --> CS1 & CS2 & CS3
+ CS1 & CS2 & CS3 <--> RD
+ CS1 & CS2 & CS3 --> PG
+ CS1 & CS2 & CS3 --> RC
+ CS1 & CS2 & CS3 --> AI
+
+ style LB fill:#3498db,stroke:#2980b9,color:#fff
+ style RD fill:#e74c3c,stroke:#c0392b,color:#fff
+ style PG fill:#27ae60,stroke:#1e8449,color:#fff
+ style AI fill:#9b59b6,stroke:#7d3c98,color:#fff
```
-## Trách Nhiệm Các Lớp
+## SignalR Hub Architecture
-### 1. Lớp Domain (ChatService.Domain)
-
-Trái tim của ứng dụng chứa business logic thuần túy. Lớp này:
-- Có **ZERO** phụ thuộc bên ngoài (ngoại trừ MediatR.Contracts cho events)
-- Chỉ chứa các class POCO
-- Triển khai các tactical patterns của DDD
-
-#### Thành Phần
-
-| Thành phần | Mục Đích |
-|------------|----------|
-| **SeedWork** | Base classes: Entity, ValueObject, Enumeration, IAggregateRoot |
-| **AggregatesModel** | Aggregate roots với entities và value objects |
-| **Events** | Domain events cho giao tiếp cross-aggregate |
-| **Exceptions** | Domain exceptions cho vi phạm business rules |
-
-### 2. Lớp Infrastructure (ChatService.Infrastructure)
-
-Triển khai kỹ thuật và các mối quan tâm bên ngoài:
-- Truy cập database (EF Core)
-- Triển khai repositories
-- Tích hợp external services
-
-### 3. Lớp API (ChatService.API)
-
-Điểm vào ứng dụng và triển khai CQRS:
-- Controllers để xử lý HTTP
-- Commands cho các thao tác ghi
-- Queries cho các thao tác đọc
-- MediatR behaviors cho cross-cutting concerns
-
-## Luồng CQRS
+### Connection Lifecycle
```mermaid
sequenceDiagram
participant Client
- participant Controller
- participant MediatR
- participant LoggingBehavior
- participant ValidatorBehavior
- participant TransactionBehavior
- participant CommandHandler
- participant Repository
- participant DbContext
+ participant Hub as ChatHub
+ participant Groups
+ participant DB as Database
+ participant Redis
- Client->>Controller: HTTP Request
- Controller->>MediatR: Send(Command)
- MediatR->>LoggingBehavior: Handle
- LoggingBehavior->>ValidatorBehavior: Next()
- ValidatorBehavior->>TransactionBehavior: Next()
- TransactionBehavior->>CommandHandler: Next()
- CommandHandler->>Repository: Add/Update/Delete
- Repository->>DbContext: SaveEntitiesAsync()
- DbContext-->>Repository: Success
- Repository-->>CommandHandler: Result
- CommandHandler-->>Controller: Response
- Controller-->>Client: HTTP Response
-```
-
-## Domain Events
-
-```mermaid
-graph LR
- AR[Aggregate Root] -->|Phát sinh| DE[Domain Event]
- DE -->|Dispatch bởi| CTX[DbContext]
- CTX -->|Publish tới| M[MediatR]
- M -->|Xử lý bởi| H1[Handler 1]
- M -->|Xử lý bởi| H2[Handler 2]
+ Client->>Hub: Connect (JWT Token)
+ Hub->>Hub: OnConnectedAsync()
+ Hub->>DB: Load user rooms
+ Hub->>Groups: AddToGroupAsync(roomId)
+ Hub->>Redis: Publish(UserOnline)
+ Hub-->>Client: Connected
- style AR fill:#50c878,stroke:#2d8659,color:#fff
- style DE fill:#f39c12,stroke:#d68910,color:#fff
- style M fill:#9b59b6,stroke:#7d3c98,color:#fff
-```
-
-## Schema Database
-
-### Sample Aggregate
-
-```mermaid
-erDiagram
- samples {
- uuid id PK
- varchar(200) name
- varchar(1000) description
- int status_id FK
- timestamp created_at
- timestamp updated_at
- }
+ Note over Client,Hub: User is now online
- sample_statuses {
- int id PK
- varchar(50) name
- }
-
- samples ||--o{ sample_statuses : has
+ Client->>Hub: SendMessage(roomId, content)
+ Hub->>DB: Save message
+ Hub->>Redis: Publish(NewMessage)
+ Redis-->>Hub: Broadcast to all instances
+ Hub->>Groups: Clients.Group(roomId)
+ Hub-->>Client: ReceiveMessage
```
-## Pipeline MediatR
+### Hub Implementation
-```
-Request → LoggingBehavior → ValidatorBehavior → TransactionBehavior → Handler → Response
- │ │ │
- ▼ ▼ ▼
- Log start/end Validate Begin/Commit
- + timing với Transaction
- FluentValidation
-```
-
-### Thứ Tự Behaviors
-
-1. **LoggingBehavior** - Ghi log xử lý request với timing
-2. **ValidatorBehavior** - Validate request sử dụng FluentValidation
-3. **TransactionBehavior** - Bao bọc command handlers trong database transactions
-
-## Xử Lý Lỗi
-
-### Phân Cấp Exceptions
-
-```
-Exception
-└── DomainException
- └── SampleDomainException
-```
-
-### Problem Details (RFC 7807)
-
-Tất cả lỗi được trả về theo định dạng Problem Details:
-
-```json
+```csharp
+public class ChatHub : Hub
{
- "type": "https://tools.ietf.org/html/rfc7807",
- "title": "Lỗi Validation",
- "status": 400,
- "detail": "Một hoặc nhiều lỗi validation đã xảy ra.",
- "errors": {
- "Name": ["Tên là bắt buộc"]
- }
+ private readonly IChatRoomRepository _roomRepository;
+ private readonly IMessageRepository _messageRepository;
+ private readonly IAIService _aiService;
+
+ public async Task JoinRoom(Guid roomId)
+ {
+ var userId = Context.UserIdentifier;
+
+ // Thêm vào SignalR Group
+ await Groups.AddToGroupAsync(Context.ConnectionId, roomId.ToString());
+
+ // Thông báo cho members khác
+ await Clients.Group(roomId.ToString())
+ .UserJoined(userId, roomId);
+
+ // Lưu trạng thái vào DB
+ await _roomRepository.AddParticipantAsync(roomId, userId);
+ }
+
+ public async Task SendMessage(Guid roomId, string content)
+ {
+ var message = new Message(roomId, Context.UserIdentifier, content);
+ await _messageRepository.AddAsync(message);
+
+ // Broadcast tin nhắn
+ await Clients.Group(roomId.ToString())
+ .ReceiveMessage(message.ToDto());
+
+ // Kiểm tra AI trigger
+ if (content.StartsWith("@gpt "))
+ {
+ await StreamAIResponse(roomId, content[5..]);
+ }
+ }
+
+ public async IAsyncEnumerable StreamAIResponse(
+ Guid roomId,
+ string prompt,
+ [EnumeratorCancellation] CancellationToken ct = default)
+ {
+ // Lấy lịch sử chat cho context
+ var history = await _messageRepository.GetHistoryAsync(roomId, 20);
+
+ await foreach (var chunk in _aiService.StreamAsync(prompt, history, ct))
+ {
+ yield return chunk;
+ }
+ }
}
```
-## Health Checks
+## Scalability Architecture
+
+### Redis Backplane
```mermaid
-graph TD
- HC[Health Check Endpoint]
- HC --> |/health/live| L[Liveness]
- HC --> |/health/ready| R[Readiness]
- HC --> |/health| F[Full Status]
+graph LR
+ subgraph "Instance 1"
+ C1[Client A] --> H1[Hub 1]
+ end
- R --> PG[(PostgreSQL)]
- R --> RD[(Redis)]
+ subgraph "Instance 2"
+ C2[Client B] --> H2[Hub 2]
+ end
- style HC fill:#3498db,stroke:#2980b9,color:#fff
- style L fill:#2ecc71,stroke:#27ae60,color:#fff
- style R fill:#f39c12,stroke:#d68910,color:#fff
+ subgraph "Redis"
+ CH[Channel: chat.messages]
+ end
+
+ H1 -->|Publish| CH
+ CH -->|Subscribe| H2
+ H2 -->|Deliver| C2
+
+ style CH fill:#e74c3c,stroke:#c0392b,color:#fff
```
-## Kiến Trúc Deployment
+### Cấu Hình Scaling
-### Docker Compose (Local)
+| Option | Ưu điểm | Nhược điểm |
+|--------|---------|------------|
+| **Redis Backplane** | Đơn giản, on-premise | Cần quản lý Redis cluster |
+| **Azure SignalR** | Serverless, không sticky sessions | Vendor lock-in, chi phí |
+| **Sticky Sessions** | Đơn giản nhất | Không hoàn hảo cho failover |
-```yaml
-services:
- myservice-api:
- build: .
- ports: ["5000:8080"]
- depends_on:
- - postgres
- - redis
-
- postgres:
- image: postgres:16-alpine
-
- redis:
- image: redis:7-alpine
+```csharp
+// Redis Backplane Configuration
+builder.Services.AddSignalR()
+ .AddStackExchangeRedis(options =>
+ {
+ options.Configuration.ChannelPrefix =
+ RedisChannel.Literal("ChatService");
+ options.Configuration.AbortOnConnectFail = false;
+ });
+
+// Azure SignalR Service
+builder.Services.AddSignalR()
+ .AddAzureSignalR(options =>
+ {
+ options.ConnectionString = azureSignalRConnectionString;
+ options.ServerStickyMode = ServerStickyMode.Required;
+ });
```
-### Kubernetes (Production)
+## User & Group Management
+
+### User Mapping
+
+```mermaid
+graph TB
+ subgraph "User Mapping"
+ U1[User ID: user-123]
+ C1[Connection 1
Phone]
+ C2[Connection 2
Laptop]
+ C3[Connection 3
Tablet]
+ end
+
+ U1 --> C1 & C2 & C3
+
+ style U1 fill:#3498db,stroke:#2980b9,color:#fff
+```
+
+### IUserIdProvider
+
+```csharp
+public class ClaimsUserIdProvider : IUserIdProvider
+{
+ public string? GetUserId(HubConnectionContext connection)
+ {
+ // Lấy User ID từ JWT Claims
+ return connection.User?
+ .FindFirst(ClaimTypes.NameIdentifier)?.Value;
+ }
+}
+
+// Registration
+builder.Services.AddSingleton();
+```
+
+### Group State Management
+
+```csharp
+// Vấn đề: SignalR Groups không persist khi restart
+// Giải pháp: Lưu trữ trong database
+
+public class GroupStateService
+{
+ private readonly IDistributedCache _cache;
+ private readonly IChatRoomRepository _repository;
+
+ public async Task RestoreUserGroups(string userId, string connectionId)
+ {
+ var rooms = await _repository.GetUserRoomsAsync(userId);
+
+ foreach (var room in rooms)
+ {
+ await _hubContext.Groups.AddToGroupAsync(
+ connectionId,
+ room.Id.ToString()
+ );
+ }
+ }
+}
+```
+
+## AI Integration Flow
+
+```mermaid
+sequenceDiagram
+ participant User
+ participant Hub as ChatHub
+ participant AI as AIService
+ participant OpenAI
+ participant DB as Database
+
+ User->>Hub: "@gpt Giải thích DDD"
+ Hub->>DB: Load history (20 messages)
+ Hub->>AI: StreamAsync(prompt, history)
+ AI->>OpenAI: ChatCompletion (stream: true)
+
+ loop Streaming
+ OpenAI-->>AI: Token chunk
+ AI-->>Hub: yield chunk
+ Hub-->>User: ReceiveAIChunk
+ end
+
+ Hub->>DB: Save AI response
+ Hub-->>User: AIResponseComplete
+```
+
+## Domain Model
+
+### Aggregate Roots
+
+```mermaid
+erDiagram
+ ChatRoom ||--o{ Message : contains
+ ChatRoom ||--o{ Participant : has
+ ChatRoom {
+ uuid id PK
+ string name
+ enum type
+ uuid owner_id
+ timestamp created_at
+ }
+
+ Message {
+ uuid id PK
+ uuid room_id FK
+ uuid sender_id
+ string content
+ enum type
+ timestamp sent_at
+ }
+
+ Participant {
+ uuid id PK
+ uuid room_id FK
+ uuid user_id
+ enum role
+ timestamp joined_at
+ timestamp last_read
+ }
+```
+
+### Domain Events
+
+| Event | Trigger | Handler |
+|-------|---------|---------|
+| `MessageSentEvent` | Tin nhắn mới | Update last_activity, broadcast |
+| `UserJoinedRoomEvent` | User join room | Notify members |
+| `UserLeftRoomEvent` | User leave room | Notify members |
+| `RoomCreatedEvent` | Tạo room mới | Add creator as admin |
+
+## MessagePack Protocol
+
+### So sánh JSON vs MessagePack
+
+| Metric | JSON | MessagePack |
+|--------|------|-------------|
+| Size | 100% | ~50-70% |
+| Parse time | 1x | 0.5-0.8x |
+| Type safety | Weak | Strong |
+
+### Cấu Hình
+
+```csharp
+// Server
+builder.Services.AddSignalR()
+ .AddMessagePackProtocol(options =>
+ {
+ options.SerializerOptions =
+ MessagePackSerializerOptions.Standard
+ .WithSecurity(MessagePackSecurity.UntrustedData)
+ .WithCompression(MessagePackCompression.Lz4BlockArray);
+ });
+```
+
+## Resiliency Patterns
+
+### Automatic Reconnect
+
+```mermaid
+stateDiagram-v2
+ [*] --> Connected
+ Connected --> Disconnected: Connection lost
+ Disconnected --> Reconnecting: Auto retry
+ Reconnecting --> Connected: Success
+ Reconnecting --> Reconnecting: Retry with backoff
+ Reconnecting --> Disconnected: Max retries exceeded
+ Disconnected --> [*]: User logout
+```
+
+### Stateful Reconnect (.NET 8+)
+
+```csharp
+// Server configuration
+builder.Services.AddSignalR(options =>
+{
+ options.StatefulReconnectBufferSize = 32 * 1024; // 32KB
+});
+
+// Với UseStatefulReconnect
+app.MapHub("/chatHub", options =>
+{
+ options.AllowStatefulReconnects = true;
+});
+```
+
+## Deployment Architecture
+
+### Kubernetes với Sticky Sessions
```yaml
apiVersion: apps/v1
kind: Deployment
metadata:
- name: myservice-api
+ name: chatservice
spec:
replicas: 3
template:
spec:
containers:
- - name: api
- image: myservice:latest
+ - name: chatservice
+ image: chatservice:latest
ports:
- containerPort: 8080
- livenessProbe:
- httpGet:
- path: /health/live
- port: 8080
- readinessProbe:
- httpGet:
- path: /health/ready
- port: 8080
+ env:
+ - name: REDIS_URL
+ valueFrom:
+ secretKeyRef:
+ name: chat-secrets
+ key: redis-url
+---
+apiVersion: v1
+kind: Service
+metadata:
+ name: chatservice
+ annotations:
+ service.beta.kubernetes.io/aws-load-balancer-sticky-sessions: "true"
+spec:
+ type: LoadBalancer
+ sessionAffinity: ClientIP
+ sessionAffinityConfig:
+ clientIP:
+ timeoutSeconds: 3600
+ ports:
+ - port: 80
+ targetPort: 8080
```
-## Cân Nhắc Bảo Mật
+### Ingress với WebSocket Support
-1. **Authentication**: JWT Bearer token (cấu hình trong production)
-2. **Authorization**: Role-based access control
-3. **Input Validation**: FluentValidation trên tất cả requests
-4. **SQL Injection**: EF Core parameterized queries
-5. **Secrets**: Biến môi trường, không bao giờ trong code
+```yaml
+apiVersion: networking.k8s.io/v1
+kind: Ingress
+metadata:
+ name: chatservice
+ annotations:
+ nginx.ingress.kubernetes.io/proxy-read-timeout: "3600"
+ nginx.ingress.kubernetes.io/proxy-send-timeout: "3600"
+ nginx.ingress.kubernetes.io/upstream-hash-by: "$request_uri"
+ nginx.ingress.kubernetes.io/affinity: "cookie"
+spec:
+ rules:
+ - host: chat.goodgo.io
+ http:
+ paths:
+ - path: /
+ pathType: Prefix
+ backend:
+ service:
+ name: chatservice
+ port:
+ number: 80
+```
-## Tối Ưu Hiệu Năng
+## Health Checks
-1. **Connection Pooling**: EF Core với Npgsql connection resilience
-2. **Async/Await**: Tất cả I/O operations đều async
-3. **Response Caching**: Thêm caching headers cho queries
-4. **Database Indexes**: Cấu hình trong EntityConfigurations
+```csharp
+builder.Services.AddHealthChecks()
+ .AddNpgSql(connectionString, name: "postgresql")
+ .AddRedis(redisConnectionString, name: "redis")
+ .AddSignalRHub(name: "signalr-hub");
+```
## Tài Liệu Tham Khảo
-- [eShopOnContainers](https://github.com/dotnet-architecture/eShopOnContainers)
-- [Hướng dẫn Kiến trúc .NET Microservices](https://docs.microsoft.com/en-us/dotnet/architecture/microservices/)
-- [Domain-Driven Design](https://martinfowler.com/bliki/DomainDrivenDesign.html)
-- [CQRS Pattern](https://docs.microsoft.com/en-us/azure/architecture/patterns/cqrs)
+- [ASP.NET Core SignalR](https://docs.microsoft.com/en-us/aspnet/core/signalr/)
+- [SignalR Scale-out with Redis](https://docs.microsoft.com/en-us/aspnet/core/signalr/redis-backplane)
+- [Stateful Reconnect](https://learn.microsoft.com/en-us/aspnet/core/signalr/configuration)
+- [Azure SignalR Service](https://docs.microsoft.com/en-us/azure/azure-signalr/)
diff --git a/services/chat-service-net/docs/vi/README.md b/services/chat-service-net/docs/vi/README.md
index 3cd0a304..2bd66cc3 100644
--- a/services/chat-service-net/docs/vi/README.md
+++ b/services/chat-service-net/docs/vi/README.md
@@ -1,19 +1,17 @@
-# Template Microservice .NET 10
+# Chat Service
-> Template microservice .NET 10 cấp doanh nghiệp theo các pattern DDD, CQRS và Clean Architecture.
+> Dịch vụ Chat thời gian thực cho nền tảng GoodGo, xây dựng trên ASP.NET Core SignalR.
## Tổng Quan
-Template này cung cấp cấu trúc sẵn sàng production cho microservices .NET dựa trên kiến trúc tham chiếu eShopOnContainers với:
+Chat Service cung cấp khả năng giao tiếp thời gian thực trong hệ thống microservices với:
-- **Domain-Driven Design (DDD)** - Aggregates, Entities, Value Objects, Domain Events
-- **CQRS Pattern** - Tách biệt Commands (ghi) và Queries (đọc) với MediatR
-- **Clean Architecture** - Phân tầng Domain, Infrastructure, API
-- **EF Core 10** - PostgreSQL với connection resilience
-- **FluentValidation** - Validation request
-- **API Versioning** - Versioning theo URL segment
-- **Health Checks** - Probes sẵn sàng cho Kubernetes
-- **Structured Logging** - Serilog với console và Seq
+- **Giao tiếp thời gian thực** - ASP.NET Core SignalR với WebSockets/SSE/Long Polling
+- **Khả năng mở rộng** - Redis Backplane hoặc Azure SignalR Service
+- **Quản lý người dùng & nhóm** - Phòng chat, ánh xạ user across devices
+- **Tích hợp AI** - Chatbot thông minh với streaming response
+- **Hiệu năng cao** - Giao thức MessagePack
+- **Khả năng phục hồi** - Auto reconnect, Stateful Reconnect
## Yêu Cầu
@@ -21,208 +19,191 @@ Template này cung cấp cấu trúc sẵn sàng production cho microservices .N
|---------|-----------|
| .NET SDK | 10.0.101+ |
| Docker | 24.0+ |
-| PostgreSQL | 15+ (hoặc dùng Docker) |
-
-```bash
-# Kiểm tra phiên bản .NET
-dotnet --version
-# Kết quả nên là: 10.0.xxx
-```
+| PostgreSQL | 16+ |
+| Redis | 7+ |
## Bắt Đầu Nhanh
-### 1. Tạo Service Mới
-
-```bash
-# Sao chép template sang service mới
-cp -r services/_template_dot_net services/your-service-name
-
-# Di chuyển đến thư mục service
-cd services/your-service-name
-
-# Đổi tên tất cả "ChatService" thành "YourService"
-find . -type f -name "*.cs" -exec sed -i '' 's/ChatService/YourService/g' {} +
-find . -type f -name "*.csproj" -exec sed -i '' 's/ChatService/YourService/g' {} +
-```
-
-### 2. Cấu Hình Môi Trường
+### 1. Cấu Hình Môi Trường
```bash
# Sao chép template môi trường
cp .env.example .env
-# Chỉnh sửa với cấu hình của bạn
+# Chỉnh sửa cấu hình
nano .env
```
-### 3. Chạy với Docker
+### 2. Chạy với Docker
```bash
-# Khởi động tất cả services (API + PostgreSQL + Redis)
+# Khởi động tất cả services
docker-compose up -d
# Xem logs
-docker-compose logs -f myservice-api
+docker-compose logs -f chatservice-api
```
-### 4. Chạy Local
+### 3. Chạy Local
```bash
-# Khôi phục dependencies
dotnet restore
-
-# Build tất cả projects
dotnet build
-
-# Chạy API
dotnet run --project src/ChatService.API
```
-## Cấu Trúc Dự Án
+## Tính Năng Chi Tiết
-```
-_template_dot_net/
-├── src/
-│ ├── ChatService.API/ # Lớp Presentation (Controllers, CQRS)
-│ │ ├── Controllers/ # Các API endpoints
-│ │ ├── Application/ # Triển khai CQRS
-│ │ │ ├── Commands/ # Thao tác ghi (MediatR)
-│ │ │ ├── Queries/ # Thao tác đọc
-│ │ │ ├── Behaviors/ # MediatR pipeline behaviors
-│ │ │ └── Validations/ # FluentValidation validators
-│ │ ├── Middleware/ # Custom middleware
-│ │ └── Program.cs # Điểm vào ứng dụng
-│ │
-│ ├── ChatService.Domain/ # Lớp Domain (Business logic thuần túy)
-│ │ ├── AggregatesModel/ # Aggregate roots và entities
-│ │ ├── Events/ # Domain events
-│ │ ├── Exceptions/ # Domain exceptions
-│ │ └── SeedWork/ # Base classes (Entity, ValueObject, etc.)
-│ │
-│ └── ChatService.Infrastructure/ # Lớp Infrastructure (Truy cập dữ liệu)
-│ ├── EntityConfigurations/ # Cấu hình EF Core Fluent API
-│ ├── Repositories/ # Triển khai repositories
-│ ├── Idempotency/ # Xử lý idempotency request
-│ └── ChatServiceContext.cs # DbContext với Unit of Work
-│
-├── tests/
-│ ├── ChatService.UnitTests/ # Unit tests (Domain, Application)
-│ └── ChatService.FunctionalTests/ # Integration tests (API endpoints)
-│
-├── Dockerfile # Multi-stage Docker build
-├── docker-compose.yml # Thiết lập phát triển local
-├── global.json # Pin phiên bản .NET SDK
-└── Directory.Build.props # Thuộc tính MSBuild chung
+### A. Giao Tiếp Thời Gian Thực
+
+| Tính năng | Mô tả |
+|-----------|-------|
+| **SignalR Hub** | Hub trung tâm xử lý tất cả real-time connections |
+| **Multi-Transport** | Tự động chọn WebSockets → SSE → Long Polling |
+| **Streaming** | Hỗ trợ IAsyncEnumerable cho streaming AI responses |
+
+```csharp
+// Gửi tin nhắn đến nhóm
+await Clients.Group(roomId).SendAsync("ReceiveMessage", message);
+
+// Streaming AI response
+public async IAsyncEnumerable StreamAIResponse(string prompt)
+{
+ await foreach (var chunk in _aiService.StreamAsync(prompt))
+ yield return chunk;
+}
```
-## Các Endpoint API
+### B. Khả Năng Mở Rộng
-| Method | Endpoint | Mô Tả |
+| Giải pháp | Use Case |
+|-----------|----------|
+| **Redis Backplane** | On-premise, multi-instance deployment |
+| **Azure SignalR Service** | Azure cloud, serverless scenarios |
+| **Sticky Sessions** | Fallback khi không dùng Azure SignalR |
+
+```csharp
+// Cấu hình Redis Backplane
+builder.Services.AddSignalR()
+ .AddStackExchangeRedis(connectionString, options => {
+ options.Configuration.ChannelPrefix = RedisChannel.Literal("ChatService");
+ });
+```
+
+### C. Quản Lý Người Dùng & Nhóm
+
+| Tính năng | Mô tả |
+|-----------|-------|
+| **Groups** | Gom kết nối theo phòng chat |
+| **User Mapping** | Ánh xạ ConnectionId → UserId qua Claims |
+| **Persistent State** | Lưu trạng thái nhóm vào database |
+
+```csharp
+// Custom User ID Provider
+public class CustomUserIdProvider : IUserIdProvider
+{
+ public string? GetUserId(HubConnectionContext connection)
+ => connection.User?.FindFirst(ClaimTypes.NameIdentifier)?.Value;
+}
+```
+
+### D. Tích Hợp AI
+
+| Tính năng | Mô tả |
+|-----------|-------|
+| **AI Assistant** | Chatbot trong nhóm (trigger: @gpt) |
+| **Streaming Response** | Đẩy từng phần câu trả lời realtime |
+| **Context History** | Lưu lịch sử chat cho context AI |
+
+### E. Giao Thức MessagePack
+
+```csharp
+// Server configuration
+builder.Services.AddSignalR()
+ .AddMessagePackProtocol();
+
+// Client configuration (JavaScript)
+const connection = new signalR.HubConnectionBuilder()
+ .withUrl("/chatHub")
+ .withHubProtocol(new signalR.protocols.msgpack.MessagePackHubProtocol())
+ .build();
+```
+
+### F. Khả Năng Phục Hồi
+
+| Tính năng | Mô tả |
+|-----------|-------|
+| **Auto Reconnect** | Client tự động kết nối lại |
+| **Stateful Reconnect** | Buffer dữ liệu khi gián đoạn (.NET 8+) |
+
+```csharp
+// Server: Bật Stateful Reconnect
+builder.Services.AddSignalR(options => {
+ options.EnableDetailedErrors = true;
+ options.StatefulReconnectBufferSize = 32 * 1024; // 32KB buffer
+});
+
+// Client: Cấu hình reconnect
+connection.WithAutomaticReconnect([0, 2000, 10000, 30000]);
+```
+
+## API Endpoints
+
+### HTTP REST APIs
+
+| Method | Endpoint | Mô tả |
|--------|----------|-------|
-| `GET` | `/api/v1/samples` | Lấy tất cả samples |
-| `GET` | `/api/v1/samples/{id}` | Lấy sample theo ID |
-| `POST` | `/api/v1/samples` | Tạo sample mới |
-| `PUT` | `/api/v1/samples/{id}` | Cập nhật sample |
-| `DELETE` | `/api/v1/samples/{id}` | Xóa sample |
-| `PATCH` | `/api/v1/samples/{id}/status` | Thay đổi trạng thái |
+| `GET` | `/api/v1/rooms` | Lấy danh sách phòng chat |
+| `POST` | `/api/v1/rooms` | Tạo phòng chat mới |
+| `GET` | `/api/v1/rooms/{id}/messages` | Lấy lịch sử tin nhắn |
+| `POST` | `/api/v1/rooms/{id}/participants` | Thêm thành viên |
+
+### SignalR Hub Methods
+
+| Method | Direction | Mô tả |
+|--------|-----------|-------|
+| `SendMessage` | Client → Server | Gửi tin nhắn |
+| `JoinRoom` | Client → Server | Tham gia phòng |
+| `LeaveRoom` | Client → Server | Rời phòng |
+| `ReceiveMessage` | Server → Client | Nhận tin nhắn |
+| `UserJoined` | Server → Client | Thông báo user mới |
### Health Endpoints
-| Endpoint | Mục Đích |
+| Endpoint | Mục đích |
|----------|----------|
-| `/health` | Trạng thái health đầy đủ |
-| `/health/live` | Kiểm tra sống |
-| `/health/ready` | Kiểm tra sẵn sàng |
-
-## Pattern CQRS
-
-### Commands (Thao Tác Ghi)
-
-```csharp
-// Định nghĩa command
-public record CreateSampleCommand(string Name, string? Description)
- : IRequest;
-
-// Xử lý command
-public class CreateSampleCommandHandler : IRequestHandler
-{
- public async Task Handle(CreateSampleCommand request, CancellationToken ct)
- {
- var sample = new Sample(request.Name, request.Description);
- _repository.Add(sample);
- await _repository.UnitOfWork.SaveEntitiesAsync(ct);
- return new CreateSampleCommandResult(sample.Id);
- }
-}
-```
-
-### Queries (Thao Tác Đọc)
-
-```csharp
-// Định nghĩa query
-public record GetSampleQuery(Guid SampleId) : IRequest;
-```
-
-## Domain Model
-
-### Aggregate Root
-
-```csharp
-public class Sample : Entity, IAggregateRoot
-{
- public string Name => _name;
- public SampleStatus Status => _status;
-
- public Sample(string name, string? description) {
- // Validation business logic
- if (string.IsNullOrWhiteSpace(name))
- throw new SampleDomainException("Tên sample không được để trống");
-
- // Domain event
- AddDomainEvent(new SampleCreatedDomainEvent(this));
- }
-
- public void Activate() {
- if (_status != SampleStatus.Draft)
- throw new SampleDomainException("Chỉ sample draft mới có thể kích hoạt");
- // Chuyển đổi trạng thái
- }
-}
-```
-
-## Kiểm Thử
-
-```bash
-# Chạy tất cả tests
-dotnet test
-
-# Chạy với coverage
-dotnet test /p:CollectCoverage=true /p:CoverageReportFormat=cobertura
-
-# Chạy project test cụ thể
-dotnet test tests/ChatService.UnitTests
-```
+| `/health` | Trạng thái đầy đủ |
+| `/health/live` | Liveness probe |
+| `/health/ready` | Readiness probe |
## Cấu Hình
### Biến Môi Trường
-| Biến | Mô Tả | Mặc định |
+| Biến | Mô tả | Mặc định |
|------|-------|----------|
-| `ASPNETCORE_ENVIRONMENT` | Tên môi trường | `Development` |
-| `DATABASE_URL` | Connection string PostgreSQL | - |
-| `REDIS_URL` | Connection string Redis | - |
-| `JWT_SECRET` | Secret ký JWT (tối thiểu 32 ký tự) | - |
+| `ASPNETCORE_ENVIRONMENT` | Môi trường | `Development` |
+| `DATABASE_URL` | PostgreSQL connection | - |
+| `REDIS_URL` | Redis connection | - |
+| `AZURE_SIGNALR_CONNECTION` | Azure SignalR (optional) | - |
+| `OPENAI_API_KEY` | OpenAI API key (optional) | - |
### appsettings.json
```json
{
"ConnectionStrings": {
- "DefaultConnection": "Host=localhost;Database=myservice;Username=postgres;Password=postgres"
+ "DefaultConnection": "Host=localhost;Database=chatservice;Username=postgres;Password=postgres",
+ "Redis": "localhost:6379"
},
- "Serilog": {
- "MinimumLevel": "Information"
+ "SignalR": {
+ "EnableMessagePack": true,
+ "StatefulReconnectBufferSize": 32768
+ },
+ "AI": {
+ "Provider": "OpenAI",
+ "Model": "gpt-4",
+ "MaxHistoryMessages": 20
}
}
```
@@ -232,33 +213,20 @@ dotnet test tests/ChatService.UnitTests
### Docker Build
```bash
-# Build Docker image
-docker build -t myservice:latest .
-
-# Chạy container
-docker run -p 5000:8080 --env-file .env myservice:latest
+docker build -t chatservice:latest .
+docker run -p 5000:8080 --env-file .env chatservice:latest
```
### Kubernetes
-Xem [ARCHITECTURE.md](./ARCHITECTURE.md) để biết manifests triển khai Kubernetes.
+Xem [ARCHITECTURE.md](./ARCHITECTURE.md) để biết chi tiết cấu hình Kubernetes với Sticky Sessions.
-## Có Gì Mới Trong .NET 10
+## Tài Liệu Tham Khảo
-- Tính năng ngôn ngữ **C# 14**
-- Hỗ trợ **Native AOT** được cải thiện
-- Hiệu suất **async/await** tốt hơn
-- **JSON serialization** được nâng cao
-- Cải thiện hiệu suất toàn diện
-- Hỗ trợ **LTS** 3 năm (đến tháng 11/2028)
-
-## Tài Nguyên
-
-- [eShopOnContainers](https://github.com/dotnet-architecture/eShopOnContainers) - Kiến trúc tham chiếu
-- [Tài liệu .NET 10](https://docs.microsoft.com/en-us/dotnet/core/whats-new/dotnet-10)
-- [DDD với .NET](https://docs.microsoft.com/en-us/dotnet/architecture/microservices/microservice-ddd-cqrs-patterns/)
-- [MediatR](https://github.com/jbogard/MediatR) - Thư viện CQRS
-- [FluentValidation](https://docs.fluentvalidation.net/) - Thư viện validation
+- [ASP.NET Core SignalR](https://docs.microsoft.com/en-us/aspnet/core/signalr/)
+- [Redis Backplane](https://docs.microsoft.com/en-us/aspnet/core/signalr/redis-backplane)
+- [Azure SignalR Service](https://docs.microsoft.com/en-us/azure/azure-signalr/)
+- [MessagePack Protocol](https://docs.microsoft.com/en-us/aspnet/core/signalr/messagepackhubprotocol)
## Giấy Phép
diff --git a/services/chat-service-net/src/ChatService.API/ChatService.API.csproj b/services/chat-service-net/src/ChatService.API/ChatService.API.csproj
index e2281c06..8ac6c9ab 100644
--- a/services/chat-service-net/src/ChatService.API/ChatService.API.csproj
+++ b/services/chat-service-net/src/ChatService.API/ChatService.API.csproj
@@ -36,6 +36,7 @@
+
diff --git a/services/chat-service-net/src/ChatService.API/Hubs/ChatHub.cs b/services/chat-service-net/src/ChatService.API/Hubs/ChatHub.cs
new file mode 100644
index 00000000..c48baca7
--- /dev/null
+++ b/services/chat-service-net/src/ChatService.API/Hubs/ChatHub.cs
@@ -0,0 +1,350 @@
+using System.Runtime.CompilerServices;
+using Microsoft.AspNetCore.Authorization;
+using Microsoft.AspNetCore.SignalR;
+using ChatService.Domain.AggregatesModel.ConversationAggregate;
+using ChatService.Domain.Contracts;
+
+namespace ChatService.API.Hubs;
+
+///
+/// EN: Main SignalR hub for real-time chat functionality.
+/// VI: SignalR hub chính cho chức năng chat thời gian thực.
+///
+[Authorize]
+public class ChatHub : Hub
+{
+ private readonly IConversationRepository _conversationRepository;
+ private readonly IAIService _aiService;
+ private readonly ILogger _logger;
+
+ // EN: Track active connections per user / VI: Theo dõi kết nối active theo user
+ private static readonly Dictionary> _userConnections = new();
+ private static readonly object _lock = new();
+
+ public ChatHub(
+ IConversationRepository conversationRepository,
+ IAIService aiService,
+ ILogger logger)
+ {
+ _conversationRepository = conversationRepository;
+ _aiService = aiService;
+ _logger = logger;
+ }
+
+ #region Connection Lifecycle
+
+ ///
+ /// EN: Called when a new connection is established.
+ /// VI: Được gọi khi kết nối mới được thiết lập.
+ ///
+ public override async Task OnConnectedAsync()
+ {
+ var userId = Context.UserIdentifier;
+ if (string.IsNullOrEmpty(userId))
+ {
+ _logger.LogWarning("Connection attempted without user identifier");
+ await base.OnConnectedAsync();
+ return;
+ }
+
+ _logger.LogInformation("User {UserId} connected with connection {ConnectionId}",
+ userId, Context.ConnectionId);
+
+ // EN: Track user connection / VI: Theo dõi kết nối user
+ lock (_lock)
+ {
+ if (!_userConnections.ContainsKey(userId))
+ {
+ _userConnections[userId] = new HashSet();
+ }
+ _userConnections[userId].Add(Context.ConnectionId);
+ }
+
+ // EN: Join user's conversation rooms / VI: Tham gia các phòng hội thoại của user
+ try
+ {
+ var conversations = await _conversationRepository.GetUserConversationsAsync(
+ Guid.Parse(userId),
+ 0,
+ 100, // Max rooms to auto-join
+ CancellationToken.None);
+
+ foreach (var conv in conversations)
+ {
+ await Groups.AddToGroupAsync(Context.ConnectionId, conv.Id.ToString());
+ _logger.LogDebug("User {UserId} auto-joined room {RoomId}", userId, conv.Id);
+ }
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError(ex, "Failed to auto-join rooms for user {UserId}", userId);
+ }
+
+ // EN: Notify others that user is online / VI: Thông báo user đã online
+ await Clients.Others.UserStatusChanged(userId, true, null);
+
+ await base.OnConnectedAsync();
+ }
+
+ ///
+ /// EN: Called when a connection is terminated.
+ /// VI: Được gọi khi kết nối bị ngắt.
+ ///
+ public override async Task OnDisconnectedAsync(Exception? exception)
+ {
+ var userId = Context.UserIdentifier;
+ if (!string.IsNullOrEmpty(userId))
+ {
+ bool isLastConnection;
+ lock (_lock)
+ {
+ if (_userConnections.TryGetValue(userId, out var connections))
+ {
+ connections.Remove(Context.ConnectionId);
+ isLastConnection = connections.Count == 0;
+ if (isLastConnection)
+ {
+ _userConnections.Remove(userId);
+ }
+ }
+ else
+ {
+ isLastConnection = true;
+ }
+ }
+
+ if (isLastConnection)
+ {
+ // EN: User is completely offline / VI: User hoàn toàn offline
+ await Clients.Others.UserStatusChanged(userId, false, DateTime.UtcNow);
+ _logger.LogInformation("User {UserId} is now offline", userId);
+ }
+ }
+
+ if (exception != null)
+ {
+ _logger.LogError(exception, "Connection {ConnectionId} disconnected with error",
+ Context.ConnectionId);
+ }
+
+ await base.OnDisconnectedAsync(exception);
+ }
+
+ #endregion
+
+ #region Room Management
+
+ ///
+ /// EN: Join a specific chat room/conversation.
+ /// VI: Tham gia phòng chat/cuộc hội thoại cụ thể.
+ ///
+ public async Task JoinRoom(Guid roomId)
+ {
+ var userId = Context.UserIdentifier ?? throw new HubException("User not authenticated");
+
+ _logger.LogInformation("User {UserId} joining room {RoomId}", userId, roomId);
+
+ await Groups.AddToGroupAsync(Context.ConnectionId, roomId.ToString());
+
+ // EN: Get user name from claims / VI: Lấy tên user từ claims
+ var userName = Context.User?.FindFirst("name")?.Value
+ ?? Context.User?.FindFirst("preferred_username")?.Value
+ ?? userId;
+
+ // EN: Notify other members / VI: Thông báo các thành viên khác
+ await Clients.OthersInGroup(roomId.ToString())
+ .UserJoined(userId, userName, roomId);
+ }
+
+ ///
+ /// EN: Leave a specific chat room/conversation.
+ /// VI: Rời phòng chat/cuộc hội thoại cụ thể.
+ ///
+ public async Task LeaveRoom(Guid roomId)
+ {
+ var userId = Context.UserIdentifier ?? throw new HubException("User not authenticated");
+
+ _logger.LogInformation("User {UserId} leaving room {RoomId}", userId, roomId);
+
+ await Groups.RemoveFromGroupAsync(Context.ConnectionId, roomId.ToString());
+
+ // EN: Notify other members / VI: Thông báo các thành viên khác
+ await Clients.OthersInGroup(roomId.ToString())
+ .UserLeft(userId, roomId);
+ }
+
+ #endregion
+
+ #region Messaging
+
+ ///
+ /// EN: Send a message to a room. The message is saved and broadcast to all room members.
+ /// VI: Gửi tin nhắn đến phòng. Tin nhắn được lưu và broadcast đến tất cả thành viên.
+ ///
+ public async Task SendMessage(Guid roomId, string content, string? messageType = null)
+ {
+ var userId = Context.UserIdentifier ?? throw new HubException("User not authenticated");
+
+ if (string.IsNullOrWhiteSpace(content))
+ {
+ throw new HubException("Message content cannot be empty");
+ }
+
+ _logger.LogInformation("User {UserId} sending message to room {RoomId}", userId, roomId);
+
+ // EN: Get user info from claims / VI: Lấy thông tin user từ claims
+ var userName = Context.User?.FindFirst("name")?.Value
+ ?? Context.User?.FindFirst("preferred_username")?.Value
+ ?? "Unknown";
+
+ // EN: Create message notification / VI: Tạo thông báo tin nhắn
+ var messageId = Guid.NewGuid();
+ var notification = new MessageNotification(
+ Id: messageId,
+ ConversationId: roomId,
+ SenderId: userId,
+ SenderName: userName,
+ Content: content,
+ MessageType: messageType ?? "text",
+ SentAt: DateTime.UtcNow
+ );
+
+ // EN: Broadcast to room / VI: Broadcast đến phòng
+ await Clients.Group(roomId.ToString()).ReceiveMessage(notification);
+
+ // EN: Check for AI trigger / VI: Kiểm tra AI trigger
+ if (content.StartsWith("@gpt ", StringComparison.OrdinalIgnoreCase))
+ {
+ var prompt = content[5..].Trim();
+ if (!string.IsNullOrEmpty(prompt))
+ {
+ // EN: Start streaming AI response in background
+ // VI: Bắt đầu streaming AI response trong background
+ _ = StreamAIResponseToRoom(roomId, prompt);
+ }
+ }
+ }
+
+ ///
+ /// EN: Send typing indicator to a room.
+ /// VI: Gửi typing indicator đến phòng.
+ ///
+ public async Task SendTypingIndicator(Guid roomId, bool isTyping)
+ {
+ var userId = Context.UserIdentifier ?? throw new HubException("User not authenticated");
+
+ var userName = Context.User?.FindFirst("name")?.Value
+ ?? Context.User?.FindFirst("preferred_username")?.Value
+ ?? "Unknown";
+
+ await Clients.OthersInGroup(roomId.ToString())
+ .TypingIndicator(userId, userName, roomId, isTyping);
+ }
+
+ ///
+ /// EN: Mark a message as read.
+ /// VI: Đánh dấu tin nhắn đã đọc.
+ ///
+ public async Task MarkMessageRead(Guid roomId, Guid messageId)
+ {
+ var userId = Context.UserIdentifier ?? throw new HubException("User not authenticated");
+
+ await Clients.OthersInGroup(roomId.ToString())
+ .MessageRead(userId, messageId, roomId);
+ }
+
+ #endregion
+
+ #region AI Integration
+
+ ///
+ /// EN: Stream AI response to the caller (invokable from client).
+ /// VI: Stream AI response đến caller (có thể gọi từ client).
+ ///
+ public async IAsyncEnumerable StreamAIResponse(
+ string prompt,
+ Guid roomId,
+ [EnumeratorCancellation] CancellationToken ct = default)
+ {
+ var userId = Context.UserIdentifier ?? throw new HubException("User not authenticated");
+
+ _logger.LogInformation("User {UserId} requested AI response in room {RoomId}", userId, roomId);
+
+ // EN: Get conversation history for context
+ // VI: Lấy lịch sử hội thoại cho context
+ var history = await GetConversationHistoryAsync(roomId, ct);
+
+ await foreach (var chunk in _aiService.StreamResponseAsync(prompt, history, ct))
+ {
+ yield return chunk;
+ }
+ }
+
+ ///
+ /// EN: Internal method to stream AI response to entire room.
+ /// VI: Method nội bộ để stream AI response đến toàn bộ phòng.
+ ///
+ private async Task StreamAIResponseToRoom(Guid roomId, string prompt)
+ {
+ var aiMessageId = Guid.NewGuid();
+ var fullResponse = new System.Text.StringBuilder();
+
+ try
+ {
+ var history = await GetConversationHistoryAsync(roomId, CancellationToken.None);
+
+ await foreach (var chunk in _aiService.StreamResponseAsync(prompt, history))
+ {
+ fullResponse.Append(chunk);
+ await Clients.Group(roomId.ToString())
+ .ReceiveAIChunk(chunk, aiMessageId);
+ }
+
+ await Clients.Group(roomId.ToString())
+ .AIResponseComplete(aiMessageId, fullResponse.ToString());
+
+ _logger.LogInformation("AI response completed for room {RoomId}", roomId);
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError(ex, "AI streaming failed for room {RoomId}", roomId);
+ await Clients.Group(roomId.ToString())
+ .AIResponseComplete(aiMessageId, "[AI Error: Unable to generate response]");
+ }
+ }
+
+ ///
+ /// EN: Get conversation history for AI context.
+ /// VI: Lấy lịch sử hội thoại cho AI context.
+ ///
+ private async Task> GetConversationHistoryAsync(
+ Guid roomId,
+ CancellationToken ct)
+ {
+ try
+ {
+ var conversation = await _conversationRepository.GetByIdAsync(roomId, ct);
+ if (conversation == null)
+ {
+ return Enumerable.Empty();
+ }
+
+ return conversation.Messages
+ .OrderByDescending(m => m.CreatedAt)
+ .Take(20)
+ .Reverse()
+ .Select(m => new ChatMessage(
+ Role: m.SenderId.ToString() == Context.UserIdentifier ? "user" : "assistant",
+ Content: m.EncryptedContent, // EN: Note: Encrypted content / VI: Lưu ý: Nội dung đã mã hóa
+ Timestamp: m.CreatedAt
+ ));
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError(ex, "Failed to get conversation history for room {RoomId}", roomId);
+ return Enumerable.Empty();
+ }
+ }
+
+ #endregion
+}
diff --git a/services/chat-service-net/src/ChatService.API/Hubs/ClaimsUserIdProvider.cs b/services/chat-service-net/src/ChatService.API/Hubs/ClaimsUserIdProvider.cs
new file mode 100644
index 00000000..53384487
--- /dev/null
+++ b/services/chat-service-net/src/ChatService.API/Hubs/ClaimsUserIdProvider.cs
@@ -0,0 +1,25 @@
+using System.Security.Claims;
+using Microsoft.AspNetCore.SignalR;
+
+namespace ChatService.API.Hubs;
+
+///
+/// EN: Custom user ID provider that extracts user ID from JWT claims.
+/// VI: Custom user ID provider lấy user ID từ JWT claims.
+///
+///
+/// EN: This enables sending messages to specific users regardless of which device they're connected from.
+/// VI: Cho phép gửi tin nhắn đến user cụ thể bất kể họ đang kết nối từ thiết bị nào.
+///
+public class ClaimsUserIdProvider : IUserIdProvider
+{
+ ///
+ public string? GetUserId(HubConnectionContext connection)
+ {
+ // EN: Try to get user ID from standard claim types
+ // VI: Thử lấy user ID từ các claim types chuẩn
+ return connection.User?.FindFirst(ClaimTypes.NameIdentifier)?.Value
+ ?? connection.User?.FindFirst("sub")?.Value
+ ?? connection.User?.FindFirst("user_id")?.Value;
+ }
+}
diff --git a/services/chat-service-net/src/ChatService.API/Program.cs b/services/chat-service-net/src/ChatService.API/Program.cs
index b4efc1ae..c7f0ab31 100644
--- a/services/chat-service-net/src/ChatService.API/Program.cs
+++ b/services/chat-service-net/src/ChatService.API/Program.cs
@@ -1,9 +1,12 @@
using Asp.Versioning;
using FluentValidation;
using Hellang.Middleware.ProblemDetails;
+using Microsoft.AspNetCore.SignalR;
using ChatService.API.Application.Behaviors;
+using ChatService.API.Hubs;
using ChatService.Infrastructure;
using Serilog;
+using StackExchange.Redis;
// EN: Configure Serilog early / VI: Cấu hình Serilog sớm
Log.Logger = new LoggerConfiguration()
@@ -38,6 +41,38 @@ try
// EN: Add FluentValidation / VI: Thêm FluentValidation
builder.Services.AddValidatorsFromAssemblyContaining();
+ // EN: Add SignalR with Redis Backplane and MessagePack
+ // VI: Thêm SignalR với Redis Backplane và MessagePack
+ var redisConnection = builder.Configuration.GetConnectionString("Redis") ?? "localhost:6379";
+ var signalRBuilder = builder.Services.AddSignalR(options =>
+ {
+ options.EnableDetailedErrors = builder.Environment.IsDevelopment();
+ options.KeepAliveInterval = TimeSpan.FromSeconds(
+ builder.Configuration.GetValue("SignalR:KeepAliveInterval", 15));
+ options.ClientTimeoutInterval = TimeSpan.FromSeconds(
+ builder.Configuration.GetValue("SignalR:ClientTimeoutInterval", 30));
+
+ // EN: Enable Stateful Reconnect (.NET 8+) / VI: Bật Stateful Reconnect
+ options.StatefulReconnectBufferSize =
+ builder.Configuration.GetValue("SignalR:StatefulReconnectBufferSize", 32768);
+ });
+
+ // EN: Add Redis Backplane for scaling / VI: Thêm Redis Backplane để scale
+ signalRBuilder.AddStackExchangeRedis(redisConnection, options =>
+ {
+ options.Configuration.ChannelPrefix = RedisChannel.Literal("ChatService");
+ options.Configuration.AbortOnConnectFail = false;
+ });
+
+ // EN: Add MessagePack protocol for better performance / VI: Thêm MessagePack cho hiệu năng tốt hơn
+ if (builder.Configuration.GetValue("SignalR:EnableMessagePack", true))
+ {
+ signalRBuilder.AddMessagePackProtocol();
+ }
+
+ // EN: Add custom User ID provider / VI: Thêm custom User ID provider
+ builder.Services.AddSingleton();
+
// EN: Add API versioning / VI: Thêm API versioning
builder.Services.AddApiVersioning(options =>
{
@@ -72,7 +107,7 @@ try
{
Title = "ChatService API",
Version = "v1",
- Description = "ChatService microservice API / API microservice ChatService"
+ Description = "Real-time Chat Service with SignalR, AI Integration / Chat Service thời gian thực với SignalR, tích hợp AI"
});
});
@@ -83,16 +118,23 @@ try
?? builder.Configuration["DATABASE_URL"]
?? "",
name: "postgresql",
- tags: ["db", "postgresql"]);
+ tags: ["db", "postgresql"])
+ .AddRedis(
+ redisConnection,
+ name: "redis",
+ tags: ["cache", "redis"]);
- // EN: Add CORS / VI: Thêm CORS
+ // EN: Add CORS for WebSocket / VI: Thêm CORS cho WebSocket
builder.Services.AddCors(options =>
{
options.AddDefaultPolicy(policy =>
{
- policy.AllowAnyOrigin()
+ policy.WithOrigins(
+ builder.Configuration.GetSection("AllowedOrigins").Get()
+ ?? ["http://localhost:3000", "http://localhost:5173"])
.AllowAnyMethod()
- .AllowAnyHeader();
+ .AllowAnyHeader()
+ .AllowCredentials(); // EN: Required for SignalR / VI: Bắt buộc cho SignalR
});
});
@@ -126,6 +168,12 @@ try
// EN: Map controllers / VI: Map controllers
app.MapControllers();
+ // EN: Map SignalR Hub / VI: Map SignalR Hub
+ app.MapHub("/hubs/chat", options =>
+ {
+ options.AllowStatefulReconnects = true;
+ });
+
// EN: Run the application / VI: Chạy ứng dụng
app.Run();
}
@@ -142,3 +190,4 @@ finally
// EN: Make Program class accessible for integration tests
// VI: Làm cho class Program có thể truy cập cho integration tests
public partial class Program { }
+
diff --git a/services/chat-service-net/src/ChatService.API/Properties/launchSettings.json b/services/chat-service-net/src/ChatService.API/Properties/launchSettings.json
index 6355d40b..34dab9ae 100644
--- a/services/chat-service-net/src/ChatService.API/Properties/launchSettings.json
+++ b/services/chat-service-net/src/ChatService.API/Properties/launchSettings.json
@@ -6,7 +6,7 @@
"dotnetRunMessages": true,
"launchBrowser": true,
"launchUrl": "swagger",
- "applicationUrl": "http://localhost:5000",
+ "applicationUrl": "http://localhost:5010",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
diff --git a/services/chat-service-net/src/ChatService.API/appsettings.json b/services/chat-service-net/src/ChatService.API/appsettings.json
index 523dc0fc..7948c7c9 100644
--- a/services/chat-service-net/src/ChatService.API/appsettings.json
+++ b/services/chat-service-net/src/ChatService.API/appsettings.json
@@ -30,10 +30,22 @@
]
},
"ConnectionStrings": {
- "DefaultConnection": "Host=localhost;Port=5432;Database=myservice_db;Username=postgres;Password=postgres"
+ "DefaultConnection": "Host=ep-holy-glitter-a4hongg7-pooler.us-east-1.aws.neon.tech;Database=chat_service;Username=neondb_owner;Password=npg_Ssfy6HKO0cXI;SSL Mode=Require",
+ "Redis": "localhost:6379"
},
- "Redis": {
- "ConnectionString": "localhost:6379"
+ "SignalR": {
+ "EnableMessagePack": true,
+ "StatefulReconnectBufferSize": 32768,
+ "KeepAliveInterval": 15,
+ "ClientTimeoutInterval": 30
+ },
+ "AI": {
+ "Provider": "OpenAI",
+ "Model": "gpt-4",
+ "MaxHistoryMessages": 20,
+ "MaxTokens": 1000,
+ "Temperature": 0.7,
+ "SystemPrompt": "You are a helpful assistant in a chat application. Respond concisely and helpfully."
},
"Jwt": {
"Secret": "your-super-secret-key-min-32-characters",
diff --git a/services/chat-service-net/src/ChatService.Domain/Contracts/IAIService.cs b/services/chat-service-net/src/ChatService.Domain/Contracts/IAIService.cs
new file mode 100644
index 00000000..0736f6af
--- /dev/null
+++ b/services/chat-service-net/src/ChatService.Domain/Contracts/IAIService.cs
@@ -0,0 +1,94 @@
+namespace ChatService.Domain.Contracts;
+
+///
+/// EN: Interface for AI service integration (OpenAI, Azure OpenAI, etc.).
+/// VI: Interface cho tích hợp AI service (OpenAI, Azure OpenAI, v.v.).
+///
+public interface IAIService
+{
+ ///
+ /// EN: Stream AI response for a given prompt with conversation context.
+ /// VI: Stream AI response cho prompt với context cuộc hội thoại.
+ ///
+ /// User prompt / Prompt từ user
+ /// Previous messages for context / Tin nhắn trước đó cho context
+ /// Cancellation token
+ /// Async stream of response chunks / Stream async các phần response
+ IAsyncEnumerable StreamResponseAsync(
+ string prompt,
+ IEnumerable conversationHistory,
+ CancellationToken ct = default);
+
+ ///
+ /// EN: Get a complete AI response (non-streaming).
+ /// VI: Lấy AI response hoàn chỉnh (không streaming).
+ ///
+ Task GetResponseAsync(
+ string prompt,
+ IEnumerable conversationHistory,
+ CancellationToken ct = default);
+
+ ///
+ /// EN: Check if AI service is available.
+ /// VI: Kiểm tra AI service có khả dụng không.
+ ///
+ Task IsAvailableAsync(CancellationToken ct = default);
+}
+
+///
+/// EN: Chat message DTO for AI context.
+/// VI: DTO tin nhắn chat cho AI context.
+///
+public record ChatMessage(
+ string Role, // "user", "assistant", "system"
+ string Content,
+ DateTime Timestamp
+);
+
+///
+/// EN: AI service configuration options.
+/// VI: Cấu hình cho AI service.
+///
+public class AIServiceOptions
+{
+ public const string SectionName = "AI";
+
+ ///
+ /// EN: AI provider (OpenAI, AzureOpenAI, Anthropic, etc.)
+ /// VI: Nhà cung cấp AI (OpenAI, AzureOpenAI, Anthropic, v.v.)
+ ///
+ public string Provider { get; set; } = "OpenAI";
+
+ ///
+ /// EN: Model name to use.
+ /// VI: Tên model sử dụng.
+ ///
+ public string Model { get; set; } = "gpt-4";
+
+ ///
+ /// EN: Maximum number of history messages to include in context.
+ /// VI: Số tin nhắn lịch sử tối đa đưa vào context.
+ ///
+ public int MaxHistoryMessages { get; set; } = 20;
+
+ ///
+ /// EN: System prompt for the AI assistant.
+ /// VI: System prompt cho AI assistant.
+ ///
+ public string SystemPrompt { get; set; } =
+ "You are a helpful assistant in a chat application. " +
+ "Respond concisely and helpfully. " +
+ "If asked about topics you don't know, admit it.";
+
+ ///
+ /// EN: Maximum tokens for response.
+ /// VI: Số token tối đa cho response.
+ ///
+ public int MaxTokens { get; set; } = 1000;
+
+ ///
+ /// EN: Temperature for response creativity (0.0 - 2.0).
+ /// VI: Temperature cho độ sáng tạo của response (0.0 - 2.0).
+ ///
+ public float Temperature { get; set; } = 0.7f;
+}
diff --git a/services/chat-service-net/src/ChatService.Domain/Contracts/IChatHubClient.cs b/services/chat-service-net/src/ChatService.Domain/Contracts/IChatHubClient.cs
new file mode 100644
index 00000000..6cf7b02a
--- /dev/null
+++ b/services/chat-service-net/src/ChatService.Domain/Contracts/IChatHubClient.cs
@@ -0,0 +1,71 @@
+namespace ChatService.Domain.Contracts;
+
+///
+/// EN: Strongly-typed SignalR client interface for type-safe hub method calls.
+/// VI: Interface SignalR client với strongly-typed cho việc gọi hub methods an toàn về kiểu.
+///
+public interface IChatHubClient
+{
+ ///
+ /// EN: Receive a new message in a conversation.
+ /// VI: Nhận tin nhắn mới trong cuộc hội thoại.
+ ///
+ Task ReceiveMessage(MessageNotification message);
+
+ ///
+ /// EN: Notification when a user joins a room.
+ /// VI: Thông báo khi user tham gia phòng.
+ ///
+ Task UserJoined(string userId, string userName, Guid roomId);
+
+ ///
+ /// EN: Notification when a user leaves a room.
+ /// VI: Thông báo khi user rời phòng.
+ ///
+ Task UserLeft(string userId, Guid roomId);
+
+ ///
+ /// EN: Receive a chunk of AI streaming response.
+ /// VI: Nhận một phần của AI streaming response.
+ ///
+ Task ReceiveAIChunk(string chunk, Guid messageId);
+
+ ///
+ /// EN: Notification when AI response is complete.
+ /// VI: Thông báo khi AI response hoàn thành.
+ ///
+ Task AIResponseComplete(Guid messageId, string fullResponse);
+
+ ///
+ /// EN: Typing indicator notification.
+ /// VI: Thông báo đang gõ tin nhắn.
+ ///
+ Task TypingIndicator(string userId, string userName, Guid roomId, bool isTyping);
+
+ ///
+ /// EN: Message read receipt notification.
+ /// VI: Thông báo đã đọc tin nhắn.
+ ///
+ Task MessageRead(string userId, Guid messageId, Guid roomId);
+
+ ///
+ /// EN: User online/offline status change.
+ /// VI: Thay đổi trạng thái online/offline của user.
+ ///
+ Task UserStatusChanged(string userId, bool isOnline, DateTime? lastSeen);
+}
+
+///
+/// EN: Message notification DTO for SignalR.
+/// VI: DTO thông báo tin nhắn cho SignalR.
+///
+public record MessageNotification(
+ Guid Id,
+ Guid ConversationId,
+ string SenderId,
+ string SenderName,
+ string Content,
+ string MessageType,
+ DateTime SentAt,
+ Guid? ReplyToMessageId = null
+);
diff --git a/services/chat-service-net/src/ChatService.Domain/Events/ConversationDomainEvents.cs b/services/chat-service-net/src/ChatService.Domain/Events/ConversationDomainEvents.cs
index db081372..cbaf7ff6 100644
--- a/services/chat-service-net/src/ChatService.Domain/Events/ConversationDomainEvents.cs
+++ b/services/chat-service-net/src/ChatService.Domain/Events/ConversationDomainEvents.cs
@@ -72,3 +72,62 @@ public class MessageReadDomainEvent : INotification
ReadAt = DateTime.UtcNow;
}
}
+
+///
+/// EN: Domain event raised when a user joins a conversation room.
+/// VI: Domain event được phát ra khi user tham gia phòng hội thoại.
+///
+public class UserJoinedRoomDomainEvent : INotification
+{
+ public Guid UserId { get; }
+ public Guid ConversationId { get; }
+ public string UserName { get; }
+ public DateTime JoinedAt { get; }
+
+ public UserJoinedRoomDomainEvent(Guid userId, Guid conversationId, string userName)
+ {
+ UserId = userId;
+ ConversationId = conversationId;
+ UserName = userName;
+ JoinedAt = DateTime.UtcNow;
+ }
+}
+
+///
+/// EN: Domain event raised when a user leaves a conversation room.
+/// VI: Domain event được phát ra khi user rời phòng hội thoại.
+///
+public class UserLeftRoomDomainEvent : INotification
+{
+ public Guid UserId { get; }
+ public Guid ConversationId { get; }
+ public DateTime LeftAt { get; }
+
+ public UserLeftRoomDomainEvent(Guid userId, Guid conversationId)
+ {
+ UserId = userId;
+ ConversationId = conversationId;
+ LeftAt = DateTime.UtcNow;
+ }
+}
+
+///
+/// EN: Domain event raised when a user is typing in a conversation.
+/// VI: Domain event được phát ra khi user đang gõ tin nhắn.
+///
+public class TypingDomainEvent : INotification
+{
+ public Guid UserId { get; }
+ public Guid ConversationId { get; }
+ public string UserName { get; }
+ public bool IsTyping { get; }
+
+ public TypingDomainEvent(Guid userId, Guid conversationId, string userName, bool isTyping)
+ {
+ UserId = userId;
+ ConversationId = conversationId;
+ UserName = userName;
+ IsTyping = isTyping;
+ }
+}
+
diff --git a/services/chat-service-net/src/ChatService.Infrastructure/DependencyInjection.cs b/services/chat-service-net/src/ChatService.Infrastructure/DependencyInjection.cs
index 43ec9067..67eb306b 100644
--- a/services/chat-service-net/src/ChatService.Infrastructure/DependencyInjection.cs
+++ b/services/chat-service-net/src/ChatService.Infrastructure/DependencyInjection.cs
@@ -3,8 +3,10 @@ using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using ChatService.Domain.AggregatesModel.UserAggregate;
using ChatService.Domain.AggregatesModel.ConversationAggregate;
+using ChatService.Domain.Contracts;
using ChatService.Infrastructure.Idempotency;
using ChatService.Infrastructure.Repositories;
+using ChatService.Infrastructure.Services;
namespace ChatService.Infrastructure;
@@ -54,6 +56,27 @@ public static class DependencyInjection
// EN: Register idempotency services / VI: Đăng ký idempotency services
services.AddScoped();
+ // EN: Register AI service / VI: Đăng ký AI service
+ services.Configure(configuration.GetSection(AIServiceOptions.SectionName));
+
+ var openAiApiKey = configuration["OPENAI_API_KEY"]
+ ?? Environment.GetEnvironmentVariable("OPENAI_API_KEY");
+
+ if (!string.IsNullOrEmpty(openAiApiKey))
+ {
+ services.AddHttpClient(client =>
+ {
+ client.DefaultRequestHeaders.Add("Authorization", $"Bearer {openAiApiKey}");
+ });
+ }
+ else
+ {
+ // EN: Use null implementation when API key is not configured
+ // VI: Sử dụng null implementation khi API key chưa được cấu hình
+ services.AddSingleton();
+ }
+
return services;
}
}
+
diff --git a/services/chat-service-net/src/ChatService.Infrastructure/Services/AIService.cs b/services/chat-service-net/src/ChatService.Infrastructure/Services/AIService.cs
new file mode 100644
index 00000000..00b9c63c
--- /dev/null
+++ b/services/chat-service-net/src/ChatService.Infrastructure/Services/AIService.cs
@@ -0,0 +1,204 @@
+using System.Runtime.CompilerServices;
+using System.Text;
+using ChatService.Domain.Contracts;
+using Microsoft.Extensions.Logging;
+using Microsoft.Extensions.Options;
+
+namespace ChatService.Infrastructure.Services;
+
+///
+/// EN: AI service implementation using OpenAI API.
+/// VI: Implementation AI service sử dụng OpenAI API.
+///
+public class AIService : IAIService
+{
+ private readonly HttpClient _httpClient;
+ private readonly AIServiceOptions _options;
+ private readonly ILogger _logger;
+
+ public AIService(
+ HttpClient httpClient,
+ IOptions options,
+ ILogger logger)
+ {
+ _httpClient = httpClient;
+ _options = options.Value;
+ _logger = logger;
+
+ // EN: Configure base address for OpenAI API
+ // VI: Cấu hình base address cho OpenAI API
+ _httpClient.BaseAddress = new Uri("https://api.openai.com/v1/");
+ _httpClient.DefaultRequestHeaders.Add("Accept", "text/event-stream");
+ }
+
+ ///
+ public async IAsyncEnumerable StreamResponseAsync(
+ string prompt,
+ IEnumerable conversationHistory,
+ [EnumeratorCancellation] CancellationToken ct = default)
+ {
+ _logger.LogInformation(
+ "Starting AI stream response for prompt: {PromptPreview}...",
+ prompt.Length > 50 ? prompt[..50] : prompt);
+
+ var messages = BuildMessagesPayload(prompt, conversationHistory);
+ var requestBody = new
+ {
+ model = _options.Model,
+ messages = messages,
+ max_tokens = _options.MaxTokens,
+ temperature = _options.Temperature,
+ stream = true
+ };
+
+ var jsonContent = System.Text.Json.JsonSerializer.Serialize(requestBody);
+ using var request = new HttpRequestMessage(HttpMethod.Post, "chat/completions")
+ {
+ Content = new StringContent(jsonContent, Encoding.UTF8, "application/json")
+ };
+
+ using var response = await _httpClient.SendAsync(
+ request,
+ HttpCompletionOption.ResponseHeadersRead,
+ ct);
+
+ response.EnsureSuccessStatusCode();
+
+ await using var stream = await response.Content.ReadAsStreamAsync(ct);
+ using var reader = new StreamReader(stream);
+
+ string? line;
+ while ((line = await reader.ReadLineAsync(ct)) != null && !ct.IsCancellationRequested)
+ {
+ if (string.IsNullOrEmpty(line)) continue;
+ if (!line.StartsWith("data: ")) continue;
+
+ var data = line[6..];
+ if (data == "[DONE]") break;
+
+ // EN: Parse SSE data and extract content delta
+ // VI: Parse SSE data và extract content delta
+ var chunk = ParseStreamChunk(data);
+ if (!string.IsNullOrEmpty(chunk))
+ {
+ yield return chunk;
+ }
+ }
+
+ _logger.LogInformation("AI stream response completed");
+ }
+
+ ///
+ public async Task GetResponseAsync(
+ string prompt,
+ IEnumerable conversationHistory,
+ CancellationToken ct = default)
+ {
+ var sb = new StringBuilder();
+ await foreach (var chunk in StreamResponseAsync(prompt, conversationHistory, ct))
+ {
+ sb.Append(chunk);
+ }
+ return sb.ToString();
+ }
+
+ ///
+ public async Task IsAvailableAsync(CancellationToken ct = default)
+ {
+ try
+ {
+ using var request = new HttpRequestMessage(HttpMethod.Get, "models");
+ using var response = await _httpClient.SendAsync(request, ct);
+ return response.IsSuccessStatusCode;
+ }
+ catch (Exception ex)
+ {
+ _logger.LogWarning(ex, "AI service availability check failed");
+ return false;
+ }
+ }
+
+ ///
+ /// EN: Build the messages array for OpenAI API.
+ /// VI: Xây dựng mảng messages cho OpenAI API.
+ ///
+ private List