diff --git a/.cursorrules b/.cursorrules
new file mode 100644
index 0000000..77511bb
--- /dev/null
+++ b/.cursorrules
@@ -0,0 +1,31 @@
+# ASP.NET Core 8 Project Rules
+
+- Use C# 12 language features where appropriate
+- Follow SOLID principles in class and interface design
+- Implement dependency injection for loose coupling
+- Use primary constructors for dependency injection in services, use cases, etc.
+- Use async/await for I/O-bound operations
+- Prefer record types for immutable data structures
+- Prefer controller endpoints over minimal APIs
+ - Utilize minimal APIs for simple endpoints (when explicitly stated or when it makes sense)
+- Implement proper exception handling and logging
+- Use strongly-typed configuration with IOptions pattern
+- Implement proper authentication and authorization
+- Use Entity Framework Core for database operations
+- Implement unit tests for business logic
+- Use integration tests for API endpoints
+- Implement proper versioning for APIs
+- Implement proper caching strategies
+- Use middleware for cross-cutting concerns
+- Implement health checks for the application
+- Use environment-specific configuration files
+- Implement proper CORS policies
+- Use secure communication with HTTPS
+- Implement proper model validation
+- Use Swagger/OpenAPI for API documentation
+- Implement proper logging with structured logging
+- Use background services for long-running tasks
+- Favor explicit typing (this is very important). Only use var when evident.
+- Make types internal and sealed by default unless otherwise specified
+- Prefer Guid for identifiers unless otherwise specified
+- Use `is null` checks instead of `== null`. The same goes for `is not null` and `!= null`.
diff --git a/.dockerignore b/.dockerignore
new file mode 100644
index 0000000..3729ff0
--- /dev/null
+++ b/.dockerignore
@@ -0,0 +1,25 @@
+**/.classpath
+**/.dockerignore
+**/.env
+**/.git
+**/.gitignore
+**/.project
+**/.settings
+**/.toolstarget
+**/.vs
+**/.vscode
+**/*.*proj.user
+**/*.dbmdl
+**/*.jfm
+**/azds.yaml
+**/bin
+**/charts
+**/docker-compose*
+**/Dockerfile*
+**/node_modules
+**/npm-debug.log
+**/obj
+**/secrets.dev.yaml
+**/values.dev.yaml
+LICENSE
+README.md
\ No newline at end of file
diff --git a/.editorconfig b/.editorconfig
new file mode 100644
index 0000000..f773573
--- /dev/null
+++ b/.editorconfig
@@ -0,0 +1,413 @@
+root = true
+
+# C# files
+[*.cs]
+
+#### Core EditorConfig Options ####
+
+# Indentation and spacing
+indent_size = 4
+indent_style = space
+tab_width = 4
+
+# New line preferences
+end_of_line = crlf
+insert_final_newline = true
+
+#### .NET Coding Conventions ####
+
+# Organize usings
+dotnet_separate_import_directive_groups = false
+dotnet_sort_system_directives_first = true
+
+# this. and Me. preferences
+dotnet_style_qualification_for_event = false:error
+dotnet_style_qualification_for_field = false:error
+dotnet_style_qualification_for_method = false:error
+dotnet_style_qualification_for_property = false:error
+
+# Language keywords vs BCL types preferences
+dotnet_style_predefined_type_for_locals_parameters_members = true:error
+dotnet_style_predefined_type_for_member_access = true:error
+
+# Parentheses preferences
+dotnet_style_parentheses_in_arithmetic_binary_operators = never_if_unnecessary:error
+dotnet_style_parentheses_in_other_binary_operators = never_if_unnecessary:error
+dotnet_style_parentheses_in_other_operators = never_if_unnecessary:error
+dotnet_style_parentheses_in_relational_binary_operators = never_if_unnecessary:error
+
+# Modifier preferences
+dotnet_style_require_accessibility_modifiers = for_non_interface_members:error
+
+# Expression-level preferences
+dotnet_style_coalesce_expression = true:error
+dotnet_style_collection_initializer = true:error
+dotnet_style_explicit_tuple_names = true:error
+dotnet_style_null_propagation = true:error
+dotnet_style_object_initializer = true:error
+dotnet_style_prefer_auto_properties = true:warning
+dotnet_style_prefer_compound_assignment = true:error
+dotnet_style_prefer_conditional_expression_over_assignment = true:error
+dotnet_style_prefer_conditional_expression_over_return = true:none
+dotnet_style_prefer_inferred_anonymous_type_member_names = true:error
+dotnet_style_prefer_inferred_tuple_names = true:error
+dotnet_style_prefer_is_null_check_over_reference_equality_method = true:error
+csharp_indent_labels = one_less_than_current
+csharp_using_directive_placement = outside_namespace:error
+csharp_prefer_simple_using_statement = true:error
+csharp_prefer_braces = true:error
+csharp_style_namespace_declarations = file_scoped:error
+csharp_style_prefer_method_group_conversion = true:silent
+csharp_style_prefer_top_level_statements = true:silent
+csharp_style_prefer_primary_constructors = true:none
+csharp_style_expression_bodied_methods = false:silent
+csharp_style_expression_bodied_constructors = false:silent
+csharp_style_expression_bodied_operators = true:error
+csharp_style_expression_bodied_properties = true:error
+csharp_style_expression_bodied_indexers = true:error
+csharp_style_expression_bodied_accessors = true:error
+csharp_style_expression_bodied_lambdas = true:error
+csharp_style_expression_bodied_local_functions = true:error
+
+[*.{cs,vb}]
+dotnet_style_prefer_simplified_boolean_expressions = true:suggestion
+dotnet_style_prefer_simplified_interpolation = true:suggestion
+dotnet_style_namespace_match_folder = true:suggestion
+
+# Field preferences
+dotnet_style_readonly_field = true:error
+
+# Parameter preferences
+dotnet_code_quality_unused_parameters = all:none
+
+#### C# Coding Conventions ####
+
+# Namespace preferences
+csharp_style_namespace_declarations= file_scoped:error
+
+# var preferences
+csharp_style_var_elsewhere = false:warning
+csharp_style_var_for_built_in_types = true:warning
+csharp_style_var_when_type_is_apparent = true:warning
+
+# Expression-bodied members
+csharp_style_expression_bodied_accessors = true:error
+csharp_style_expression_bodied_constructors = false:silent
+csharp_style_expression_bodied_indexers = true:error
+csharp_style_expression_bodied_lambdas = true:error
+csharp_style_expression_bodied_local_functions = true:error
+csharp_style_expression_bodied_methods = false:silent
+csharp_style_expression_bodied_operators = true:error
+csharp_style_expression_bodied_properties = true:error
+
+# Pattern matching preferences
+csharp_style_pattern_matching_over_as_with_null_check = true:error
+csharp_style_pattern_matching_over_is_with_cast_check = true:error
+csharp_style_prefer_switch_expression = true:error
+
+# Null-checking preferences
+csharp_style_conditional_delegate_call = true:error
+
+# Modifier preferences
+csharp_prefer_static_local_function = true:error
+csharp_preferred_modifier_order = public,private,protected,internal,static,extern,new,virtual,abstract,sealed,override,readonly,unsafe,volatile,async
+
+# Code-block preferences
+csharp_prefer_braces = true:error
+csharp_prefer_simple_using_statement = true:error
+
+# Expression-level preferences
+csharp_prefer_simple_default_expression = true:error
+csharp_style_deconstructed_variable_declaration = true:suggestion
+csharp_style_inlined_variable_declaration = true:error
+csharp_style_pattern_local_over_anonymous_function = true:error
+csharp_style_prefer_index_operator = true:suggestion
+csharp_style_prefer_range_operator = true:suggestion
+csharp_style_throw_expression = true:suggestion
+csharp_style_unused_value_assignment_preference = discard_variable:silent
+csharp_style_unused_value_expression_statement_preference = discard_variable:none
+csharp_style_prefer_method_group_conversion = true:silent
+csharp_style_prefer_top_level_statements = true:silent
+
+# 'using' directive preferences
+csharp_using_directive_placement = outside_namespace:error
+
+#### C# Formatting Rules ####
+
+# New line preferences
+csharp_new_line_before_catch =true
+csharp_new_line_before_else =true
+csharp_new_line_before_finally =true
+csharp_new_line_before_members_in_anonymous_types = false,
+csharp_new_line_before_members_in_object_initializers = false,
+csharp_new_line_before_open_brace = all
+csharp_new_line_between_query_expression_clauses = true
+
+# Indentation preferences
+csharp_indent_block_contents = true
+csharp_indent_braces = false
+csharp_indent_case_contents = true
+csharp_indent_case_contents_when_block = true
+csharp_indent_labels = one_less_than_current
+csharp_indent_switch_labels = true
+
+# Space preferences
+csharp_space_after_cast = false
+csharp_space_after_colon_in_inheritance_clause = true
+csharp_space_after_comma = true
+csharp_space_after_dot = false
+csharp_space_after_keywords_in_control_flow_statements = true
+csharp_space_after_semicolon_in_for_statement = true
+csharp_space_around_binary_operators = before_and_after
+csharp_space_around_declaration_statements = false
+csharp_space_before_colon_in_inheritance_clause = true
+csharp_space_before_comma = false
+csharp_space_before_dot = false
+csharp_space_before_open_square_brackets = false
+csharp_space_before_semicolon_in_for_statement = false
+csharp_space_between_empty_square_brackets = false
+csharp_space_between_method_call_empty_parameter_list_parentheses = false
+csharp_space_between_method_call_name_and_opening_parenthesis = false
+csharp_space_between_method_call_parameter_list_parentheses = false
+csharp_space_between_method_declaration_empty_parameter_list_parentheses = false
+csharp_space_between_method_declaration_name_and_open_parenthesis = false
+csharp_space_between_method_declaration_parameter_list_parentheses = false
+csharp_space_between_parentheses = false
+csharp_space_between_square_brackets = false
+
+# Wrapping preferences
+csharp_preserve_single_line_blocks = true
+csharp_preserve_single_line_statements = false
+
+#### Naming styles ####
+
+# Naming rules
+
+dotnet_naming_rule.interface_should_be_begins_with_i.severity = suggestion
+dotnet_naming_rule.interface_should_be_begins_with_i.symbols = interface
+dotnet_naming_rule.interface_should_be_begins_with_i.style = begins_with_i
+
+dotnet_naming_rule.types_should_be_pascal_case.severity = suggestion
+dotnet_naming_rule.types_should_be_pascal_case.symbols = types
+dotnet_naming_rule.types_should_be_pascal_case.style = pascal_case
+
+dotnet_naming_rule.non_field_members_should_be_pascal_case.severity = suggestion
+dotnet_naming_rule.non_field_members_should_be_pascal_case.symbols = non_field_members
+dotnet_naming_rule.non_field_members_should_be_pascal_case.style = pascal_case
+
+# Symbol specifications
+
+dotnet_naming_symbols.interface.applicable_kinds = interface
+dotnet_naming_symbols.interface.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected
+dotnet_naming_symbols.interface.required_modifiers =
+
+dotnet_naming_symbols.types.applicable_kinds = class, struct, interface, enum
+dotnet_naming_symbols.types.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected
+dotnet_naming_symbols.types.required_modifiers =
+
+dotnet_naming_symbols.non_field_members.applicable_kinds = property, event, method
+dotnet_naming_symbols.non_field_members.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected
+dotnet_naming_symbols.non_field_members.required_modifiers =
+
+# Naming styles
+
+dotnet_naming_style.pascal_case.required_prefix =
+dotnet_naming_style.pascal_case.required_suffix =
+dotnet_naming_style.pascal_case.word_separator =
+dotnet_naming_style.pascal_case.capitalization = pascal_case
+
+dotnet_naming_style.begins_with_i.required_prefix = I
+dotnet_naming_style.begins_with_i.required_suffix =
+dotnet_naming_style.begins_with_i.word_separator =
+dotnet_naming_style.begins_with_i.capitalization = pascal_case
+
+# Custom Rules - configure these as required
+
+# .NET Code Analyzers rules
+
+# CA1000: Do not declare static members on generic types
+dotnet_diagnostic.CA1000.severity = none
+
+# CA1002: Do not expose generic lists
+dotnet_diagnostic.CA1002.severity = none
+
+# CA1008: Enums should have zero value
+dotnet_diagnostic.CA1008.severity = none
+
+# CA1019: Define accessors for attribute arguments
+dotnet_diagnostic.CA1019.severity = none
+
+# CA1024: Use properties where appropriate
+dotnet_diagnostic.CA1024.severity = none
+
+# CA1030: Use events where appropriate
+dotnet_diagnostic.CA1030.severity = none
+
+# CA1031: Do not catch general exception types
+dotnet_diagnostic.CA1031.severity = none
+
+# CA1032: Implement standard exception constructors
+dotnet_diagnostic.CA1032.severity = none
+
+# CA1034: Nested types should not be visible
+dotnet_diagnostic.CA1034.severity = none
+
+# CA1040: Avoid empty interfaces
+dotnet_diagnostic.CA1040.severity = none
+
+# CA1051: Do not declare visible instance fields
+dotnet_diagnostic.CA1051.severity = none
+
+# CA1056: URI-like properties should not be strings
+dotnet_diagnostic.CA1056.severity = none
+
+# CA1062: Validate arguments of public methods
+dotnet_diagnostic.CA1062.severity = none
+
+# CA1063: Implement IDisposable Correctly
+dotnet_diagnostic.CA1063.severity = none
+
+# CA1307: Specify StringComparison for clarity
+dotnet_diagnostic.CA1307.severity = none
+
+# CA1515: Consider making public types internal
+dotnet_diagnostic.CA1515.severity = none
+
+# CA1700: Do not name enum values 'Reserved'
+dotnet_diagnostic.CA1700.severity = none
+
+# CA1707: Identifiers should not contain underscores
+dotnet_diagnostic.CA1707.severity = none
+
+# CA1711: Identifiers should not have incorrect suffix
+dotnet_diagnostic.CA1711.severity = none
+
+# CA1716: Identifiers should not match keywords
+dotnet_diagnostic.CA1716.severity = none
+
+# CA1724: Type names should not match namespaces
+dotnet_diagnostic.CA1724.severity = none
+
+# CA1725: Parameter names should match base declaration
+dotnet_diagnostic.CA1725.severity = none
+
+# CA1812: Avoid uninstantiated internal classes
+dotnet_diagnostic.CA1812.severity = none
+
+# CA1816: Dispose methods should call SuppressFinalize
+dotnet_diagnostic.CA1816.severity = none
+
+# CA1819: Properties should not return arrays
+dotnet_diagnostic.CA1819.severity = none
+
+# CA1822: Mark members as static
+dotnet_diagnostic.CA1822.severity = none
+
+# CA1848: Use the LoggerMessage delegates
+dotnet_diagnostic.CA1848.severity = none
+
+# CA1860: Avoid using 'Enumerable.Any()' extension method
+dotnet_diagnostic.CA1860.severity = none
+
+# CA2007: Consider calling ConfigureAwait on the awaited task
+dotnet_diagnostic.CA2007.severity = none
+
+# CA2201: Do not raise reserved exception types
+dotnet_diagnostic.CA2201.severity = none
+
+# CA2211: Non-constant fields should not be visible
+dotnet_diagnostic.CA2211.severity = none
+
+# CA2213: Disposable fields should be disposed
+dotnet_diagnostic.CA2213.severity = none
+
+# CA2225: Operator overloads have named alternates
+dotnet_diagnostic.CA2225.severity = none
+
+# CA2227: Collection properties should be read only
+dotnet_diagnostic.CA2227.severity = none
+
+# CA2234: Pass system uri objects instead of strings
+dotnet_diagnostic.CA2234.severity = none
+
+# CA2326: Do not use TypeNameHandling values other than None
+dotnet_diagnostic.CA2326.severity = none
+
+# CA2326: Do not use insecure JsonSerializerSettings
+dotnet_diagnostic.CA2327.severity = none
+
+# CS8600: Converting null literal or possible null value to non-nullable type.
+dotnet_diagnostic.CS8600.severity = none
+
+# CS8603: Possible null reference return.
+dotnet_diagnostic.CS8603.severity = none
+
+# CS8618: Non-nullable field must contain a non-null value when exiting constructor. Consider declaring as nullable.
+dotnet_diagnostic.CS8618.severity = none
+
+# IDE Code Analyzers rules
+
+# IDE0005
+dotnet_diagnostic.IDE0005.severity = none
+
+# IDE0008: Use explicit type instead of 'var'
+dotnet_diagnostic.IDE0008.severity = none
+
+# IDE0046: Convert to conditional expression
+dotnet_diagnostic.IDE0046.severity = none
+
+# IDE0058: Expression value is never used
+dotnet_diagnostic.IDE0058.severity = none
+
+# IDE0060: Remove unused parameter
+dotnet_diagnostic.IDE0060.severity = none
+
+# IDE0072: Add missing cases
+dotnet_diagnostic.IDE0072.severity = none
+
+# IDE0290: Use primary constructor
+dotnet_diagnostic.IDE0290.severity = none
+
+# SonarAnalyzer.CSharp rules
+
+# S112: General or reserved exceptions should never be thrown
+dotnet_diagnostic.S112.severity = none
+
+# S125: Sections of code should not be commented out
+dotnet_diagnostic.S125.severity = none
+
+# S1135: Track uses of "TODO" tags
+dotnet_diagnostic.S1135.severity = none
+
+# S2094: Utility classes should not have public constructors
+dotnet_diagnostic.S1118.severity = none
+
+# S2094: Classes should not be empty
+dotnet_diagnostic.S2094.severity = none
+
+# S2325: Methods and properties that don't access instance data should be static
+dotnet_diagnostic.S2325.severity = none
+
+# S2326: Generic type parameter is not used in interface
+dotnet_diagnostic.S2326.severity = none
+
+# S2365: Properties should not make collection or array copies
+dotnet_diagnostic.S2365.severity = none
+
+# S3267: Loops should be simplified with "LINQ" expressions
+dotnet_diagnostic.S3267.severity = none
+
+# S3881: "IDisposable" should be implemented correctly
+dotnet_diagnostic.S3881.severity = none
+
+# S4136: Method overloads should be grouped together
+dotnet_diagnostic.S4136.severity = none
+
+# S4158: Empty collections should not be accessed or iterated
+dotnet_diagnostic.S4158.severity = none
+
+# S6605: Collection-specific "Exists" method should be used instead of the "Any" extension
+dotnet_diagnostic.S6605.severity = none
+
+# S6781: JWT secret keys should not be disclosed
+dotnet_diagnostic.S6781.severity = none
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..6b9ae06
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,400 @@
+## Ignore Visual Studio temporary files, build results, and
+## files generated by popular Visual Studio add-ons.
+##
+## Get latest from https://github.com/github/gitignore/blob/main/VisualStudio.gitignore
+
+# User-specific files
+*.rsuser
+*.suo
+*.user
+*.userosscache
+*.sln.docstates
+
+# User-specific files (MonoDevelop/Xamarin Studio)
+*.userprefs
+
+# Mono auto generated files
+mono_crash.*
+
+# Build results
+[Dd]ebug/
+[Dd]ebugPublic/
+[Rr]elease/
+[Rr]eleases/
+x64/
+x86/
+[Ww][Ii][Nn]32/
+[Aa][Rr][Mm]/
+[Aa][Rr][Mm]64/
+bld/
+[Bb]in/
+[Oo]bj/
+[Ll]og/
+[Ll]ogs/
+
+# Visual Studio 2015/2017 cache/options directory
+.vs/
+# Uncomment if you have tasks that create the project's static files in wwwroot
+#wwwroot/
+
+# Visual Studio 2017 auto generated files
+Generated\ Files/
+
+# MSTest test Results
+[Tt]est[Rr]esult*/
+[Bb]uild[Ll]og.*
+
+# NUnit
+*.VisualState.xml
+TestResult.xml
+nunit-*.xml
+
+# Build Results of an ATL Project
+[Dd]ebugPS/
+[Rr]eleasePS/
+dlldata.c
+
+# Benchmark Results
+BenchmarkDotNet.Artifacts/
+
+# .NET Core
+project.lock.json
+project.fragment.lock.json
+artifacts/
+
+# ASP.NET Scaffolding
+ScaffoldingReadMe.txt
+
+# StyleCop
+StyleCopReport.xml
+
+# Files built by Visual Studio
+*_i.c
+*_p.c
+*_h.h
+*.ilk
+*.meta
+*.obj
+*.iobj
+*.pch
+*.pdb
+*.ipdb
+*.pgc
+*.pgd
+*.rsp
+*.sbr
+*.tlb
+*.tli
+*.tlh
+*.tmp
+*.tmp_proj
+*_wpftmp.csproj
+*.log
+*.tlog
+*.vspscc
+*.vssscc
+.builds
+*.pidb
+*.svclog
+*.scc
+
+# Chutzpah Test files
+_Chutzpah*
+
+# Visual C++ cache files
+ipch/
+*.aps
+*.ncb
+*.opendb
+*.opensdf
+*.sdf
+*.cachefile
+*.VC.db
+*.VC.VC.opendb
+
+# Visual Studio profiler
+*.psess
+*.vsp
+*.vspx
+*.sap
+
+# Visual Studio Trace Files
+*.e2e
+
+# TFS 2012 Local Workspace
+$tf/
+
+# Guidance Automation Toolkit
+*.gpState
+
+# ReSharper is a .NET coding add-in
+_ReSharper*/
+*.[Rr]e[Ss]harper
+*.DotSettings.user
+
+# TeamCity is a build add-in
+_TeamCity*
+
+# DotCover is a Code Coverage Tool
+*.dotCover
+
+# AxoCover is a Code Coverage Tool
+.axoCover/*
+!.axoCover/settings.json
+
+# Coverlet is a free, cross platform Code Coverage Tool
+coverage*.json
+coverage*.xml
+coverage*.info
+
+# Visual Studio code coverage results
+*.coverage
+*.coveragexml
+
+# NCrunch
+_NCrunch_*
+.*crunch*.local.xml
+nCrunchTemp_*
+
+# MightyMoose
+*.mm.*
+AutoTest.Net/
+
+# Web workbench (sass)
+.sass-cache/
+
+# Installshield output folder
+[Ee]xpress/
+
+# DocProject is a documentation generator add-in
+DocProject/buildhelp/
+DocProject/Help/*.HxT
+DocProject/Help/*.HxC
+DocProject/Help/*.hhc
+DocProject/Help/*.hhk
+DocProject/Help/*.hhp
+DocProject/Help/Html2
+DocProject/Help/html
+
+# Click-Once directory
+publish/
+
+# Publish Web Output
+*.[Pp]ublish.xml
+*.azurePubxml
+# Note: Comment the next line if you want to checkin your web deploy settings,
+# but database connection strings (with potential passwords) will be unencrypted
+*.pubxml
+*.publishproj
+
+# Microsoft Azure Web App publish settings. Comment the next line if you want to
+# checkin your Azure Web App publish settings, but sensitive information contained
+# in these scripts will be unencrypted
+PublishScripts/
+
+# NuGet Packages
+*.nupkg
+# NuGet Symbol Packages
+*.snupkg
+# The packages folder can be ignored because of Package Restore
+**/[Pp]ackages/*
+# except build/, which is used as an MSBuild target.
+!**/[Pp]ackages/build/
+# Uncomment if necessary however generally it will be regenerated when needed
+#!**/[Pp]ackages/repositories.config
+# NuGet v3's project.json files produces more ignorable files
+*.nuget.props
+*.nuget.targets
+
+# Microsoft Azure Build Output
+csx/
+*.build.csdef
+
+# Microsoft Azure Emulator
+ecf/
+rcf/
+
+# Windows Store app package directories and files
+AppPackages/
+BundleArtifacts/
+Package.StoreAssociation.xml
+_pkginfo.txt
+*.appx
+*.appxbundle
+*.appxupload
+
+# Visual Studio cache files
+# files ending in .cache can be ignored
+*.[Cc]ache
+# but keep track of directories ending in .cache
+!?*.[Cc]ache/
+
+# Others
+ClientBin/
+~$*
+*~
+*.dbmdl
+*.dbproj.schemaview
+*.jfm
+*.pfx
+*.publishsettings
+orleans.codegen.cs
+
+# Including strong name files can present a security risk
+# (https://github.com/github/gitignore/pull/2483#issue-259490424)
+#*.snk
+
+# Since there are multiple workflows, uncomment next line to ignore bower_components
+# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622)
+#bower_components/
+
+# RIA/Silverlight projects
+Generated_Code/
+
+# Backup & report files from converting an old project file
+# to a newer Visual Studio version. Backup files are not needed,
+# because we have git ;-)
+_UpgradeReport_Files/
+Backup*/
+UpgradeLog*.XML
+UpgradeLog*.htm
+ServiceFabricBackup/
+*.rptproj.bak
+
+# SQL Server files
+*.mdf
+*.ldf
+*.ndf
+
+# Business Intelligence projects
+*.rdl.data
+*.bim.layout
+*.bim_*.settings
+*.rptproj.rsuser
+*- [Bb]ackup.rdl
+*- [Bb]ackup ([0-9]).rdl
+*- [Bb]ackup ([0-9][0-9]).rdl
+
+# Microsoft Fakes
+FakesAssemblies/
+
+# GhostDoc plugin setting file
+*.GhostDoc.xml
+
+# Node.js Tools for Visual Studio
+.ntvs_analysis.dat
+node_modules/
+
+# Visual Studio 6 build log
+*.plg
+
+# Visual Studio 6 workspace options file
+*.opt
+
+# Visual Studio 6 auto-generated workspace file (contains which files were open etc.)
+*.vbw
+
+# Visual Studio 6 auto-generated project file (contains which files were open etc.)
+*.vbp
+
+# Visual Studio 6 workspace and project file (working project files containing files to include in project)
+*.dsw
+*.dsp
+
+# Visual Studio 6 technical files
+*.ncb
+*.aps
+
+# Visual Studio LightSwitch build output
+**/*.HTMLClient/GeneratedArtifacts
+**/*.DesktopClient/GeneratedArtifacts
+**/*.DesktopClient/ModelManifest.xml
+**/*.Server/GeneratedArtifacts
+**/*.Server/ModelManifest.xml
+_Pvt_Extensions
+
+# Paket dependency manager
+.paket/paket.exe
+paket-files/
+
+# FAKE - F# Make
+.fake/
+
+# CodeRush personal settings
+.cr/personal
+
+# Python Tools for Visual Studio (PTVS)
+__pycache__/
+*.pyc
+
+# Cake - Uncomment if you are using it
+# tools/**
+# !tools/packages.config
+
+# Tabs Studio
+*.tss
+
+# Telerik's JustMock configuration file
+*.jmconfig
+
+# BizTalk build output
+*.btp.cs
+*.btm.cs
+*.odx.cs
+*.xsd.cs
+
+# OpenCover UI analysis results
+OpenCover/
+
+# Azure Stream Analytics local run output
+ASALocalRun/
+
+# MSBuild Binary and Structured Log
+*.binlog
+
+# NVidia Nsight GPU debugger configuration file
+*.nvuser
+
+# MFractors (Xamarin productivity tool) working folder
+.mfractor/
+
+# Local History for Visual Studio
+.localhistory/
+
+# Visual Studio History (VSHistory) files
+.vshistory/
+
+# BeatPulse healthcheck temp database
+healthchecksdb
+
+# Backup folder for Package Reference Convert tool in Visual Studio 2017
+MigrationBackup/
+
+# Ionide (cross platform F# VS Code tools) working folder
+.ionide/
+
+# Fody - auto-generated XML schema
+FodyWeavers.xsd
+
+# VS Code files for those working on multiple tools
+.vscode/*
+!.vscode/settings.json
+!.vscode/tasks.json
+!.vscode/launch.json
+!.vscode/extensions.json
+*.code-workspace
+
+# Local History for Visual Studio Code
+.history/
+
+# Windows Installer files from build outputs
+*.cab
+*.msi
+*.msix
+*.msm
+*.msp
+
+# JetBrains Rider
+*.sln.iml
+
+.containers/
diff --git a/Directory.Build.props b/Directory.Build.props
new file mode 100644
index 0000000..f588c79
--- /dev/null
+++ b/Directory.Build.props
@@ -0,0 +1,23 @@
+
+
+
+ net8.0
+
+
+ enable
+ enable
+
+ latest
+ All
+ true
+ true
+ true
+
+
+
\ No newline at end of file
diff --git a/LegalAssistant.AppService.sln b/LegalAssistant.AppService.sln
new file mode 100644
index 0000000..98e4dbb
--- /dev/null
+++ b/LegalAssistant.AppService.sln
@@ -0,0 +1,48 @@
+
+Microsoft Visual Studio Solution File, Format Version 12.00
+# Visual Studio Version 17
+VisualStudioVersion = 17.0.31903.59
+MinimumVisualStudioVersion = 10.0.40219.1
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{EDE5F5E1-80F9-4DCE-80B0-BFDBF36DC680}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Application", "src\Application\Application.csproj", "{44042BE8-3AF1-45B5-8719-2AFFDA30CF41}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Infrastructure", "src\Infrastructure\Infrastructure.csproj", "{55A6D295-C238-464F-9729-3E9E9B4972B5}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Web.Api", "src\Web.Api\Web.Api.csproj", "{BB7A6132-5274-490D-A502-FCA2CF51BA94}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Domain", "src\Domain\Domain.csproj", "{0DA30A4D-944B-49F4-AEE5-E89DF7B7EC9A}"
+EndProject
+Global
+ GlobalSection(SolutionConfigurationPlatforms) = preSolution
+ Debug|Any CPU = Debug|Any CPU
+ Release|Any CPU = Release|Any CPU
+ EndGlobalSection
+ GlobalSection(SolutionProperties) = preSolution
+ HideSolutionNode = FALSE
+ EndGlobalSection
+ GlobalSection(ProjectConfigurationPlatforms) = postSolution
+ {44042BE8-3AF1-45B5-8719-2AFFDA30CF41}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {44042BE8-3AF1-45B5-8719-2AFFDA30CF41}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {44042BE8-3AF1-45B5-8719-2AFFDA30CF41}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {44042BE8-3AF1-45B5-8719-2AFFDA30CF41}.Release|Any CPU.Build.0 = Release|Any CPU
+ {55A6D295-C238-464F-9729-3E9E9B4972B5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {55A6D295-C238-464F-9729-3E9E9B4972B5}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {55A6D295-C238-464F-9729-3E9E9B4972B5}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {55A6D295-C238-464F-9729-3E9E9B4972B5}.Release|Any CPU.Build.0 = Release|Any CPU
+ {BB7A6132-5274-490D-A502-FCA2CF51BA94}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {BB7A6132-5274-490D-A502-FCA2CF51BA94}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {BB7A6132-5274-490D-A502-FCA2CF51BA94}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {BB7A6132-5274-490D-A502-FCA2CF51BA94}.Release|Any CPU.Build.0 = Release|Any CPU
+ {0DA30A4D-944B-49F4-AEE5-E89DF7B7EC9A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {0DA30A4D-944B-49F4-AEE5-E89DF7B7EC9A}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {0DA30A4D-944B-49F4-AEE5-E89DF7B7EC9A}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {0DA30A4D-944B-49F4-AEE5-E89DF7B7EC9A}.Release|Any CPU.Build.0 = Release|Any CPU
+ EndGlobalSection
+ GlobalSection(NestedProjects) = preSolution
+ {44042BE8-3AF1-45B5-8719-2AFFDA30CF41} = {EDE5F5E1-80F9-4DCE-80B0-BFDBF36DC680}
+ {55A6D295-C238-464F-9729-3E9E9B4972B5} = {EDE5F5E1-80F9-4DCE-80B0-BFDBF36DC680}
+ {BB7A6132-5274-490D-A502-FCA2CF51BA94} = {EDE5F5E1-80F9-4DCE-80B0-BFDBF36DC680}
+ {0DA30A4D-944B-49F4-AEE5-E89DF7B7EC9A} = {EDE5F5E1-80F9-4DCE-80B0-BFDBF36DC680}
+ EndGlobalSection
+EndGlobal
diff --git a/README.md b/README.md
index 62009a5..5908f17 100644
--- a/README.md
+++ b/README.md
@@ -75,6 +75,10 @@ dotnet run --project src/Web.Api
## 📖 Detailed Documentation
+- 📚 **[Documentation Folder](docs/)** - Tài liệu chi tiết về architecture và patterns
+- 🏗️ **[Architecture Overview](docs/ARCHITECTURE_OVERVIEW.md)** - Hiểu tổng quan kiến trúc
+- 🔄 **[Event Handler Flow](docs/EVENT_HANDLER_FLOW.md)** - Cách Domain Events hoạt động
+
Xem README trong từng folder để hiểu chi tiết:
- [Domain Layer](./src/Domain/README.md)
diff --git a/docs/ARCHITECTURE_OVERVIEW.md b/docs/ARCHITECTURE_OVERVIEW.md
new file mode 100644
index 0000000..54c8a9a
--- /dev/null
+++ b/docs/ARCHITECTURE_OVERVIEW.md
@@ -0,0 +1,147 @@
+# 🎯 KIẾN TRÚC DỄ HIỂU - Legal Assistant
+
+## 🔴 **MỤC ĐÍCH CỦA TỪNG LAYER**
+
+```
+🌐 Web.Api/ ← API endpoints (controllers)
+ ↓ gọi
+🔧 Application/ ← Use cases (login, create user, etc.)
+ ↓ sử dụng
+🏛️ Domain/ ← Business rules (User, Conversation)
+ ↑ implement
+🗄️ Infrastructure/ ← Database, email, external services
+```
+
+## 🔥 **LUỒNG HOẠT ĐỘNG CƠ BẢN**
+
+### 1️⃣ **User muốn đăng ký tài khoản:**
+```
+📱 Client gửi POST /api/users
+ ↓
+🌐 UsersController.CreateUser()
+ ↓
+🔧 CreateUserCommandHandler.Handle()
+ ↓
+🏛️ UserAggregate.Create() → tạo User entity + UserCreatedEvent
+ ↓
+🗄️ UserRepository.SaveAsync() → lưu vào database
+ ↓
+📧 UserCreatedEventHandler → gửi email welcome
+```
+
+### 2️⃣ **User muốn tạo conversation:**
+```
+📱 Client gửi POST /api/conversations
+ ↓
+🌐 ConversationsController.Create()
+ ↓
+🔧 CreateConversationCommandHandler.Handle()
+ ↓
+🏛️ ConversationAggregate.Create() → tạo Conversation entity
+ ↓
+🗄️ ConversationRepository.SaveAsync()
+```
+
+## 🧩 **CÁC PATTERN ĐƯỢC SỬ DỤNG**
+
+### 🔵 **1. CQRS (Command Query Responsibility Segregation)**
+- **Commands** = Thay đổi data (Create, Update, Delete)
+- **Queries** = Đọc data (Get, List, Search)
+
+```csharp
+// Command - thay đổi data
+public record CreateUserCommand(string Email, string FullName, string Password);
+
+// Query - đọc data
+public record GetUserByIdQuery(Guid UserId);
+```
+
+### 🔵 **2. Domain Events**
+- Khi có sự kiện quan trọng → raise event → trigger side effects
+
+```csharp
+// Khi user được tạo
+UserAggregate.Create() → raises UserCreatedEvent
+ ↓
+UserCreatedEventHandler → gửi email welcome
+```
+
+### 🔵 **3. MediatR Pipeline Behaviors**
+- **Middleware** cho tất cả requests
+- Validation, Logging, Authorization, etc.
+
+```csharp
+Request → ValidationBehavior → AuthorizationBehavior → Handler → Response
+```
+
+## 📂 **FOLDERS QUAN TRỌNG NHẤT**
+
+### 🏛️ **Domain/** - Business Logic
+```
+Entities/ ← Data objects (User, Conversation, Message)
+Aggregates/ ← Business operations (UserAggregate, ConversationAggregate)
+Events/ ← Domain events (UserCreatedEvent, etc.)
+ValueObjects/ ← Value objects (Email, Password)
+```
+
+### 🔧 **Application/** - Use Cases
+```
+Features/ ← Organized by business feature
+├── Auth/ ← Login, logout, register
+├── User/ ← Create, update, get user
+└── Conversation/ ← Create, get conversations
+
+Common/
+├── Behaviors/ ← Pipeline middleware
+├── Exceptions/ ← Custom exceptions
+└── Models/ ← Shared models
+```
+
+### 🗄️ **Infrastructure/** - External Services
+```
+Data/ ← Database (Entity Framework)
+Services/ ← Email, external APIs
+Repositories/ ← Data access implementations
+```
+
+### 🌐 **Web.Api/** - HTTP Layer
+```
+Controllers/ ← API endpoints
+Middleware/ ← HTTP middleware
+Models/ ← Request/response models
+```
+
+## 🎯 **QUY TẮC DEPENDENCY**
+
+```
+Web.Api → Application → Domain
+ ↓ ↓
+Infrastructure ←――→
+```
+
+- **Domain** không depend vào ai cả
+- **Application** chỉ depend Domain
+- **Infrastructure** depend Domain + Application
+- **Web.Api** depend Application + Infrastructure
+
+## 🚀 **BƯỚC ĐẦU TIÊN ĐỂ HIỂU**
+
+1. 🏛️ Xem **Domain/Entities/** → hiểu data structure
+2. 🏛️ Xem **Domain/Aggregates/** → hiểu business logic
+3. 🔧 Xem **Application/Features/User/** → hiểu use cases
+4. 🌐 Xem **Web.Api/Controllers/** → hiểu API endpoints
+
+## 💡 **TIPS ĐỂ KHÔNG BỊ CONFUSED**
+
+✅ **Bắt đầu từ Domain Entities** → hiểu data trước
+✅ **Theo dõi 1 luồng từ đầu đến cuối** (VD: CreateUser)
+✅ **Bỏ qua các pattern phức tạp** lúc đầu (Events, Behaviors)
+✅ **Focus vào business logic** trong Aggregates
+
+❌ **Đừng cố hiểu tất cả pattern cùng lúc**
+❌ **Đừng bắt đầu từ Infrastructure**
+❌ **Đừng quan tâm Behaviors/Events lúc đầu**
+
+---
+
+**🎯 Mục tiêu: Hiểu được luồng tạo User từ API → Database**
diff --git a/docs/EVENT_HANDLER_FLOW.md b/docs/EVENT_HANDLER_FLOW.md
new file mode 100644
index 0000000..8b25537
--- /dev/null
+++ b/docs/EVENT_HANDLER_FLOW.md
@@ -0,0 +1,213 @@
+# 🔄 Event Handler - Luồng hoạt động chi tiết
+
+## 📍 **1. TỔNG QUAN LUỒNG EVENT**
+
+```
+UserAggregate.Create() → Raises Event → MediatR Dispatch → Event Handler → Side Effects
+```
+
+## 🎯 **2. VÍ DỤ CỤ THỂ: User Registration**
+
+### **Step 1: Aggregate raises Event**
+```csharp
+// Trong UserAggregate.Create()
+public static UserAggregate Create(string email, string fullName, string passwordHash, UserRole[]? roles = null)
+{
+ // Tạo user entity
+ var user = new Entities.User
+ {
+ Email = email,
+ FullName = fullName,
+ PasswordHash = passwordHash,
+ Roles = roles ?? [UserRole.User]
+ };
+
+ var aggregate = new UserAggregate(user);
+
+ // 🔥 RAISE EVENT - đây là điểm quan trọng!
+ aggregate.AddDomainEvent(new UserCreatedEvent(
+ user.Id,
+ user.Email,
+ user.FullName,
+ user.Roles.Select(r => r.ToString()).ToArray()));
+
+ return aggregate;
+}
+```
+
+### **Step 2: Event được lưu trong Aggregate**
+```csharp
+// Trong BaseAggregateRoot
+public abstract class BaseAggregateRoot : BaseEntity
+{
+ private readonly List _domainEvents = [];
+
+ protected void AddDomainEvent(IDomainEvent domainEvent)
+ {
+ _domainEvents.Add(domainEvent); // 📦 Event được lưu ở đây
+ }
+
+ public IReadOnlyCollection DomainEvents => _domainEvents.AsReadOnly();
+}
+```
+
+### **Step 3: Command Handler save Aggregate**
+```csharp
+// Trong CreateUserCommandHandler
+public async Task> Handle(CreateUserCommand request, CancellationToken cancellationToken)
+{
+ // Tạo aggregate (với events)
+ var userAggregate = UserAggregate.Create(request.Email, request.FullName, hashedPassword);
+
+ // 💾 Save aggregate - events vẫn nằm trong aggregate
+ await _userRepository.CreateAsync(userAggregate.GetUser(), cancellationToken);
+
+ return Result.Success(response);
+} // ← Command hoàn thành, giờ events sẽ được dispatch
+```
+
+### **Step 4: DomainEventBehavior dispatch Events**
+```csharp
+// Trong DomainEventBehavior (MediatR Pipeline)
+public async Task Handle(TRequest request, RequestHandlerDelegate next, CancellationToken cancellationToken)
+{
+ // 1. Execute command first
+ var response = await next(); // ← CreateUserCommandHandler chạy xong
+
+ // 2. Dispatch events after command success
+ if (request is ICommand)
+ {
+ // 🔍 Lấy tất cả entities có events từ DbContext
+ var entitiesWithEvents = await GetEntitiesWithDomainEventsAsync();
+
+ foreach (var entity in entitiesWithEvents)
+ {
+ foreach (var domainEvent in entity.DomainEvents)
+ {
+ // 📡 Dispatch từng event qua MediatR
+ await _mediator.Publish(domainEvent, cancellationToken);
+ }
+
+ // 🧹 Clear events sau khi dispatch
+ entity.ClearDomainEvents();
+ }
+ }
+
+ return response;
+}
+```
+
+### **Step 5: MediatR route Event đến Handler**
+```csharp
+// MediatR tự động tìm handler cho UserCreatedEvent
+await _mediator.Publish(userCreatedEvent, cancellationToken);
+ ↓
+// Route đến UserCreatedEventHandler.Handle()
+```
+
+### **Step 6: Event Handler thực hiện Side Effects**
+```csharp
+public async Task Handle(UserCreatedEvent notification, CancellationToken cancellationToken)
+{
+ logger.LogInformation("User created: {UserId} - {Email}",
+ notification.UserId, notification.Email);
+
+ // 🔥 SIDE EFFECTS - Những việc xảy ra khi user được tạo:
+
+ // 1. 📧 Gửi welcome email
+ await emailService.SendWelcomeEmailAsync(notification.Email, notification.FullName);
+
+ // 2. 👤 Tạo user profile mặc định
+ await profileService.CreateDefaultProfileAsync(notification.UserId);
+
+ // 3. ⚙️ Khởi tạo settings mặc định
+ await settingsService.CreateDefaultSettingsAsync(notification.UserId);
+
+ // 4. 📊 Track analytics
+ await analyticsService.TrackUserRegistrationAsync(notification.UserId);
+
+ // 5. 🔔 Gửi notification cho admin
+ await notificationService.NotifyAdminNewUserAsync(notification.UserId);
+}
+```
+
+## 🎯 **3. TẠI SAO SỬ DỤNG EVENT HANDLER?**
+
+### ✅ **ADVANTAGES:**
+
+| **Vấn đề** | **Không có Events** | **Có Events** |
+|------------|--------------------|--------------------|
+| **🔗 Coupling** | Handler phải biết tất cả side effects | Handler chỉ lo business logic |
+| **🧪 Testing** | Khó test vì nhiều dependencies | Dễ test từng handler riêng |
+| **📈 Scalability** | Thêm feature = sửa handler cũ | Thêm feature = thêm handler mới |
+| **🔄 Consistency** | Có thể quên side effects | Tự động trigger tất cả handlers |
+
+### **VD: Không có Events** ❌
+```csharp
+public async Task Handle(CreateUserCommand request, CancellationToken cancellationToken)
+{
+ // Tạo user
+ var user = UserAggregate.Create(...);
+ await _userRepository.CreateAsync(user);
+
+ // 😰 Phải nhớ làm tất cả side effects trong 1 handler
+ await _emailService.SendWelcomeEmailAsync(...); // Email
+ await _profileService.CreateDefaultProfileAsync(...); // Profile
+ await _settingsService.CreateDefaultSettingsAsync(...); // Settings
+ await _analyticsService.TrackUserRegistrationAsync(...); // Analytics
+ await _notificationService.NotifyAdminNewUserAsync(...); // Notification
+
+ // 🤢 Handler trở nên phức tạp và khó maintain
+ return Result.Success();
+}
+```
+
+### **VD: Có Events** ✅
+```csharp
+// CreateUserCommandHandler - CHỈ LO BUSINESS LOGIC
+public async Task Handle(CreateUserCommand request, CancellationToken cancellationToken)
+{
+ var user = UserAggregate.Create(...); // ← Automatically raises UserCreatedEvent
+ await _userRepository.CreateAsync(user);
+ return Result.Success();
+} // ← Events tự động được dispatch
+
+// UserCreatedEventHandler - LO SIDE EFFECTS
+public async Task Handle(UserCreatedEvent notification, CancellationToken cancellationToken)
+{
+ await _emailService.SendWelcomeEmailAsync(...);
+}
+
+// UserProfileEventHandler - LO PROFILE
+public async Task Handle(UserCreatedEvent notification, CancellationToken cancellationToken)
+{
+ await _profileService.CreateDefaultProfileAsync(...);
+}
+```
+
+## 🚀 **4. MULTIPLE EVENT HANDLERS**
+
+**1 Event có thể có nhiều handlers:**
+
+```csharp
+// UserCreatedEvent sẽ trigger TẤT CẢ handlers này:
+
+UserCreatedEventHandler → Gửi email welcome
+UserProfileEventHandler → Tạo profile
+UserSettingsEventHandler → Tạo settings
+UserAnalyticsEventHandler → Track analytics
+UserNotificationEventHandler → Notify admin
+```
+
+**MediatR tự động gọi TẤT CẢ handlers song song!**
+
+## 💡 **5. KẾT LUẬN**
+
+Event Handler = **"Khi X xảy ra, hãy làm Y"**
+
+- **Decoupling**: Business logic tách biệt khỏi side effects
+- **Extensible**: Dễ thêm features mới
+- **Testable**: Test từng handler độc lập
+- **Reliable**: Không bao giờ quên side effects
+
+**🎯 Đây là pattern rất mạnh trong microservices và domain-driven design!**
diff --git a/docs/README.md b/docs/README.md
new file mode 100644
index 0000000..1f8ab2e
--- /dev/null
+++ b/docs/README.md
@@ -0,0 +1,57 @@
+# 📚 Documentation - Legal Assistant
+
+**Tài liệu hướng dẫn hiểu và phát triển Legal Assistant Application**
+
+## 📖 **Danh sách tài liệu**
+
+### 🏗️ **1. ARCHITECTURE_OVERVIEW.md**
+- **🎯 Mục đích**: Hiểu tổng quan kiến trúc Clean Architecture
+- **👥 Đối tượng**: Developer mới, người muốn hiểu project structure
+- **📋 Nội dung**:
+ - Dependency flow giữa các layers
+ - Mục đích từng folder
+ - Patterns được sử dụng (CQRS, DDD, Events)
+ - Quy tắc coding và best practices
+
+### 🔄 **2. EVENT_HANDLER_FLOW.md**
+- **🎯 Mục đích**: Hiểu cách Domain Events hoạt động
+- **👥 Đối tượng**: Developer cần hiểu Event-Driven Architecture
+- **📋 Nội dung**:
+ - Luồng hoạt động từ Event creation → Handler execution
+ - So sánh có/không có Events
+ - Multiple Event Handlers
+ - Side effects và decoupling
+
+## 🚀 **Cách sử dụng tài liệu**
+
+### **🆕 Người mới:**
+1. 📖 Đọc **ARCHITECTURE_OVERVIEW.md** trước
+2. 🔍 Trace 1 luồng CreateUser từ đầu đến cuối
+3. 📚 Đọc **EVENT_HANDLER_FLOW.md** để hiểu Events
+
+### **🔧 Developer có kinh nghiệm:**
+1. 🏗️ Review **ARCHITECTURE_OVERVIEW.md** để hiểu patterns
+2. 🔄 Đọc **EVENT_HANDLER_FLOW.md** để implement Events
+
+### **👨💼 Tech Lead/Architect:**
+1. 📊 Review cả 2 docs để hiểu design decisions
+2. 🎯 Sử dụng làm onboarding material cho team
+
+## 🧭 **Navigation Map**
+
+```
+📁 docs/
+├── 📖 README.md ← Bạn đang ở đây
+├── 🏗️ ARCHITECTURE_OVERVIEW.md ← Bắt đầu từ đây
+└── 🔄 EVENT_HANDLER_FLOW.md ← Sau khi hiểu kiến trúc
+```
+
+## 📞 **Hỗ trợ**
+
+- **❓ Có thắc mắc về architecture**: Xem ARCHITECTURE_OVERVIEW.md
+- **🔄 Không hiểu Events**: Xem EVENT_HANDLER_FLOW.md
+- **🆘 Vẫn chưa rõ**: Hỏi team hoặc tạo issue
+
+---
+
+**🎯 Mục tiêu: Giúp mọi người hiểu và contribute vào project một cách hiệu quả!**
diff --git a/src/Application/Application.csproj b/src/Application/Application.csproj
new file mode 100644
index 0000000..d650da5
--- /dev/null
+++ b/src/Application/Application.csproj
@@ -0,0 +1,20 @@
+
+
+
+ net8.0
+ enable
+ enable
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/Application/Common/Behaviors/AuthorizationBehavior.cs b/src/Application/Common/Behaviors/AuthorizationBehavior.cs
new file mode 100644
index 0000000..90ce6f0
--- /dev/null
+++ b/src/Application/Common/Behaviors/AuthorizationBehavior.cs
@@ -0,0 +1,126 @@
+using Application.Common.Exceptions;
+using MediatR;
+using Microsoft.Extensions.Logging;
+
+namespace Application.Common.Behaviors;
+
+///
+/// Authorization requirement
+///
+public sealed record AuthorizationRequirement
+{
+ ///
+ /// Required roles (user must have at least one)
+ ///
+ public string[] Roles { get; init; } = [];
+
+ ///
+ /// Required permissions (user must have all)
+ ///
+ public string[] Permissions { get; init; } = [];
+
+ ///
+ /// Require authentication (default true)
+ ///
+ public bool RequireAuthentication { get; init; } = true;
+}
+
+///
+/// Marker interface for requests that require authorization
+///
+public interface IAuthorizedRequest
+{
+ ///
+ /// Authorization requirements
+ ///
+ AuthorizationRequirement AuthorizationRequirement { get; }
+}
+
+///
+/// Current user information (injected from HTTP context)
+///
+public interface ICurrentUser
+{
+ ///
+ /// Current user ID
+ ///
+ Guid? UserId { get; }
+
+ ///
+ /// Is user authenticated
+ ///
+ bool IsAuthenticated { get; }
+
+ ///
+ /// User roles
+ ///
+ string[] Roles { get; }
+
+ ///
+ /// User permissions
+ ///
+ string[] Permissions { get; }
+}
+
+///
+/// Authorization behavior for MediatR pipeline
+///
+/// Request type
+/// Response type
+public sealed class AuthorizationBehavior(
+ ICurrentUser currentUser,
+ ILogger> logger)
+ : IPipelineBehavior
+ where TRequest : notnull
+{
+ public async Task Handle(TRequest request, RequestHandlerDelegate next, CancellationToken cancellationToken)
+ {
+ if (request is not IAuthorizedRequest authorizedRequest)
+ {
+ return await next();
+ }
+
+ var requirement = authorizedRequest.AuthorizationRequirement;
+
+ // Check authentication
+ if (requirement.RequireAuthentication && !currentUser.IsAuthenticated)
+ {
+ logger.LogWarning("Unauthorized access attempt for {RequestName}", typeof(TRequest).Name);
+ throw new UnauthorizedException("Authentication required");
+ }
+
+ // Check roles
+ if (requirement.Roles.Length > 0)
+ {
+ var hasRequiredRole = requirement.Roles.Any(role =>
+ currentUser.Roles.Contains(role, StringComparer.OrdinalIgnoreCase));
+
+ if (!hasRequiredRole)
+ {
+ logger.LogWarning("Access denied for user {UserId} to {RequestName}. Required roles: {RequiredRoles}, User roles: {UserRoles}",
+ currentUser.UserId, typeof(TRequest).Name, requirement.Roles, currentUser.Roles);
+ throw new ForbiddenException($"Required roles: {string.Join(", ", requirement.Roles)}");
+ }
+ }
+
+ // Check permissions
+ if (requirement.Permissions.Length > 0)
+ {
+ var hasAllPermissions = requirement.Permissions.All(permission =>
+ currentUser.Permissions.Contains(permission, StringComparer.OrdinalIgnoreCase));
+
+ if (!hasAllPermissions)
+ {
+ var missingPermissions = requirement.Permissions.Except(currentUser.Permissions).ToArray();
+ logger.LogWarning("Access denied for user {UserId} to {RequestName}. Missing permissions: {MissingPermissions}",
+ currentUser.UserId, typeof(TRequest).Name, missingPermissions);
+ throw new ForbiddenException($"Missing permissions: {string.Join(", ", missingPermissions)}");
+ }
+ }
+
+ logger.LogDebug("Authorization passed for user {UserId} to {RequestName}",
+ currentUser.UserId, typeof(TRequest).Name);
+
+ return await next();
+ }
+}
diff --git a/src/Application/Common/Behaviors/CachingBehavior.cs b/src/Application/Common/Behaviors/CachingBehavior.cs
new file mode 100644
index 0000000..c756cb1
--- /dev/null
+++ b/src/Application/Common/Behaviors/CachingBehavior.cs
@@ -0,0 +1,73 @@
+using MediatR;
+using Microsoft.Extensions.Caching.Memory;
+using Microsoft.Extensions.Logging;
+using System.Text.Json;
+
+namespace Application.Common.Behaviors;
+
+///
+/// Marker interface for cacheable queries
+///
+public interface ICacheableQuery
+{
+ ///
+ /// Cache key for the query
+ ///
+ string CacheKey { get; }
+
+ ///
+ /// Cache expiration time
+ ///
+ TimeSpan? CacheExpiration { get; }
+}
+
+///
+/// Caching behavior for MediatR pipeline
+///
+/// Request type
+/// Response type
+public sealed class CachingBehavior(
+ IMemoryCache cache,
+ ILogger> logger)
+ : IPipelineBehavior
+ where TRequest : notnull
+{
+ public async Task Handle(TRequest request, RequestHandlerDelegate next, CancellationToken cancellationToken)
+ {
+ if (request is not ICacheableQuery cacheableQuery)
+ {
+ return await next();
+ }
+
+ var cacheKey = cacheableQuery.CacheKey;
+
+ // Try to get from cache
+ if (cache.TryGetValue(cacheKey, out TResponse? cachedResponse))
+ {
+ logger.LogDebug("Cache hit for key: {CacheKey}", cacheKey);
+ return cachedResponse!;
+ }
+
+ logger.LogDebug("Cache miss for key: {CacheKey}", cacheKey);
+
+ // Execute the request
+ var response = await next();
+
+ // Cache the response
+ var cacheOptions = new MemoryCacheEntryOptions();
+
+ if (cacheableQuery.CacheExpiration.HasValue)
+ {
+ cacheOptions.SetAbsoluteExpiration(cacheableQuery.CacheExpiration.Value);
+ }
+ else
+ {
+ cacheOptions.SetAbsoluteExpiration(TimeSpan.FromMinutes(5)); // Default 5 minutes
+ }
+
+ cache.Set(cacheKey, response, cacheOptions);
+ logger.LogDebug("Cached response for key: {CacheKey}", cacheKey);
+
+ return response;
+ }
+}
diff --git a/src/Application/Common/Behaviors/DomainEventBehavior.cs b/src/Application/Common/Behaviors/DomainEventBehavior.cs
new file mode 100644
index 0000000..b06a551
--- /dev/null
+++ b/src/Application/Common/Behaviors/DomainEventBehavior.cs
@@ -0,0 +1,35 @@
+using Application.Common;
+using MediatR;
+using Microsoft.Extensions.Logging;
+
+namespace Application.Common.Behaviors;
+
+///
+/// Pipeline behavior to dispatch domain events after command execution
+///
+/// Request type
+/// Response type
+public sealed class DomainEventBehavior(
+ ILogger> logger)
+ : IPipelineBehavior
+ where TRequest : notnull
+{
+ public async Task Handle(TRequest request, RequestHandlerDelegate next, CancellationToken cancellationToken)
+ {
+ // Execute the command first
+ var response = await next();
+
+ // Only dispatch events for commands, not queries
+ if (request is ICommand)
+ {
+ logger.LogDebug("Command completed, checking for domain events to dispatch");
+
+ // TODO: Get entities with domain events from current context/unit of work
+ // This would typically come from your DbContext or Repository pattern
+ // var entitiesWithEvents = await GetEntitiesWithDomainEventsAsync();
+ // await domainEventDispatcher.DispatchEventsAsync(entitiesWithEvents, cancellationToken);
+ }
+
+ return response;
+ }
+}
diff --git a/src/Application/Common/Behaviors/LoggingBehavior.cs b/src/Application/Common/Behaviors/LoggingBehavior.cs
new file mode 100644
index 0000000..8f8f3fa
--- /dev/null
+++ b/src/Application/Common/Behaviors/LoggingBehavior.cs
@@ -0,0 +1,41 @@
+using MediatR;
+using Microsoft.Extensions.Logging;
+using System.Diagnostics;
+
+namespace Application.Common.Behaviors;
+
+///
+/// Logging behavior for MediatR pipeline
+///
+/// Request type
+/// Response type
+public sealed class LoggingBehavior(ILogger> logger)
+ : IPipelineBehavior
+ where TRequest : notnull
+{
+ public async Task Handle(TRequest request, RequestHandlerDelegate next, CancellationToken cancellationToken)
+ {
+ var requestName = typeof(TRequest).Name;
+ var stopwatch = Stopwatch.StartNew();
+
+ logger.LogInformation("Starting request {RequestName}", requestName);
+
+ try
+ {
+ var response = await next();
+
+ stopwatch.Stop();
+ logger.LogInformation("Completed request {RequestName} in {ElapsedMilliseconds}ms",
+ requestName, stopwatch.ElapsedMilliseconds);
+
+ return response;
+ }
+ catch (Exception ex)
+ {
+ stopwatch.Stop();
+ logger.LogError(ex, "Request {RequestName} failed after {ElapsedMilliseconds}ms",
+ requestName, stopwatch.ElapsedMilliseconds);
+ throw;
+ }
+ }
+}
diff --git a/src/Application/Common/Behaviors/PerformanceBehavior.cs b/src/Application/Common/Behaviors/PerformanceBehavior.cs
new file mode 100644
index 0000000..d768e07
--- /dev/null
+++ b/src/Application/Common/Behaviors/PerformanceBehavior.cs
@@ -0,0 +1,38 @@
+using MediatR;
+using Microsoft.Extensions.Logging;
+using System.Diagnostics;
+
+namespace Application.Common.Behaviors;
+
+///
+/// Performance monitoring behavior for MediatR pipeline
+///
+/// Request type
+/// Response type
+public sealed class PerformanceBehavior(ILogger> logger)
+ : IPipelineBehavior
+ where TRequest : notnull
+{
+ private const int SlowRequestThresholdMs = 5000; // 5 seconds
+
+ public async Task Handle(TRequest request, RequestHandlerDelegate next, CancellationToken cancellationToken)
+ {
+ var stopwatch = Stopwatch.StartNew();
+
+ var response = await next();
+
+ stopwatch.Stop();
+
+ var elapsedMilliseconds = stopwatch.ElapsedMilliseconds;
+
+ if (elapsedMilliseconds > SlowRequestThresholdMs)
+ {
+ var requestName = typeof(TRequest).Name;
+
+ logger.LogWarning("Slow request detected: {RequestName} took {ElapsedMilliseconds}ms",
+ requestName, elapsedMilliseconds);
+ }
+
+ return response;
+ }
+}
diff --git a/src/Application/Common/Behaviors/TransactionBehavior.cs b/src/Application/Common/Behaviors/TransactionBehavior.cs
new file mode 100644
index 0000000..2af9883
--- /dev/null
+++ b/src/Application/Common/Behaviors/TransactionBehavior.cs
@@ -0,0 +1,54 @@
+using MediatR;
+using Microsoft.Extensions.Logging;
+
+namespace Application.Common.Behaviors;
+
+///
+/// Marker interface for commands that require transaction
+///
+public interface ITransactionalCommand
+{
+}
+
+///
+/// Transaction behavior for MediatR pipeline
+///
+/// Request type
+/// Response type
+public sealed class TransactionBehavior(
+ ILogger> logger)
+ : IPipelineBehavior
+ where TRequest : notnull
+{
+ public async Task Handle(TRequest request, RequestHandlerDelegate next, CancellationToken cancellationToken)
+ {
+ if (request is not ITransactionalCommand)
+ {
+ return await next();
+ }
+
+ logger.LogDebug("Starting transaction for {RequestName}", typeof(TRequest).Name);
+
+ // TODO: Implement actual transaction logic when database context is available
+ // using var transaction = await context.Database.BeginTransactionAsync(cancellationToken);
+
+ try
+ {
+ var response = await next();
+
+ // TODO: Commit transaction
+ // await transaction.CommitAsync(cancellationToken);
+
+ logger.LogDebug("Transaction committed for {RequestName}", typeof(TRequest).Name);
+ return response;
+ }
+ catch (Exception ex)
+ {
+ // TODO: Rollback transaction
+ // await transaction.RollbackAsync(cancellationToken);
+
+ logger.LogError(ex, "Transaction rolled back for {RequestName}", typeof(TRequest).Name);
+ throw;
+ }
+ }
+}
diff --git a/src/Application/Common/Behaviors/ValidationBehavior.cs b/src/Application/Common/Behaviors/ValidationBehavior.cs
new file mode 100644
index 0000000..bc604aa
--- /dev/null
+++ b/src/Application/Common/Behaviors/ValidationBehavior.cs
@@ -0,0 +1,40 @@
+using Application.Common.Exceptions;
+using FluentValidation;
+using MediatR;
+
+namespace Application.Common.Behaviors;
+
+///
+/// Validation behavior for MediatR pipeline
+///
+/// Request type
+/// Response type
+public sealed class ValidationBehavior(IEnumerable> validators)
+ : IPipelineBehavior
+ where TRequest : notnull
+{
+ public async Task Handle(TRequest request, RequestHandlerDelegate next, CancellationToken cancellationToken)
+ {
+ if (!validators.Any())
+ {
+ return await next();
+ }
+
+ var context = new ValidationContext(request);
+
+ var validationResults = await Task.WhenAll(
+ validators.Select(v => v.ValidateAsync(context, cancellationToken)));
+
+ var failures = validationResults
+ .Where(r => r.Errors.Count != 0)
+ .SelectMany(r => r.Errors)
+ .ToList();
+
+ if (failures.Count != 0)
+ {
+ throw new Exceptions.ValidationException(failures);
+ }
+
+ return await next();
+ }
+}
diff --git a/src/Application/Common/Exceptions/ApplicationException.cs b/src/Application/Common/Exceptions/ApplicationException.cs
new file mode 100644
index 0000000..b98b823
--- /dev/null
+++ b/src/Application/Common/Exceptions/ApplicationException.cs
@@ -0,0 +1,15 @@
+namespace Application.Common.Exceptions;
+
+///
+/// Base application exception
+///
+public abstract class ApplicationException : Exception
+{
+ protected ApplicationException(string message) : base(message)
+ {
+ }
+
+ protected ApplicationException(string message, Exception innerException) : base(message, innerException)
+ {
+ }
+}
diff --git a/src/Application/Common/Exceptions/BusinessRuleException.cs b/src/Application/Common/Exceptions/BusinessRuleException.cs
new file mode 100644
index 0000000..3e08ef4
--- /dev/null
+++ b/src/Application/Common/Exceptions/BusinessRuleException.cs
@@ -0,0 +1,21 @@
+namespace Application.Common.Exceptions;
+
+///
+/// Exception thrown when a business rule is violated
+///
+public sealed class BusinessRuleException : ApplicationException
+{
+ ///
+ /// Business rule code for identification
+ ///
+ public string? RuleCode { get; }
+
+ public BusinessRuleException(string message) : base(message)
+ {
+ }
+
+ public BusinessRuleException(string ruleCode, string message) : base(message)
+ {
+ RuleCode = ruleCode;
+ }
+}
diff --git a/src/Application/Common/Exceptions/ConflictException.cs b/src/Application/Common/Exceptions/ConflictException.cs
new file mode 100644
index 0000000..c3530b6
--- /dev/null
+++ b/src/Application/Common/Exceptions/ConflictException.cs
@@ -0,0 +1,16 @@
+namespace Application.Common.Exceptions;
+
+///
+/// Exception thrown when there is a conflict (e.g., duplicate data)
+///
+public sealed class ConflictException : ApplicationException
+{
+ public ConflictException(string message) : base(message)
+ {
+ }
+
+ public ConflictException(string name, object value)
+ : base($"Entity \"{name}\" with value \"{value}\" already exists.")
+ {
+ }
+}
diff --git a/src/Application/Common/Exceptions/ExternalServiceException.cs b/src/Application/Common/Exceptions/ExternalServiceException.cs
new file mode 100644
index 0000000..a842e66
--- /dev/null
+++ b/src/Application/Common/Exceptions/ExternalServiceException.cs
@@ -0,0 +1,34 @@
+namespace Application.Common.Exceptions;
+
+///
+/// Exception thrown when external service call fails
+///
+public sealed class ExternalServiceException : ApplicationException
+{
+ ///
+ /// Service name that failed
+ ///
+ public string ServiceName { get; }
+
+ ///
+ /// HTTP status code (if applicable)
+ ///
+ public int? StatusCode { get; }
+
+ public ExternalServiceException(string serviceName, string message) : base(message)
+ {
+ ServiceName = serviceName;
+ }
+
+ public ExternalServiceException(string serviceName, string message, int statusCode) : base(message)
+ {
+ ServiceName = serviceName;
+ StatusCode = statusCode;
+ }
+
+ public ExternalServiceException(string serviceName, string message, Exception innerException)
+ : base(message, innerException)
+ {
+ ServiceName = serviceName;
+ }
+}
diff --git a/src/Application/Common/Exceptions/ForbiddenException.cs b/src/Application/Common/Exceptions/ForbiddenException.cs
new file mode 100644
index 0000000..a2d3209
--- /dev/null
+++ b/src/Application/Common/Exceptions/ForbiddenException.cs
@@ -0,0 +1,15 @@
+namespace Application.Common.Exceptions;
+
+///
+/// Exception thrown when user is authenticated but lacks permission
+///
+public sealed class ForbiddenException : ApplicationException
+{
+ public ForbiddenException() : base("User does not have permission to access this resource.")
+ {
+ }
+
+ public ForbiddenException(string message) : base(message)
+ {
+ }
+}
diff --git a/src/Application/Common/Exceptions/NotFoundException.cs b/src/Application/Common/Exceptions/NotFoundException.cs
new file mode 100644
index 0000000..dc587b9
--- /dev/null
+++ b/src/Application/Common/Exceptions/NotFoundException.cs
@@ -0,0 +1,16 @@
+namespace Application.Common.Exceptions;
+
+///
+/// Exception thrown when a requested entity is not found
+///
+public sealed class NotFoundException : ApplicationException
+{
+ public NotFoundException(string name, object key)
+ : base($"Entity \"{name}\" ({key}) was not found.")
+ {
+ }
+
+ public NotFoundException(string message) : base(message)
+ {
+ }
+}
diff --git a/src/Application/Common/Exceptions/UnauthorizedException.cs b/src/Application/Common/Exceptions/UnauthorizedException.cs
new file mode 100644
index 0000000..aaaa604
--- /dev/null
+++ b/src/Application/Common/Exceptions/UnauthorizedException.cs
@@ -0,0 +1,15 @@
+namespace Application.Common.Exceptions;
+
+///
+/// Exception thrown when user is not authorized to perform an action
+///
+public sealed class UnauthorizedException : ApplicationException
+{
+ public UnauthorizedException() : base("User is not authorized to perform this action.")
+ {
+ }
+
+ public UnauthorizedException(string message) : base(message)
+ {
+ }
+}
diff --git a/src/Application/Common/Exceptions/ValidationException.cs b/src/Application/Common/Exceptions/ValidationException.cs
new file mode 100644
index 0000000..54cf0a7
--- /dev/null
+++ b/src/Application/Common/Exceptions/ValidationException.cs
@@ -0,0 +1,39 @@
+using FluentValidation.Results;
+
+namespace Application.Common.Exceptions;
+
+///
+/// Exception thrown when validation fails
+///
+public sealed class ValidationException : ApplicationException
+{
+ ///
+ /// Validation errors
+ ///
+ public IReadOnlyDictionary Errors { get; }
+
+ public ValidationException() : base("One or more validation failures have occurred.")
+ {
+ Errors = new Dictionary();
+ }
+
+ public ValidationException(IEnumerable failures) : this()
+ {
+ Errors = failures
+ .GroupBy(e => e.PropertyName, e => e.ErrorMessage)
+ .ToDictionary(failureGroup => failureGroup.Key, failureGroup => failureGroup.ToArray());
+ }
+
+ public ValidationException(string propertyName, string errorMessage) : this()
+ {
+ Errors = new Dictionary
+ {
+ [propertyName] = [errorMessage]
+ };
+ }
+
+ public ValidationException(Dictionary errors) : this()
+ {
+ Errors = errors;
+ }
+}
diff --git a/src/Application/Common/ICommand.cs b/src/Application/Common/ICommand.cs
new file mode 100644
index 0000000..985141e
--- /dev/null
+++ b/src/Application/Common/ICommand.cs
@@ -0,0 +1,18 @@
+using MediatR;
+
+namespace Application.Common;
+
+///
+/// Marker interface for commands
+///
+public interface ICommand : IRequest
+{
+}
+
+///
+/// Marker interface for commands with response
+///
+/// Response type
+public interface ICommand : IRequest
+{
+}
diff --git a/src/Application/Common/ICommandHandler.cs b/src/Application/Common/ICommandHandler.cs
new file mode 100644
index 0000000..2e575e9
--- /dev/null
+++ b/src/Application/Common/ICommandHandler.cs
@@ -0,0 +1,22 @@
+using MediatR;
+
+namespace Application.Common;
+
+///
+/// Command handler interface
+///
+/// Command type
+public interface ICommandHandler : IRequestHandler
+ where TCommand : ICommand
+{
+}
+
+///
+/// Command handler interface with response
+///
+/// Command type
+/// Response type
+public interface ICommandHandler : IRequestHandler
+ where TCommand : ICommand
+{
+}
diff --git a/src/Application/Common/IDomainEventDispatcher.cs b/src/Application/Common/IDomainEventDispatcher.cs
new file mode 100644
index 0000000..9c4bcfa
--- /dev/null
+++ b/src/Application/Common/IDomainEventDispatcher.cs
@@ -0,0 +1,19 @@
+using Domain.Common;
+
+namespace Application.Common;
+
+///
+/// Interface for dispatching domain events
+///
+public interface IDomainEventDispatcher
+{
+ ///
+ /// Dispatch all domain events from entities
+ ///
+ Task DispatchEventsAsync(IEnumerable entities, CancellationToken cancellationToken = default);
+
+ ///
+ /// Dispatch a single domain event
+ ///
+ Task DispatchEventAsync(IDomainEvent domainEvent, CancellationToken cancellationToken = default);
+}
diff --git a/src/Application/Common/IDomainEventHandler.cs b/src/Application/Common/IDomainEventHandler.cs
new file mode 100644
index 0000000..b10af39
--- /dev/null
+++ b/src/Application/Common/IDomainEventHandler.cs
@@ -0,0 +1,13 @@
+using Domain.Common;
+using MediatR;
+
+namespace Application.Common;
+
+///
+/// Domain event handler interface
+///
+/// Domain event type
+public interface IDomainEventHandler : INotificationHandler
+ where TDomainEvent : IDomainEvent
+{
+}
diff --git a/src/Application/Common/IQuery.cs b/src/Application/Common/IQuery.cs
new file mode 100644
index 0000000..4aa3156
--- /dev/null
+++ b/src/Application/Common/IQuery.cs
@@ -0,0 +1,11 @@
+using MediatR;
+
+namespace Application.Common;
+
+///
+/// Marker interface for queries
+///
+/// Response type
+public interface IQuery : IRequest
+{
+}
diff --git a/src/Application/Common/IQueryHandler.cs b/src/Application/Common/IQueryHandler.cs
new file mode 100644
index 0000000..84c3d4c
--- /dev/null
+++ b/src/Application/Common/IQueryHandler.cs
@@ -0,0 +1,13 @@
+using MediatR;
+
+namespace Application.Common;
+
+///
+/// Query handler interface
+///
+/// Query type
+/// Response type
+public interface IQueryHandler : IRequestHandler
+ where TQuery : IQuery
+{
+}
diff --git a/src/Application/Common/Models/AuditableEntity.cs b/src/Application/Common/Models/AuditableEntity.cs
new file mode 100644
index 0000000..a28f526
--- /dev/null
+++ b/src/Application/Common/Models/AuditableEntity.cs
@@ -0,0 +1,47 @@
+namespace Application.Common.Models;
+
+///
+/// Base auditable entity with common audit fields
+///
+public abstract record AuditableEntity
+{
+ ///
+ /// Entity ID
+ ///
+ public required Guid Id { get; init; }
+
+ ///
+ /// When the entity was created
+ ///
+ public required DateTime CreatedAt { get; init; }
+
+ ///
+ /// Who created the entity
+ ///
+ public required Guid CreatedBy { get; init; }
+
+ ///
+ /// When the entity was last updated
+ ///
+ public DateTime? UpdatedAt { get; init; }
+
+ ///
+ /// Who last updated the entity
+ ///
+ public Guid? UpdatedBy { get; init; }
+
+ ///
+ /// Soft delete flag
+ ///
+ public bool IsDeleted { get; init; }
+
+ ///
+ /// When the entity was deleted (soft delete)
+ ///
+ public DateTime? DeletedAt { get; init; }
+
+ ///
+ /// Who deleted the entity (soft delete)
+ ///
+ public Guid? DeletedBy { get; init; }
+}
diff --git a/src/Application/Common/Models/PaginatedResult.cs b/src/Application/Common/Models/PaginatedResult.cs
new file mode 100644
index 0000000..72ab220
--- /dev/null
+++ b/src/Application/Common/Models/PaginatedResult.cs
@@ -0,0 +1,54 @@
+namespace Application.Common.Models;
+
+///
+/// Paginated result wrapper for queries that return lists
+///
+/// Type of items in the result
+public sealed record PaginatedResult
+{
+ ///
+ /// List of items for current page
+ ///
+ public required IReadOnlyList Items { get; init; }
+
+ ///
+ /// Current page number (1-based)
+ ///
+ public required int PageNumber { get; init; }
+
+ ///
+ /// Number of items per page
+ ///
+ public required int PageSize { get; init; }
+
+ ///
+ /// Total number of items across all pages
+ ///
+ public required int TotalCount { get; init; }
+
+ ///
+ /// Total number of pages
+ ///
+ public int TotalPages => (int)Math.Ceiling((double)TotalCount / PageSize);
+
+ ///
+ /// Indicates if there is a previous page
+ ///
+ public bool HasPreviousPage => PageNumber > 1;
+
+ ///
+ /// Indicates if there is a next page
+ ///
+ public bool HasNextPage => PageNumber < TotalPages;
+
+ ///
+ /// Create empty paginated result
+ ///
+ public static PaginatedResult Empty() => new()
+ {
+ Items = [],
+ PageNumber = 1,
+ PageSize = 10,
+ TotalCount = 0
+ };
+}
diff --git a/src/Application/Common/Models/PaginationRequest.cs b/src/Application/Common/Models/PaginationRequest.cs
new file mode 100644
index 0000000..4ebb9c0
--- /dev/null
+++ b/src/Application/Common/Models/PaginationRequest.cs
@@ -0,0 +1,43 @@
+namespace Application.Common.Models;
+
+///
+/// Base pagination request for queries
+///
+public abstract record PaginationRequest
+{
+ private int _pageNumber = 1;
+ private int _pageSize = 10;
+
+ ///
+ /// Page number (1-based, minimum 1)
+ ///
+ public int PageNumber
+ {
+ get => _pageNumber;
+ init => _pageNumber = value < 1 ? 1 : value;
+ }
+
+ ///
+ /// Page size (minimum 1, maximum 100)
+ ///
+ public int PageSize
+ {
+ get => _pageSize;
+ init => _pageSize = value switch
+ {
+ < 1 => 1,
+ > 100 => 100,
+ _ => value
+ };
+ }
+
+ ///
+ /// Calculate skip count for database queries
+ ///
+ public int Skip => (PageNumber - 1) * PageSize;
+
+ ///
+ /// Get take count (same as PageSize)
+ ///
+ public int Take => PageSize;
+}
diff --git a/src/Application/Common/Models/SortOrder.cs b/src/Application/Common/Models/SortOrder.cs
new file mode 100644
index 0000000..3cdf4c6
--- /dev/null
+++ b/src/Application/Common/Models/SortOrder.cs
@@ -0,0 +1,51 @@
+namespace Application.Common.Models;
+
+///
+/// Sort order enumeration
+///
+public enum SortOrder
+{
+ ///
+ /// Ascending order
+ ///
+ Ascending = 0,
+
+ ///
+ /// Descending order
+ ///
+ Descending = 1
+}
+
+///
+/// Sort specification
+///
+public sealed record SortSpecification
+{
+ ///
+ /// Field name to sort by
+ ///
+ public required string FieldName { get; init; }
+
+ ///
+ /// Sort order
+ ///
+ public SortOrder Order { get; init; } = SortOrder.Ascending;
+
+ ///
+ /// Create ascending sort
+ ///
+ public static SortSpecification Ascending(string fieldName) => new()
+ {
+ FieldName = fieldName,
+ Order = SortOrder.Ascending
+ };
+
+ ///
+ /// Create descending sort
+ ///
+ public static SortSpecification Descending(string fieldName) => new()
+ {
+ FieldName = fieldName,
+ Order = SortOrder.Descending
+ };
+}
diff --git a/src/Application/EventHandlers/UserCreatedEventHandler.cs b/src/Application/EventHandlers/UserCreatedEventHandler.cs
new file mode 100644
index 0000000..c73eb32
--- /dev/null
+++ b/src/Application/EventHandlers/UserCreatedEventHandler.cs
@@ -0,0 +1,34 @@
+using Application.Common;
+using Domain.Events.User;
+using Microsoft.Extensions.Logging;
+
+namespace Application.EventHandlers;
+
+///
+/// Handler for UserCreatedEvent
+///
+public sealed class UserCreatedEventHandler(
+ ILogger logger) : IDomainEventHandler
+{
+ public async Task Handle(UserCreatedEvent notification, CancellationToken cancellationToken)
+ {
+ logger.LogInformation("User created: {UserId} - {Email}",
+ notification.UserId, notification.Email);
+
+ // Side effects when user is created:
+
+ // 1. Send welcome email
+ // await emailService.SendWelcomeEmailAsync(notification.Email, notification.FullName);
+
+ // 2. Create user profile
+ // await profileService.CreateDefaultProfileAsync(notification.UserId);
+
+ // 3. Initialize user settings
+ // await settingsService.CreateDefaultSettingsAsync(notification.UserId);
+
+ // 4. Add to analytics
+ // await analyticsService.TrackUserRegistrationAsync(notification.UserId, notification.Roles);
+
+ await Task.CompletedTask; // Remove this when implementing actual services
+ }
+}
diff --git a/src/Application/EventHandlers/UserDeactivatedEventHandler.cs b/src/Application/EventHandlers/UserDeactivatedEventHandler.cs
new file mode 100644
index 0000000..0a01851
--- /dev/null
+++ b/src/Application/EventHandlers/UserDeactivatedEventHandler.cs
@@ -0,0 +1,36 @@
+using Application.Common;
+using Domain.Events.User;
+using Microsoft.Extensions.Logging;
+
+namespace Application.EventHandlers;
+
+///
+/// Handler for UserDeactivatedEvent
+///
+public sealed class UserDeactivatedEventHandler(
+ ILogger logger) : IDomainEventHandler
+{
+ public async Task Handle(UserDeactivatedEvent notification, CancellationToken cancellationToken)
+ {
+ logger.LogInformation("User deactivated: {UserId}", notification.UserId);
+
+ // Side effects when user is deactivated:
+
+ // 1. Revoke all user sessions
+ // await sessionService.RevokeAllUserSessionsAsync(notification.UserId);
+
+ // 2. Cancel pending subscriptions
+ // await subscriptionService.CancelUserSubscriptionsAsync(notification.UserId);
+
+ // 3. Archive user data
+ // await dataArchiveService.ArchiveUserDataAsync(notification.UserId);
+
+ // 4. Send deactivation notification
+ // await emailService.SendAccountDeactivatedEmailAsync(notification.UserId);
+
+ // 5. Update analytics
+ // await analyticsService.TrackUserDeactivationAsync(notification.UserId);
+
+ await Task.CompletedTask; // Remove this when implementing actual services
+ }
+}
diff --git a/src/Application/EventHandlers/UserUpdatedEventHandler.cs b/src/Application/EventHandlers/UserUpdatedEventHandler.cs
new file mode 100644
index 0000000..8c3653b
--- /dev/null
+++ b/src/Application/EventHandlers/UserUpdatedEventHandler.cs
@@ -0,0 +1,34 @@
+using Application.Common;
+using Domain.Events.User;
+using Microsoft.Extensions.Logging;
+
+namespace Application.EventHandlers;
+
+///
+/// Handler for UserUpdatedEvent
+///
+public sealed class UserUpdatedEventHandler(
+ ILogger logger) : IDomainEventHandler
+{
+ public async Task Handle(UserUpdatedEvent notification, CancellationToken cancellationToken)
+ {
+ logger.LogInformation("User updated: {UserId} - {FullName}",
+ notification.UserId, notification.FullName);
+
+ // Side effects when user is updated:
+
+ // 1. Update search index
+ // await searchService.UpdateUserIndexAsync(notification.UserId, notification.FullName);
+
+ // 2. Sync with external systems
+ // await externalSyncService.SyncUserDataAsync(notification.UserId);
+
+ // 3. Update cached user data
+ // await cacheService.InvalidateUserCacheAsync(notification.UserId);
+
+ // 4. Send profile update notification
+ // await notificationService.SendProfileUpdatedNotificationAsync(notification.UserId);
+
+ await Task.CompletedTask; // Remove this when implementing actual services
+ }
+}
diff --git a/src/Application/Features/Auth/GetProfile/GetProfileQuery.cs b/src/Application/Features/Auth/GetProfile/GetProfileQuery.cs
new file mode 100644
index 0000000..3c3ab21
--- /dev/null
+++ b/src/Application/Features/Auth/GetProfile/GetProfileQuery.cs
@@ -0,0 +1,61 @@
+using Application.Common;
+using Application.Common.Behaviors;
+using Domain.Common;
+
+namespace Application.Features.Auth.GetProfile;
+
+///
+/// Get profile query with caching
+///
+public sealed record GetProfileQuery : IQuery>, ICacheableQuery
+{
+ ///
+ /// User ID
+ ///
+ public required Guid UserId { get; init; }
+
+ // ICacheableQuery implementation
+ public string CacheKey => $"user-profile:{UserId}";
+ public TimeSpan? CacheExpiration => TimeSpan.FromMinutes(15);
+}
+
+///
+/// Get profile response
+///
+public sealed record GetProfileResponse
+{
+ ///
+ /// User ID
+ ///
+ public required Guid Id { get; init; }
+
+ ///
+ /// Email address
+ ///
+ public required string Email { get; init; }
+
+ ///
+ /// Full name
+ ///
+ public required string FullName { get; init; }
+
+ ///
+ /// Phone number
+ ///
+ public string? PhoneNumber { get; init; }
+
+ ///
+ /// User roles
+ ///
+ public required string[] Roles { get; init; }
+
+ ///
+ /// Created date
+ ///
+ public required DateTime CreatedAt { get; init; }
+
+ ///
+ /// Last updated date
+ ///
+ public DateTime? UpdatedAt { get; init; }
+}
diff --git a/src/Application/Features/Auth/GetProfile/GetProfileQueryHandler.cs b/src/Application/Features/Auth/GetProfile/GetProfileQueryHandler.cs
new file mode 100644
index 0000000..f758e06
--- /dev/null
+++ b/src/Application/Features/Auth/GetProfile/GetProfileQueryHandler.cs
@@ -0,0 +1,25 @@
+using Application.Common;
+using Application.Interfaces;
+using Domain.Common;
+
+namespace Application.Features.Auth.GetProfile;
+
+///
+/// Get profile query handler
+///
+#pragma warning disable CS9113 // Parameter is unread.
+public sealed class GetProfileQueryHandler(IUserRepository userRepository)
+#pragma warning restore CS9113 // Parameter is unread.
+ : IQueryHandler>
+{
+ public async Task> Handle(GetProfileQuery request, CancellationToken cancellationToken)
+ {
+ // TODO: Implement get profile logic
+ // 1. Retrieve user by ID
+ // 2. Map to GetProfileResponse
+ // 3. Return result
+
+ await Task.Delay(1, cancellationToken);
+ throw new NotImplementedException("Get profile logic not implemented yet");
+ }
+}
diff --git a/src/Application/Features/Auth/Login/LoginCommand.cs b/src/Application/Features/Auth/Login/LoginCommand.cs
new file mode 100644
index 0000000..0af1b7c
--- /dev/null
+++ b/src/Application/Features/Auth/Login/LoginCommand.cs
@@ -0,0 +1,77 @@
+using Application.Common;
+using Domain.Common;
+
+namespace Application.Features.Auth.Login;
+
+///
+/// Login command
+///
+public sealed record LoginCommand : ICommand>
+{
+ ///
+ /// Email address
+ ///
+ public required string Email { get; init; }
+
+ ///
+ /// Password
+ ///
+ public required string Password { get; init; }
+
+ ///
+ /// Remember me flag
+ ///
+ public bool RememberMe { get; init; }
+}
+
+///
+/// Login response
+///
+public sealed record LoginResponse
+{
+ ///
+ /// Access token
+ ///
+ public required string AccessToken { get; init; }
+
+ ///
+ /// Refresh token
+ ///
+ public required string RefreshToken { get; init; }
+
+ ///
+ /// Token expiration time
+ ///
+ public required DateTime ExpiresAt { get; init; }
+
+ ///
+ /// User information
+ ///
+ public required UserInfo User { get; init; }
+}
+
+///
+/// User information
+///
+public sealed record UserInfo
+{
+ ///
+ /// User ID
+ ///
+ public required Guid Id { get; init; }
+
+ ///
+ /// Email address
+ ///
+ public required string Email { get; init; }
+
+ ///
+ /// Full name
+ ///
+ public required string FullName { get; init; }
+
+ ///
+ /// User roles
+ ///
+ public required string[] Roles { get; init; }
+}
diff --git a/src/Application/Features/Auth/Login/LoginCommandHandler.cs b/src/Application/Features/Auth/Login/LoginCommandHandler.cs
new file mode 100644
index 0000000..831711d
--- /dev/null
+++ b/src/Application/Features/Auth/Login/LoginCommandHandler.cs
@@ -0,0 +1,56 @@
+using Application.Common;
+using Application.Common.Exceptions;
+using Application.Interfaces;
+using Domain.Common;
+
+namespace Application.Features.Auth.Login;
+
+///
+/// Login command handler
+///
+public sealed class LoginCommandHandler(
+ IUserRepository userRepository,
+ IPasswordHasher passwordHasher,
+ ITokenService tokenService) : ICommandHandler>
+{
+ public async Task> Handle(LoginCommand request, CancellationToken cancellationToken)
+ {
+ // Find user by email
+ var user = await userRepository.GetByEmailAsync(request.Email, cancellationToken)
+ ?? throw new UnauthorizedException("Invalid email or password.");
+
+ // Verify password
+ if (!passwordHasher.VerifyPassword(request.Password, user.PasswordHash))
+ {
+ throw new UnauthorizedException("Invalid email or password.");
+ }
+
+ // Check if user is active
+ if (user.IsDeleted)
+ {
+ throw new ForbiddenException("User account has been deactivated.");
+ }
+
+ // Generate tokens
+ var userInfo = new UserInfo
+ {
+ Id = user.Id,
+ Email = user.Email,
+ FullName = user.FullName,
+ Roles = user.Roles
+ };
+
+ var accessToken = tokenService.GenerateAccessToken(userInfo);
+ var refreshToken = tokenService.GenerateRefreshToken();
+
+ var response = new LoginResponse
+ {
+ AccessToken = accessToken,
+ RefreshToken = refreshToken,
+ ExpiresAt = DateTime.UtcNow.AddHours(1), // TODO: Get from configuration
+ User = userInfo
+ };
+
+ return Result.Success(response);
+ }
+}
diff --git a/src/Application/Features/Auth/Login/LoginCommandValidator.cs b/src/Application/Features/Auth/Login/LoginCommandValidator.cs
new file mode 100644
index 0000000..624bce3
--- /dev/null
+++ b/src/Application/Features/Auth/Login/LoginCommandValidator.cs
@@ -0,0 +1,24 @@
+using FluentValidation;
+
+namespace Application.Features.Auth.Login;
+
+///
+/// Login command validator
+///
+public sealed class LoginCommandValidator : AbstractValidator
+{
+ public LoginCommandValidator()
+ {
+ RuleFor(x => x.Email)
+ .NotEmpty()
+ .WithMessage("Email is required")
+ .EmailAddress()
+ .WithMessage("Email must be a valid email address");
+
+ RuleFor(x => x.Password)
+ .NotEmpty()
+ .WithMessage("Password is required")
+ .MinimumLength(6)
+ .WithMessage("Password must be at least 6 characters long");
+ }
+}
diff --git a/src/Application/Features/Auth/Logout/LogoutCommand.cs b/src/Application/Features/Auth/Logout/LogoutCommand.cs
new file mode 100644
index 0000000..50340ea
--- /dev/null
+++ b/src/Application/Features/Auth/Logout/LogoutCommand.cs
@@ -0,0 +1,15 @@
+using Application.Common;
+using Domain.Common;
+
+namespace Application.Features.Auth.Logout;
+
+///
+/// Logout command
+///
+public sealed record LogoutCommand : ICommand
+{
+ ///
+ /// Refresh token to invalidate
+ ///
+ public required string RefreshToken { get; init; }
+}
diff --git a/src/Application/Features/Auth/Logout/LogoutCommandHandler.cs b/src/Application/Features/Auth/Logout/LogoutCommandHandler.cs
new file mode 100644
index 0000000..a3a9e68
--- /dev/null
+++ b/src/Application/Features/Auth/Logout/LogoutCommandHandler.cs
@@ -0,0 +1,25 @@
+using Application.Common;
+using Domain.Common;
+
+namespace Application.Features.Auth.Logout;
+
+///
+/// Logout command handler
+///
+public sealed class LogoutCommandHandler : ICommandHandler
+{
+ public LogoutCommandHandler()
+ {
+ // TODO: Inject dependencies (ITokenService, IRefreshTokenRepository, etc.)
+ }
+
+ public async Task Handle(LogoutCommand request, CancellationToken cancellationToken)
+ {
+ // TODO: Implement logout logic
+ // 1. Invalidate refresh token
+ // 2. Add access token to blacklist (optional)
+
+ await Task.Delay(1, cancellationToken);
+ throw new NotImplementedException("Logout logic not implemented yet");
+ }
+}
diff --git a/src/Application/Features/Auth/Logout/LogoutCommandValidator.cs b/src/Application/Features/Auth/Logout/LogoutCommandValidator.cs
new file mode 100644
index 0000000..4ab0723
--- /dev/null
+++ b/src/Application/Features/Auth/Logout/LogoutCommandValidator.cs
@@ -0,0 +1,16 @@
+using FluentValidation;
+
+namespace Application.Features.Auth.Logout;
+
+///
+/// Logout command validator
+///
+public sealed class LogoutCommandValidator : AbstractValidator
+{
+ public LogoutCommandValidator()
+ {
+ RuleFor(x => x.RefreshToken)
+ .NotEmpty()
+ .WithMessage("Refresh token is required");
+ }
+}
diff --git a/src/Application/Features/Conversation/CreateConversation/CreateConversationCommand.cs b/src/Application/Features/Conversation/CreateConversation/CreateConversationCommand.cs
new file mode 100644
index 0000000..3be97a3
--- /dev/null
+++ b/src/Application/Features/Conversation/CreateConversation/CreateConversationCommand.cs
@@ -0,0 +1,46 @@
+using Application.Common;
+using Domain.Common;
+
+namespace Application.Features.Conversation.CreateConversation;
+
+///
+/// Create conversation command
+///
+public sealed record CreateConversationCommand : ICommand>
+{
+ ///
+ /// User ID who creates the conversation
+ ///
+ public required Guid UserId { get; init; }
+
+ ///
+ /// Conversation title
+ ///
+ public required string Title { get; init; }
+
+ ///
+ /// Initial message (optional)
+ ///
+ public string? InitialMessage { get; init; }
+}
+
+///
+/// Create conversation response
+///
+public sealed record CreateConversationResponse
+{
+ ///
+ /// Created conversation ID
+ ///
+ public required Guid Id { get; init; }
+
+ ///
+ /// Conversation title
+ ///
+ public required string Title { get; init; }
+
+ ///
+ /// Created date
+ ///
+ public required DateTime CreatedAt { get; init; }
+}
diff --git a/src/Application/Features/User/CreateUser/CreateUserCommand.cs b/src/Application/Features/User/CreateUser/CreateUserCommand.cs
new file mode 100644
index 0000000..c479804
--- /dev/null
+++ b/src/Application/Features/User/CreateUser/CreateUserCommand.cs
@@ -0,0 +1,66 @@
+using Application.Common;
+using Application.Common.Behaviors;
+using Domain.Common;
+
+namespace Application.Features.User.CreateUser;
+
+///
+/// Create user command with transaction and authorization
+///
+public sealed record CreateUserCommand : ICommand>,
+ ITransactionalCommand, IAuthorizedRequest
+{
+ ///
+ /// Email address
+ ///
+ public required string Email { get; init; }
+
+ ///
+ /// Full name
+ ///
+ public required string FullName { get; init; }
+
+ ///
+ /// Password
+ ///
+ public required string Password { get; init; }
+
+ ///
+ /// Phone number
+ ///
+ public string? PhoneNumber { get; init; }
+
+ ///
+ /// User roles
+ ///
+ public string[] Roles { get; init; } = ["User"];
+
+ // IAuthorizedRequest implementation
+ public AuthorizationRequirement AuthorizationRequirement => new()
+ {
+ Roles = ["Admin"],
+ Permissions = ["users.create"],
+ RequireAuthentication = true
+ };
+}
+
+///
+/// Create user response
+///
+public sealed record CreateUserResponse
+{
+ ///
+ /// Created user ID
+ ///
+ public required Guid Id { get; init; }
+
+ ///
+ /// Email address
+ ///
+ public required string Email { get; init; }
+
+ ///
+ /// Full name
+ ///
+ public required string FullName { get; init; }
+}
diff --git a/src/Application/Features/User/CreateUser/CreateUserCommandHandler.cs b/src/Application/Features/User/CreateUser/CreateUserCommandHandler.cs
new file mode 100644
index 0000000..d6166b8
--- /dev/null
+++ b/src/Application/Features/User/CreateUser/CreateUserCommandHandler.cs
@@ -0,0 +1,50 @@
+using Application.Common;
+using Application.Common.Exceptions;
+using Application.Interfaces;
+using Domain.Common;
+using Domain.Aggregates.User;
+
+namespace Application.Features.User.CreateUser;
+
+///
+/// Create user command handler
+///
+public sealed class CreateUserCommandHandler(
+ IUserRepository userRepository,
+ IPasswordHasher passwordHasher) : ICommandHandler>
+{
+ public async Task> Handle(CreateUserCommand request, CancellationToken cancellationToken)
+ {
+ // Check if user already exists
+ var existingUser = await userRepository.GetByEmailAsync(request.Email, cancellationToken);
+ if (existingUser is not null)
+ {
+ throw new ConflictException("User", request.Email);
+ }
+
+ // Create new user
+ var user = Domain.Aggregates.User.User.Create(
+ request.Email,
+ request.FullName,
+ passwordHasher.HashPassword(request.Password),
+ request.Roles);
+
+ try
+ {
+ var createdUser = await userRepository.CreateAsync(user, cancellationToken);
+
+ var response = new CreateUserResponse
+ {
+ Id = createdUser.Id,
+ Email = createdUser.Email,
+ FullName = createdUser.FullName
+ };
+
+ return Result.Success(response);
+ }
+ catch (Exception ex)
+ {
+ throw new ExternalServiceException("Database", "Failed to create user", ex);
+ }
+ }
+}
diff --git a/src/Application/Features/User/CreateUser/CreateUserCommandValidator.cs b/src/Application/Features/User/CreateUser/CreateUserCommandValidator.cs
new file mode 100644
index 0000000..c1f9a9b
--- /dev/null
+++ b/src/Application/Features/User/CreateUser/CreateUserCommandValidator.cs
@@ -0,0 +1,45 @@
+using FluentValidation;
+
+namespace Application.Features.User.CreateUser;
+
+///
+/// Create user command validator
+///
+public sealed class CreateUserCommandValidator : AbstractValidator
+{
+ public CreateUserCommandValidator()
+ {
+ RuleFor(x => x.Email)
+ .NotEmpty()
+ .WithMessage("Email is required")
+ .EmailAddress()
+ .WithMessage("Email must be a valid email address")
+ .MaximumLength(255)
+ .WithMessage("Email cannot exceed 255 characters");
+
+ RuleFor(x => x.FullName)
+ .NotEmpty()
+ .WithMessage("Full name is required")
+ .MaximumLength(100)
+ .WithMessage("Full name cannot exceed 100 characters");
+
+ RuleFor(x => x.Password)
+ .NotEmpty()
+ .WithMessage("Password is required")
+ .MinimumLength(8)
+ .WithMessage("Password must be at least 8 characters long")
+ .Matches(@"^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)")
+ .WithMessage("Password must contain at least one lowercase, one uppercase letter and one digit");
+
+ RuleFor(x => x.PhoneNumber)
+ .Matches(@"^\+?[1-9]\d{1,14}$")
+ .When(x => !string.IsNullOrEmpty(x.PhoneNumber))
+ .WithMessage("Phone number format is invalid");
+
+ RuleFor(x => x.Roles)
+ .NotEmpty()
+ .WithMessage("At least one role is required")
+ .Must(roles => roles.All(role => !string.IsNullOrWhiteSpace(role)))
+ .WithMessage("Role names cannot be empty");
+ }
+}
diff --git a/src/Application/Features/User/GetUsers/GetUsersQuery.cs b/src/Application/Features/User/GetUsers/GetUsersQuery.cs
new file mode 100644
index 0000000..d8aafec
--- /dev/null
+++ b/src/Application/Features/User/GetUsers/GetUsersQuery.cs
@@ -0,0 +1,75 @@
+using Application.Common;
+using Application.Common.Behaviors;
+using Application.Common.Models;
+using Domain.Common;
+
+namespace Application.Features.User.GetUsers;
+
+///
+/// Get users query with pagination, sorting, and caching
+///
+public sealed record GetUsersQuery : PaginationRequest, IQuery>>,
+ ICacheableQuery, IAuthorizedRequest
+{
+ ///
+ /// Search term to filter users
+ ///
+ public string? SearchTerm { get; init; }
+
+ ///
+ /// Role filter
+ ///
+ public string? Role { get; init; }
+
+ ///
+ /// Sort specification
+ ///
+ public SortSpecification? Sort { get; init; }
+
+ ///
+ /// Include deleted users
+ ///
+ public bool IncludeDeleted { get; init; }
+
+ // ICacheableQuery implementation
+ public string CacheKey => $"users:{PageNumber}:{PageSize}:{SearchTerm}:{Role}:{Sort?.FieldName}:{Sort?.Order}:{IncludeDeleted}";
+ public TimeSpan? CacheExpiration => TimeSpan.FromMinutes(10);
+
+ // IAuthorizedRequest implementation
+ public AuthorizationRequirement AuthorizationRequirement => new()
+ {
+ Roles = ["Admin", "Manager"],
+ RequireAuthentication = true
+ };
+}
+
+///
+/// User summary for list display
+///
+public sealed record UserSummary : AuditableEntity
+{
+ ///
+ /// Email address
+ ///
+ public required string Email { get; init; }
+
+ ///
+ /// Full name
+ ///
+ public required string FullName { get; init; }
+
+ ///
+ /// User roles
+ ///
+ public required string[] Roles { get; init; }
+
+ ///
+ /// Is user active
+ ///
+ public required bool IsActive { get; init; }
+
+ ///
+ /// Last login date
+ ///
+ public DateTime? LastLoginAt { get; init; }
+}
diff --git a/src/Application/Interfaces/IPasswordHasher.cs b/src/Application/Interfaces/IPasswordHasher.cs
new file mode 100644
index 0000000..7024e61
--- /dev/null
+++ b/src/Application/Interfaces/IPasswordHasher.cs
@@ -0,0 +1,17 @@
+namespace Application.Interfaces;
+
+///
+/// Password hasher interface
+///
+public interface IPasswordHasher
+{
+ ///
+ /// Hash password
+ ///
+ string HashPassword(string password);
+
+ ///
+ /// Verify password
+ ///
+ bool VerifyPassword(string password, string hash);
+}
diff --git a/src/Application/Interfaces/ITokenService.cs b/src/Application/Interfaces/ITokenService.cs
new file mode 100644
index 0000000..eacf984
--- /dev/null
+++ b/src/Application/Interfaces/ITokenService.cs
@@ -0,0 +1,29 @@
+using Application.Features.Auth.Login;
+
+namespace Application.Interfaces;
+
+///
+/// Token service interface
+///
+public interface ITokenService
+{
+ ///
+ /// Generate access token
+ ///
+ string GenerateAccessToken(UserInfo user);
+
+ ///
+ /// Generate refresh token
+ ///
+ string GenerateRefreshToken();
+
+ ///
+ /// Validate access token
+ ///
+ Task ValidateAccessTokenAsync(string token);
+
+ ///
+ /// Get user claims from token
+ ///
+ Task?> GetClaimsFromTokenAsync(string token);
+}
diff --git a/src/Application/Interfaces/IUserRepository.cs b/src/Application/Interfaces/IUserRepository.cs
new file mode 100644
index 0000000..c233c15
--- /dev/null
+++ b/src/Application/Interfaces/IUserRepository.cs
@@ -0,0 +1,29 @@
+using Domain.Aggregates.User;
+
+namespace Application.Interfaces;
+
+///
+/// User repository interface
+///
+public interface IUserRepository
+{
+ ///
+ /// Get user by email
+ ///
+ Task GetByEmailAsync(string email, CancellationToken cancellationToken = default);
+
+ ///
+ /// Get user by ID
+ ///
+ Task GetByIdAsync(Guid id, CancellationToken cancellationToken = default);
+
+ ///
+ /// Update user
+ ///
+ Task UpdateAsync(User user, CancellationToken cancellationToken = default);
+
+ ///
+ /// Create user
+ ///
+ Task CreateAsync(User user, CancellationToken cancellationToken = default);
+}
diff --git a/src/Application/README.md b/src/Application/README.md
new file mode 100644
index 0000000..1a9d334
--- /dev/null
+++ b/src/Application/README.md
@@ -0,0 +1,188 @@
+# 🔧 Application Layer
+
+**Orchestrates use cases và business workflows. Định nghĩa "what" the system does.**
+
+## 📁 Cấu trúc Folders
+
+```
+Application/
+├── Common/ # 🔗 Shared Components
+│ ├── ICommand.cs # Command interfaces (CQRS)
+│ ├── ICommandHandler.cs # Command handler interfaces
+│ ├── IQuery.cs # Query interfaces (CQRS)
+│ ├── IQueryHandler.cs # Query handler interfaces
+│ ├── IDomainEventHandler.cs # Domain event handler interface
+│ ├── IDomainEventDispatcher.cs # Domain event dispatcher
+│ │
+│ ├── Behaviors/ # 🔄 MediatR Pipeline Behaviors
+│ │ ├── ValidationBehavior.cs # Validate requests
+│ │ ├── LoggingBehavior.cs # Log all requests/responses
+│ │ ├── PerformanceBehavior.cs # Monitor performance
+│ │ ├── AuthorizationBehavior.cs # Check permissions
+│ │ ├── CachingBehavior.cs # Cache responses
+│ │ ├── TransactionBehavior.cs # Database transactions
+│ │ └── DomainEventBehavior.cs # Dispatch domain events
+│ │
+│ ├── Exceptions/ # 🚨 Custom Exceptions
+│ │ ├── ApplicationException.cs # Base application exception
+│ │ ├── ValidationException.cs # Validation errors
+│ │ ├── NotFoundException.cs # Entity not found
+│ │ ├── UnauthorizedException.cs # Auth failures
+│ │ ├── ForbiddenException.cs # Permission denied
+│ │ ├── ConflictException.cs # Business conflicts
+│ │ ├── BusinessRuleException.cs # Business rule violations
+│ │ └── ExternalServiceException.cs # External service errors
+│ │
+│ └── Models/ # 📋 Shared Models
+│ ├── PaginatedResult.cs # Paginated responses
+│ ├── PaginationRequest.cs # Pagination parameters
+│ ├── AuditableEntity.cs # Base auditable entity
+│ └── SortOrder.cs # Sorting directions
+│
+├── Features/ # 🎯 Feature-based Organization (Vertical Slices)
+│ ├── Auth/ # Authentication features
+│ │ ├── Login/
+│ │ │ ├── LoginCommand.cs # Login command
+│ │ │ ├── LoginCommandHandler.cs # Login logic
+│ │ │ └── LoginCommandValidator.cs # Login validation
+│ │ ├── Logout/
+│ │ ├── GetProfile/
+│ │ ├── ChangePassword/
+│ │ └── RefreshToken/
+│ │
+│ ├── User/ # User management features
+│ │ ├── CreateUser/
+│ │ │ ├── CreateUserCommand.cs
+│ │ │ ├── CreateUserCommandHandler.cs
+│ │ │ └── CreateUserCommandValidator.cs
+│ │ ├── GetUsers/
+│ │ ├── UpdateUserProfile/
+│ │ └── DeleteUser/
+│ │
+│ └── Conversation/ # Conversation features
+│ ├── CreateConversation/
+│ ├── GetConversations/
+│ ├── SendMessage/
+│ ├── GetMessages/
+│ └── DeleteConversation/
+│
+├── EventHandlers/ # 📢 Domain Event Handlers
+│ ├── UserCreatedEventHandler.cs # Handle user creation
+│ ├── UserUpdatedEventHandler.cs # Handle user updates
+│ └── UserDeactivatedEventHandler.cs # Handle user deactivation
+│
+└── Interfaces/ # 🔌 External Service Interfaces (DIP)
+ ├── IUserRepository.cs # User data access
+ ├── IPasswordHasher.cs # Password hashing service
+ └── ITokenService.cs # JWT token service
+```
+
+## 🎯 Mục đích từng folder
+
+### 🎯 **Features/** - Vertical Slices
+Tổ chức theo **features** thay vì **technical layers**
+
+**Ưu điểm:**
+- ✅ **High Cohesion** - Mọi thứ liên quan đến 1 feature ở cùng chỗ
+- ✅ **Easy to Find** - Dễ tìm code liên quan đến use case
+- ✅ **Team Productivity** - Team có thể work parallel trên các features
+- ✅ **Maintainable** - Thay đổi feature không ảnh hưởng features khác
+
+**Structure mỗi feature:**
+```
+FeatureName/
+├── Command.cs # Input model
+├── CommandHandler.cs # Business logic
+├── CommandValidator.cs # Input validation
+└── Response.cs # Output model
+```
+
+### 🔄 **Behaviors/** - Cross-cutting Concerns
+**Pipeline behaviors** chạy trước/sau mọi request
+
+**Thứ tự quan trọng:**
+1. **Logging** - Log request đầu vào
+2. **Performance** - Đo thời gian execution
+3. **Authorization** - Check permissions
+4. **Validation** - Validate input
+5. **Caching** - Cache responses (cho queries)
+6. **Transaction** - Database transactions (cho commands)
+7. **DomainEvent** - Dispatch domain events
+
+### 🚨 **Exceptions/** - Error Handling
+**Structured exception hierarchy** cho error handling
+
+- **ApplicationException** - Base cho tất cả app exceptions
+- **ValidationException** - Input validation errors
+- **BusinessRuleException** - Business logic violations
+- **External exceptions** - Integration failures
+
+### 📢 **EventHandlers/** - Side Effects
+**Handle domain events** để thực hiện side effects
+
+**Ví dụ UserCreatedEvent:**
+- Send welcome email
+- Create default user profile
+- Initialize user settings
+- Track analytics
+
+### 🔌 **Interfaces/** - Dependency Inversion
+**Define contracts** cho external services
+
+**Implemented by Infrastructure layer:**
+- Database repositories
+- External APIs
+- File storage
+- Email services
+- Caching services
+
+## 🔄 CQRS Pattern
+
+### Commands (Write Operations)
+```csharp
+public record CreateUserCommand : ICommand>
+{
+ public string Email { get; init; }
+ public string FullName { get; init; }
+ public string Password { get; init; }
+}
+```
+
+### Queries (Read Operations)
+```csharp
+public record GetUsersQuery : IQuery>
+{
+ public int Page { get; init; } = 1;
+ public int PageSize { get; init; } = 10;
+}
+```
+
+## 🚫 Điều KHÔNG ĐƯỢC làm
+
+- ❌ **No Infrastructure concerns** - Không có EF, SQL, HTTP
+- ❌ **No UI logic** - Không có presentation logic
+- ❌ **No external dependencies** - Chỉ abstractions/interfaces
+- ❌ **No business rules** - Business logic thuộc về Domain
+
+## ✅ Điều NÊN làm
+
+- ✅ **Orchestrate use cases** - Coordinate business workflows
+- ✅ **Define interfaces** - Contracts cho external services
+- ✅ **Handle domain events** - Side effects từ domain events
+- ✅ **Validate inputs** - Input validation với FluentValidation
+- ✅ **Transform data** - Map giữa domain objects và DTOs
+
+## 🔗 Dependencies
+
+- **Domain Layer** - Core business logic
+- **MediatR** - CQRS và mediator pattern
+- **FluentValidation** - Input validation
+- **Microsoft.Extensions.*** - Abstractions (Logging, Caching)
+
+## 💡 Best Practices
+
+1. **Thin Handlers** - Logic minimal, delegate to domain
+2. **Validation First** - Validate inputs trước khi process
+3. **Error Handling** - Consistent exception handling
+4. **Event-Driven** - Use domain events cho side effects
+5. **Testable** - Easy to unit test handlers
diff --git a/src/Domain/Aggregates/Conversation/Conversation.cs b/src/Domain/Aggregates/Conversation/Conversation.cs
new file mode 100644
index 0000000..7da105b
--- /dev/null
+++ b/src/Domain/Aggregates/Conversation/Conversation.cs
@@ -0,0 +1,221 @@
+using Domain.Common;
+using Domain.Entities;
+using Domain.Events.Conversation;
+using Domain.Enums;
+
+namespace Domain.Aggregates.Conversation;
+
+///
+/// Conversation aggregate root - Manages conversation business operations
+///
+public sealed class ConversationAggregate : BaseAggregateRoot
+{
+ private readonly Entities.Conversation _conversation;
+ private readonly List _messages = [];
+
+ public ConversationAggregate(Entities.Conversation conversation)
+ {
+ _conversation = conversation ?? throw new ArgumentNullException(nameof(conversation));
+ Id = conversation.Id;
+ }
+
+ // Expose conversation properties
+ public Guid OwnerId => _conversation.OwnerId;
+ public string Title => _conversation.Title;
+ public bool IsPrivate => _conversation.IsPrivate;
+ public string[] Tags => _conversation.Tags;
+ public int Priority => _conversation.Priority;
+ public ConversationStatus Status => _conversation.Status;
+ public bool IsActive => _conversation.IsActive;
+ public IReadOnlyList Messages => _messages.AsReadOnly();
+
+ // Get underlying entities
+ public Entities.Conversation GetConversation() => _conversation;
+
+ ///
+ /// Create a new conversation aggregate
+ ///
+ public static ConversationAggregate Create(Guid ownerId, string title, bool isPrivate = false)
+ {
+ // Validate business rules
+ if (ownerId == Guid.Empty)
+ {
+ throw new ArgumentException("Owner ID is required", nameof(ownerId));
+ }
+
+ if (string.IsNullOrWhiteSpace(title))
+ {
+ throw new ArgumentException("Title is required", nameof(title));
+ }
+
+ // Create conversation entity
+ var conversation = new Entities.Conversation
+ {
+ OwnerId = ownerId,
+ Title = title,
+ IsPrivate = isPrivate,
+ Priority = CalculateInitialPriority(isPrivate),
+ Status = ConversationStatus.Active
+ };
+
+ var aggregate = new ConversationAggregate(conversation);
+
+ // Raise domain event
+ aggregate.AddDomainEvent(new ConversationCreatedEvent(
+ conversation.Id,
+ ownerId,
+ title,
+ isPrivate));
+
+ return aggregate;
+ }
+
+ ///
+ /// Add message to conversation
+ ///
+ public void AddMessage(Guid senderId, string content, MessageType type = MessageType.Text, bool isFromBot = false)
+ {
+ // Business rules validation
+ if (_conversation.Status != ConversationStatus.Active)
+ {
+ throw new InvalidOperationException("Cannot add messages to inactive conversations");
+ }
+
+ if (string.IsNullOrWhiteSpace(content))
+ {
+ throw new ArgumentException("Message content is required", nameof(content));
+ }
+
+ var message = new Entities.Message
+ {
+ ConversationId = _conversation.Id,
+ SenderId = senderId,
+ Content = content,
+ Type = type,
+ IsFromBot = isFromBot
+ };
+
+ _messages.Add(message);
+ _conversation.UpdatedAt = DateTime.UtcNow;
+ UpdatedAt = DateTime.UtcNow;
+
+ // Update priority based on activity
+ UpdatePriorityBasedOnActivity();
+ }
+
+ ///
+ /// Close conversation
+ ///
+ public void Close()
+ {
+ if (_conversation.Status == ConversationStatus.Closed)
+ {
+ return;
+ }
+
+ _conversation.Status = ConversationStatus.Closed;
+ _conversation.UpdatedAt = DateTime.UtcNow;
+ UpdatedAt = DateTime.UtcNow;
+
+ // Raise domain event
+ AddDomainEvent(new ConversationClosedEvent(_conversation.Id, _conversation.OwnerId));
+ }
+
+ ///
+ /// Update conversation title
+ ///
+ public void UpdateTitle(string newTitle)
+ {
+ if (string.IsNullOrWhiteSpace(newTitle))
+ {
+ throw new ArgumentException("Title cannot be empty", nameof(newTitle));
+ }
+
+ // Business rule: Only owner or admins can update title
+ _conversation.Title = newTitle;
+ _conversation.UpdatedAt = DateTime.UtcNow;
+ UpdatedAt = DateTime.UtcNow;
+ }
+
+ ///
+ /// Change privacy settings
+ ///
+ public void SetPrivacy(bool isPrivate, UserRole userRole)
+ {
+ // Business rule: Only owner or admins can change privacy
+ if (userRole != UserRole.Admin && _conversation.OwnerId != Id)
+ {
+ throw new UnauthorizedAccessException("Only owner or admin can change privacy settings");
+ }
+
+ _conversation.IsPrivate = isPrivate;
+ _conversation.UpdatedAt = DateTime.UtcNow;
+ UpdatedAt = DateTime.UtcNow;
+ }
+
+ ///
+ /// Add tags to conversation
+ ///
+ public void AddTags(params string[] newTags)
+ {
+ if (newTags is null || newTags.Length == 0)
+ {
+ return;
+ }
+
+ var existingTags = _conversation.Tags.ToList();
+ var validNewTags = newTags
+ .Where(tag => !string.IsNullOrWhiteSpace(tag))
+ .Where(tag => !existingTags.Contains(tag, StringComparer.OrdinalIgnoreCase))
+ .ToList();
+
+ if (validNewTags.Count == 0)
+ {
+ return;
+ }
+
+ existingTags.AddRange(validNewTags);
+ _conversation.Tags = existingTags.ToArray();
+ _conversation.UpdatedAt = DateTime.UtcNow;
+ UpdatedAt = DateTime.UtcNow;
+ }
+
+ ///
+ /// Check if user can access this conversation
+ ///
+ public bool CanBeAccessedBy(Guid userId, UserRole[] userRoles)
+ {
+ return _conversation.IsAccessibleBy(userId, userRoles.Select(r => r.ToString()).ToArray());
+ }
+
+ // Private helper methods
+ private static int CalculateInitialPriority(bool isPrivate)
+ {
+ // Business rule: Private conversations get higher initial priority
+ return isPrivate ? 5 : 1;
+ }
+
+ private void UpdatePriorityBasedOnActivity()
+ {
+ // Business rule: Recent activity increases priority
+ var recentMessages = _messages.Count(m => m.IsRecent);
+ _conversation.Priority = Math.Min(10, _conversation.Priority + recentMessages);
+ }
+}
+
+///
+/// Conversation closed event
+///
+public sealed record ConversationClosedEvent : IDomainEvent
+{
+ public Guid ConversationId { get; }
+ public Guid OwnerId { get; }
+ public DateTime OccurredOn { get; }
+
+ public ConversationClosedEvent(Guid conversationId, Guid ownerId)
+ {
+ ConversationId = conversationId;
+ OwnerId = ownerId;
+ OccurredOn = DateTime.UtcNow;
+ }
+}
diff --git a/src/Domain/Aggregates/User/User.cs b/src/Domain/Aggregates/User/User.cs
new file mode 100644
index 0000000..a660dd5
--- /dev/null
+++ b/src/Domain/Aggregates/User/User.cs
@@ -0,0 +1,170 @@
+using Domain.Common;
+using Domain.Entities;
+using Domain.Events.User;
+using Domain.Enums;
+using Domain.ValueObjects;
+
+namespace Domain.Aggregates.User;
+
+///
+/// User aggregate root - Manages user business operations
+///
+public sealed class UserAggregate : BaseAggregateRoot
+{
+ private readonly Entities.User _user;
+
+ public UserAggregate(Entities.User user)
+ {
+ _user = user ?? throw new ArgumentNullException(nameof(user));
+ Id = user.Id;
+ }
+
+ // Expose user properties
+ public string Email => _user.Email;
+ public string FullName => _user.FullName;
+ public string PasswordHash => _user.PasswordHash;
+ public string? PhoneNumber => _user.PhoneNumber;
+ public UserRole[] Roles => _user.Roles;
+ public bool IsActive => _user.IsActive;
+
+ // Get the underlying entity
+ public Entities.User GetUser() => _user;
+
+ ///
+ /// Create a new user aggregate
+ ///
+ public static UserAggregate Create(string email, string fullName, string passwordHash, UserRole[]? roles = null)
+ {
+ // Validate business rules
+ if (string.IsNullOrWhiteSpace(email))
+ {
+ throw new ArgumentException("Email is required", nameof(email));
+ }
+
+ if (string.IsNullOrWhiteSpace(fullName))
+ {
+ throw new ArgumentException("Full name is required", nameof(fullName));
+ }
+
+ if (string.IsNullOrWhiteSpace(passwordHash))
+ {
+ throw new ArgumentException("Password hash is required", nameof(passwordHash));
+ }
+
+ // Create user entity
+ var user = new Entities.User
+ {
+ Email = email,
+ FullName = fullName,
+ PasswordHash = passwordHash,
+ Roles = roles ?? [UserRole.User]
+ };
+
+ var aggregate = new UserAggregate(user);
+
+ // Raise domain event
+ aggregate.AddDomainEvent(new UserCreatedEvent(user.Id, user.Email, user.FullName, user.Roles.Select(r => r.ToString()).ToArray()));
+
+ return aggregate;
+ }
+
+ ///
+ /// Deactivate user (soft delete)
+ ///
+ public void Deactivate()
+ {
+ // Business rule: Cannot deactivate if user has active conversations
+ if (HasActiveConversations())
+ {
+ throw new InvalidOperationException("Cannot deactivate user with active conversations");
+ }
+
+ _user.IsDeleted = true;
+ _user.DeletedAt = DateTime.UtcNow;
+ UpdatedAt = DateTime.UtcNow;
+
+ // Raise domain event
+ AddDomainEvent(new UserDeactivatedEvent(_user.Id));
+ }
+
+ ///
+ /// Update user information
+ ///
+ public void UpdateInfo(string fullName, string? phoneNumber = null)
+ {
+ if (string.IsNullOrWhiteSpace(fullName))
+ {
+ throw new ArgumentException("Full name is required", nameof(fullName));
+ }
+
+ _user.FullName = fullName;
+ _user.PhoneNumber = phoneNumber;
+ _user.UpdatedAt = DateTime.UtcNow;
+ UpdatedAt = DateTime.UtcNow;
+
+ // Raise domain event
+ AddDomainEvent(new UserUpdatedEvent(_user.Id, fullName, phoneNumber));
+ }
+
+ ///
+ /// Add role to user
+ ///
+ public void AddRole(UserRole role)
+ {
+ if (_user.Roles.Contains(role))
+ {
+ return;
+ }
+
+ var rolesList = _user.Roles.ToList();
+ rolesList.Add(role);
+ _user.Roles = rolesList.ToArray();
+ _user.UpdatedAt = DateTime.UtcNow;
+ UpdatedAt = DateTime.UtcNow;
+ }
+
+ ///
+ /// Remove role from user
+ ///
+ public void RemoveRole(UserRole role)
+ {
+ if (!_user.Roles.Contains(role))
+ {
+ return;
+ }
+
+ // Business rule: User must have at least one role
+ if (_user.Roles.Length <= 1)
+ {
+ throw new InvalidOperationException("User must have at least one role");
+ }
+
+ _user.Roles = _user.Roles.Where(r => r != role).ToArray();
+ _user.UpdatedAt = DateTime.UtcNow;
+ UpdatedAt = DateTime.UtcNow;
+ }
+
+ ///
+ /// Check if user can perform action
+ ///
+ public bool CanPerformAction(string action)
+ {
+ // Business logic for permissions
+ return action switch
+ {
+ "CreateConversation" => IsActive,
+ "EditProfile" => IsActive,
+ "ManageUsers" => _user.HasRole(UserRole.Admin),
+ "AccessLegalDatabase" => _user.HasAnyRole(UserRole.Admin, UserRole.LegalExpert),
+ _ => false
+ };
+ }
+
+ // Private helper methods
+ private bool HasActiveConversations()
+ {
+ // This would typically be checked via a domain service
+ // For now, assume false
+ return false;
+ }
+}
diff --git a/src/Domain/Common/BaseAggregateRoot.cs b/src/Domain/Common/BaseAggregateRoot.cs
new file mode 100644
index 0000000..9dc7963
--- /dev/null
+++ b/src/Domain/Common/BaseAggregateRoot.cs
@@ -0,0 +1,38 @@
+namespace Domain.Common;
+
+///
+/// Base aggregate root with domain events
+///
+public abstract class BaseAggregateRoot : BaseEntity
+{
+ private readonly List _domainEvents = [];
+
+ ///
+ /// Domain events raised by this aggregate
+ ///
+ public IReadOnlyCollection DomainEvents => _domainEvents.AsReadOnly();
+
+ ///
+ /// Add a domain event
+ ///
+ protected void AddDomainEvent(IDomainEvent domainEvent)
+ {
+ _domainEvents.Add(domainEvent);
+ }
+
+ ///
+ /// Remove a domain event
+ ///
+ protected void RemoveDomainEvent(IDomainEvent domainEvent)
+ {
+ _domainEvents.Remove(domainEvent);
+ }
+
+ ///
+ /// Clear all domain events
+ ///
+ public void ClearDomainEvents()
+ {
+ _domainEvents.Clear();
+ }
+}
diff --git a/src/Domain/Common/BaseEntity.cs b/src/Domain/Common/BaseEntity.cs
new file mode 100644
index 0000000..404b7fa
--- /dev/null
+++ b/src/Domain/Common/BaseEntity.cs
@@ -0,0 +1,38 @@
+namespace Domain.Common;
+
+///
+/// Base entity with common properties
+///
+public abstract class BaseEntity
+{
+ ///
+ /// Entity identifier
+ ///
+ public Guid Id { get; set; }
+
+ ///
+ /// Creation timestamp
+ ///
+ public DateTime CreatedAt { get; set; }
+
+ ///
+ /// Last update timestamp
+ ///
+ public DateTime? UpdatedAt { get; set; }
+
+ ///
+ /// Soft delete flag
+ ///
+ public bool IsDeleted { get; set; }
+
+ ///
+ /// When entity was deleted
+ ///
+ public DateTime? DeletedAt { get; set; }
+
+ protected BaseEntity()
+ {
+ Id = Guid.NewGuid();
+ CreatedAt = DateTime.UtcNow;
+ }
+}
diff --git a/src/Domain/Common/Error.cs b/src/Domain/Common/Error.cs
new file mode 100644
index 0000000..5bbdf08
--- /dev/null
+++ b/src/Domain/Common/Error.cs
@@ -0,0 +1,35 @@
+namespace Domain.Common;
+
+public record Error
+{
+ public static readonly Error None = new(string.Empty, string.Empty, ErrorType.Failure);
+ public static readonly Error NullValue = new(
+ "General.Null",
+ "Null value was provided",
+ ErrorType.Failure);
+
+ public Error(string code, string description, ErrorType type)
+ {
+ Code = code;
+ Description = description;
+ Type = type;
+ }
+
+ public string Code { get; }
+
+ public string Description { get; }
+
+ public ErrorType Type { get; }
+
+ public static Error Failure(string code, string description) =>
+ new(code, description, ErrorType.Failure);
+
+ public static Error NotFound(string code, string description) =>
+ new(code, description, ErrorType.NotFound);
+
+ public static Error Problem(string code, string description) =>
+ new(code, description, ErrorType.Problem);
+
+ public static Error Conflict(string code, string description) =>
+ new(code, description, ErrorType.Conflict);
+}
diff --git a/src/Domain/Common/ErrorType.cs b/src/Domain/Common/ErrorType.cs
new file mode 100644
index 0000000..ff8f68b
--- /dev/null
+++ b/src/Domain/Common/ErrorType.cs
@@ -0,0 +1,15 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading.Tasks;
+
+namespace Domain.Common;
+
+public enum ErrorType
+{
+ Failure = 0,
+ Validation = 1,
+ Problem = 2,
+ NotFound = 3,
+ Conflict = 4
+}
diff --git a/src/Domain/Common/IDomainEvent.cs b/src/Domain/Common/IDomainEvent.cs
new file mode 100644
index 0000000..f47633e
--- /dev/null
+++ b/src/Domain/Common/IDomainEvent.cs
@@ -0,0 +1,14 @@
+using MediatR;
+
+namespace Domain.Common;
+
+///
+/// Marker interface for domain events
+///
+public interface IDomainEvent : INotification
+{
+ ///
+ /// When the event occurred
+ ///
+ DateTime OccurredOn { get; }
+}
diff --git a/src/Domain/Common/Result.cs b/src/Domain/Common/Result.cs
new file mode 100644
index 0000000..69e0c50
--- /dev/null
+++ b/src/Domain/Common/Result.cs
@@ -0,0 +1,56 @@
+using System.Diagnostics.CodeAnalysis;
+
+namespace Domain.Common;
+
+public class Result
+{
+ public Result(bool isSuccess, Error error)
+ {
+ if (isSuccess && error != Error.None ||
+ !isSuccess && error == Error.None)
+ {
+ throw new ArgumentException("Invalid error", nameof(error));
+ }
+
+ IsSuccess = isSuccess;
+ Error = error;
+ }
+
+ public bool IsSuccess { get; }
+
+ public bool IsFailure => !IsSuccess;
+
+ public Error Error { get; }
+
+ public static Result Success() => new(true, Error.None);
+
+ public static Result Success(TValue value) =>
+ new(value, true, Error.None);
+
+ public static Result Failure(Error error) => new(false, error);
+
+ public static Result Failure(Error error) =>
+ new(default, false, error);
+}
+
+public class Result : Result
+{
+ private readonly TValue? _value;
+
+ public Result(TValue? value, bool isSuccess, Error error)
+ : base(isSuccess, error)
+ {
+ _value = value;
+ }
+
+ [NotNull]
+ public TValue Value => IsSuccess
+ ? _value!
+ : throw new InvalidOperationException("The value of a failure result can't be accessed.");
+
+ public static implicit operator Result(TValue? value) =>
+ value is not null ? Success(value) : Failure(Error.NullValue);
+
+ public static Result ValidationFailure(Error error) =>
+ new(default, false, error);
+}
diff --git a/src/Domain/Common/ValidationError.cs b/src/Domain/Common/ValidationError.cs
new file mode 100644
index 0000000..486e6e0
--- /dev/null
+++ b/src/Domain/Common/ValidationError.cs
@@ -0,0 +1,18 @@
+namespace Domain.Common;
+
+public sealed record ValidationError : Error
+{
+ public ValidationError(Error[] errors)
+ : base(
+ "Validation.General",
+ "One or more validation errors occurred",
+ ErrorType.Validation)
+ {
+ Errors = errors;
+ }
+
+ public Error[] Errors { get; }
+
+ public static ValidationError FromResults(IEnumerable results) =>
+ new(results.Where(r => r.IsFailure).Select(r => r.Error).ToArray());
+}
diff --git a/src/Domain/Domain.csproj b/src/Domain/Domain.csproj
new file mode 100644
index 0000000..a19c932
--- /dev/null
+++ b/src/Domain/Domain.csproj
@@ -0,0 +1,13 @@
+
+
+
+ net8.0
+ enable
+ enable
+
+
+
+
+
+
+
diff --git a/src/Domain/Entities/Conversation.cs b/src/Domain/Entities/Conversation.cs
new file mode 100644
index 0000000..2bfd9d6
--- /dev/null
+++ b/src/Domain/Entities/Conversation.cs
@@ -0,0 +1,67 @@
+using Domain.Common;
+
+namespace Domain.Entities;
+
+///
+/// Conversation entity - Pure domain entity
+///
+public sealed class Conversation : BaseEntity
+{
+ public required Guid OwnerId { get; set; }
+ public required string Title { get; set; }
+ public bool IsPrivate { get; set; }
+ public string[] Tags { get; set; } = [];
+ public int Priority { get; set; }
+ public ConversationStatus Status { get; set; } = ConversationStatus.Active;
+
+ // Navigation properties
+ public User Owner { get; set; } = null!;
+ public ICollection Messages { get; set; } = [];
+
+ ///
+ /// Check if conversation is accessible by user
+ ///
+ public bool IsAccessibleBy(Guid userId, string[] userRoles)
+ {
+ // Owner can always access
+ if (OwnerId == userId)
+ {
+ return true;
+ }
+
+ // Admin can access all
+ if (userRoles.Contains("Admin"))
+ {
+ return true;
+ }
+
+ // Legal experts can access non-private conversations
+ if (userRoles.Contains("LegalExpert") && !IsPrivate)
+ {
+ return true;
+ }
+
+ return false;
+ }
+
+ ///
+ /// Check if conversation is active
+ ///
+ public bool IsActive => Status == ConversationStatus.Active && !IsDeleted;
+
+ ///
+ /// Get message count
+ ///
+ public int MessageCount => Messages?.Count ?? 0;
+}
+
+///
+/// Conversation status enumeration
+///
+public enum ConversationStatus
+{
+ Active = 0,
+ Closed = 1,
+ Archived = 2,
+ Suspended = 3
+}
diff --git a/src/Domain/Entities/Message.cs b/src/Domain/Entities/Message.cs
new file mode 100644
index 0000000..8368fdd
--- /dev/null
+++ b/src/Domain/Entities/Message.cs
@@ -0,0 +1,81 @@
+using Domain.Common;
+
+namespace Domain.Entities;
+
+///
+/// Message entity - Pure domain entity
+///
+public sealed class Message : BaseEntity
+{
+ public required Guid ConversationId { get; set; }
+ public required Guid SenderId { get; set; }
+ public required string Content { get; set; }
+ public MessageType Type { get; set; } = MessageType.Text;
+ public string? Metadata { get; set; } // JSON metadata for attachments, etc.
+ public bool IsFromBot { get; set; }
+ public bool IsEdited { get; set; }
+ public DateTime? EditedAt { get; set; }
+
+ // Navigation properties
+ public Conversation Conversation { get; set; } = null!;
+ public User Sender { get; set; } = null!;
+
+ ///
+ /// Check if message can be edited
+ ///
+ public bool CanBeEditedBy(Guid userId)
+ {
+ // Only sender can edit
+ if (SenderId != userId)
+ {
+ return false;
+ }
+
+ // Bot messages cannot be edited
+ if (IsFromBot)
+ {
+ return false;
+ }
+
+ // Can only edit within 5 minutes
+ if (DateTime.UtcNow - CreatedAt > TimeSpan.FromMinutes(5))
+ {
+ return false;
+ }
+
+ return true;
+ }
+
+ ///
+ /// Check if message is recent (within 1 hour)
+ ///
+ public bool IsRecent => DateTime.UtcNow - CreatedAt <= TimeSpan.FromHours(1);
+
+ ///
+ /// Get content preview (first 100 characters)
+ ///
+ public string GetPreview(int maxLength = 100)
+ {
+ if (string.IsNullOrWhiteSpace(Content))
+ {
+ return string.Empty;
+ }
+
+ return Content.Length <= maxLength
+ ? Content
+ : Content[..maxLength] + "...";
+ }
+}
+
+///
+/// Message types enumeration
+///
+public enum MessageType
+{
+ Text = 0,
+ Image = 1,
+ Document = 2,
+ Audio = 3,
+ System = 4,
+ LegalAdvice = 5
+}
diff --git a/src/Domain/Entities/User.cs b/src/Domain/Entities/User.cs
new file mode 100644
index 0000000..6273d73
--- /dev/null
+++ b/src/Domain/Entities/User.cs
@@ -0,0 +1,40 @@
+using Domain.Common;
+using Domain.Enums;
+
+namespace Domain.Entities;
+
+///
+/// User entity - Pure domain entity
+///
+public sealed class User : BaseEntity
+{
+ public required string Email { get; set; }
+ public required string FullName { get; set; }
+ public required string PasswordHash { get; set; }
+ public string? PhoneNumber { get; set; }
+ public UserRole[] Roles { get; set; } = [];
+
+ // Navigation properties for related entities
+ public ICollection OwnedConversations { get; set; } = [];
+ public ICollection Messages { get; set; } = [];
+
+ ///
+ /// Check if user has specific role
+ ///
+ public bool HasRole(UserRole role) => Roles.Contains(role);
+
+ ///
+ /// Check if user has any of the specified roles
+ ///
+ public bool HasAnyRole(params UserRole[] roles) => roles.Any(role => Roles.Contains(role));
+
+ ///
+ /// Validate email format (basic validation in entity)
+ ///
+ public bool IsEmailValid() => !string.IsNullOrWhiteSpace(Email) && Email.Contains('@');
+
+ ///
+ /// Check if user is active
+ ///
+ public bool IsActive => !IsDeleted;
+}
diff --git a/src/Domain/Enums/UserRole.cs b/src/Domain/Enums/UserRole.cs
new file mode 100644
index 0000000..5553aec
--- /dev/null
+++ b/src/Domain/Enums/UserRole.cs
@@ -0,0 +1,62 @@
+namespace Domain.Enums;
+
+///
+/// User roles enumeration
+///
+public enum UserRole
+{
+ ///
+ /// Regular user
+ ///
+ User = 0,
+
+ ///
+ /// Administrator
+ ///
+ Admin = 1,
+
+ ///
+ /// Manager
+ ///
+ Manager = 2,
+
+ ///
+ /// Legal expert
+ ///
+ LegalExpert = 3,
+
+ ///
+ /// Premium user with extended features
+ ///
+ Premium = 4,
+
+ ///
+ /// System user (for automated processes)
+ ///
+ System = 5
+}
+
+///
+/// Extension methods for UserRole enum
+///
+public static class UserRoleExtensions
+{
+ ///
+ /// Get display name for role
+ ///
+ public static string GetDisplayName(this UserRole role) => role switch
+ {
+ UserRole.User => "User",
+ UserRole.Admin => "Administrator",
+ UserRole.Manager => "Manager",
+ UserRole.LegalExpert => "Legal Expert",
+ UserRole.Premium => "Premium User",
+ UserRole.System => "System",
+ _ => "Unknown"
+ };
+
+ ///
+ /// Check if role has elevated privileges
+ ///
+ public static bool IsElevated(this UserRole role) => role is UserRole.LegalExpert or UserRole.Manager or UserRole.Admin or UserRole.System;
+}
diff --git a/src/Domain/Events/Conversation/ConversationCreatedEvent.cs b/src/Domain/Events/Conversation/ConversationCreatedEvent.cs
new file mode 100644
index 0000000..6e8e0a4
--- /dev/null
+++ b/src/Domain/Events/Conversation/ConversationCreatedEvent.cs
@@ -0,0 +1,24 @@
+using Domain.Common;
+
+namespace Domain.Events.Conversation;
+
+///
+/// Domain event raised when a new conversation is created
+///
+public sealed record ConversationCreatedEvent : IDomainEvent
+{
+ public Guid ConversationId { get; }
+ public Guid OwnerId { get; }
+ public string Title { get; }
+ public bool IsPrivate { get; }
+ public DateTime OccurredOn { get; }
+
+ public ConversationCreatedEvent(Guid conversationId, Guid ownerId, string title, bool isPrivate)
+ {
+ ConversationId = conversationId;
+ OwnerId = ownerId;
+ Title = title;
+ IsPrivate = isPrivate;
+ OccurredOn = DateTime.UtcNow;
+ }
+}
diff --git a/src/Domain/Events/Examples/DomainEventsUsageExamples.cs b/src/Domain/Events/Examples/DomainEventsUsageExamples.cs
new file mode 100644
index 0000000..20b4482
--- /dev/null
+++ b/src/Domain/Events/Examples/DomainEventsUsageExamples.cs
@@ -0,0 +1,173 @@
+using Domain.Aggregates.User;
+using Domain.Events.User;
+using Domain.Common;
+
+namespace Domain.Events.Examples;
+
+///
+/// Examples demonstrating when and how to use Domain Events
+///
+public static class DomainEventsUsageExamples
+{
+ ///
+ /// Example 1: User Registration Flow
+ /// Khi user đăng ký, cần trigger nhiều side effects
+ ///
+ public static void UserRegistrationExample()
+ {
+ // 1. Tạo user aggregate
+ var user = Domain.Aggregates.User.User.Create("john@example.com", "John Doe", "hashedPassword", ["User"]);
+
+ // 2. Domain event sẽ được raise automatically trong User.Create()
+ // UserCreatedEvent sẽ trigger:
+ // - Send welcome email
+ // - Create user profile
+ // - Initialize settings
+ // - Track analytics
+
+ // 3. Save user to database (events sẽ được dispatch sau khi save)
+ // await userRepository.CreateAsync(user);
+ }
+
+ ///
+ /// Example 2: User Deactivation Flow
+ /// Khi deactivate user, cần clean up tất cả related data
+ ///
+ public static void UserDeactivationExample()
+ {
+ // 1. Load user from repository
+ // var user = await userRepository.GetByIdAsync(userId);
+
+ // 2. Deactivate user
+ // user.Deactivate();
+
+ // 3. Domain event sẽ được raise automatically
+ // UserDeactivatedEvent sẽ trigger:
+ // - Revoke all sessions
+ // - Cancel subscriptions
+ // - Archive data
+ // - Send notification
+
+ // 4. Save changes (events sẽ được dispatch)
+ // await userRepository.UpdateAsync(user);
+ }
+
+ ///
+ /// Example 3: Legal Consultation Started
+ /// Complex business flow with multiple domain events
+ ///
+ public static void LegalConsultationExample()
+ {
+ // Scenario: User starts a legal consultation
+
+ // 1. ConversationCreatedEvent triggers:
+ // - Assign legal expert
+ // - Create initial assessment
+ // - Setup billing
+ // - Send notifications
+
+ // 2. UserConsultationStartedEvent triggers:
+ // - Update user subscription usage
+ // - Track analytics
+ // - Create case file
+
+ // 3. ExpertAssignedEvent triggers:
+ // - Notify expert
+ // - Update expert workload
+ // - Schedule initial meeting
+ }
+
+ ///
+ /// Example 4: Payment Processing
+ /// Financial domain events for legal services
+ ///
+ public static void PaymentProcessingExample()
+ {
+ // Scenario: User pays for legal consultation
+
+ // 1. PaymentInitiatedEvent
+ // 2. PaymentProcessedEvent triggers:
+ // - Update user account balance
+ // - Grant access to premium features
+ // - Send receipt
+ // - Update billing history
+
+ // 3. ServiceAccessGrantedEvent triggers:
+ // - Enable premium features
+ // - Send confirmation
+ // - Update user tier
+ }
+}
+
+///
+/// Best practices for Domain Events
+///
+public static class DomainEventsBestPractices
+{
+ ///
+ /// 1. Events should be IMMUTABLE (sử dụng record)
+ /// 2. Events should contain enough data to handle side effects
+ /// 3. Events should be named in past tense (UserCreated, not CreateUser)
+ /// 4. Keep events focused on single responsibility
+ /// 5. Don't put business logic in events - use handlers
+ ///
+ public static void BestPracticesExample()
+ {
+ // ✅ GOOD: Immutable, descriptive, past tense
+ var userCreated = new UserCreatedEvent(
+ userId: Guid.NewGuid(),
+ email: "user@example.com",
+ fullName: "John Doe",
+ roles: ["User"]
+ );
+
+ // ❌ BAD: Mutable, confusing name
+ // var createUserEvent = new CreateUserEvent { ... };
+ }
+}
+
+///
+/// Common Domain Events in Legal Assistant System
+///
+public static class LegalAssistantDomainEvents
+{
+ ///
+ /// User Domain Events:
+ /// - UserCreatedEvent
+ /// - UserDeactivatedEvent
+ /// - UserUpdatedEvent
+ /// - UserSubscriptionChangedEvent
+ /// - UserTierUpgradedEvent
+ ///
+ public static void UserEvents() { }
+
+ ///
+ /// Conversation Domain Events:
+ /// - ConversationCreatedEvent
+ /// - ConversationClosedEvent
+ /// - MessageSentEvent
+ /// - ExpertAssignedEvent
+ /// - ConversationRatedEvent
+ ///
+ public static void ConversationEvents() { }
+
+ ///
+ /// Legal Service Domain Events:
+ /// - ConsultationStartedEvent
+ /// - ConsultationCompletedEvent
+ /// - DocumentGeneratedEvent
+ /// - LegalAdviceProvidedEvent
+ /// - CaseFileCreatedEvent
+ ///
+ public static void LegalServiceEvents() { }
+
+ ///
+ /// Payment Domain Events:
+ /// - PaymentInitiatedEvent
+ /// - PaymentCompletedEvent
+ /// - PaymentFailedEvent
+ /// - RefundProcessedEvent
+ /// - InvoiceGeneratedEvent
+ ///
+ public static void PaymentEvents() { }
+}
diff --git a/src/Domain/Events/User/UserCreatedEvent.cs b/src/Domain/Events/User/UserCreatedEvent.cs
new file mode 100644
index 0000000..cca2511
--- /dev/null
+++ b/src/Domain/Events/User/UserCreatedEvent.cs
@@ -0,0 +1,24 @@
+using Domain.Common;
+
+namespace Domain.Events.User;
+
+///
+/// Domain event raised when a new user is created
+///
+public sealed record UserCreatedEvent : IDomainEvent
+{
+ public Guid UserId { get; }
+ public string Email { get; }
+ public string FullName { get; }
+ public string[] Roles { get; }
+ public DateTime OccurredOn { get; }
+
+ public UserCreatedEvent(Guid userId, string email, string fullName, string[] roles)
+ {
+ UserId = userId;
+ Email = email;
+ FullName = fullName;
+ Roles = roles;
+ OccurredOn = DateTime.UtcNow;
+ }
+}
diff --git a/src/Domain/Events/User/UserDeactivatedEvent.cs b/src/Domain/Events/User/UserDeactivatedEvent.cs
new file mode 100644
index 0000000..c75f62d
--- /dev/null
+++ b/src/Domain/Events/User/UserDeactivatedEvent.cs
@@ -0,0 +1,18 @@
+using Domain.Common;
+
+namespace Domain.Events.User;
+
+///
+/// Domain event raised when a user is deactivated
+///
+public sealed record UserDeactivatedEvent : IDomainEvent
+{
+ public Guid UserId { get; }
+ public DateTime OccurredOn { get; }
+
+ public UserDeactivatedEvent(Guid userId)
+ {
+ UserId = userId;
+ OccurredOn = DateTime.UtcNow;
+ }
+}
diff --git a/src/Domain/Events/User/UserUpdatedEvent.cs b/src/Domain/Events/User/UserUpdatedEvent.cs
new file mode 100644
index 0000000..c391460
--- /dev/null
+++ b/src/Domain/Events/User/UserUpdatedEvent.cs
@@ -0,0 +1,22 @@
+using Domain.Common;
+
+namespace Domain.Events.User;
+
+///
+/// Domain event raised when user information is updated
+///
+public sealed record UserUpdatedEvent : IDomainEvent
+{
+ public Guid UserId { get; }
+ public string FullName { get; }
+ public string? PhoneNumber { get; }
+ public DateTime OccurredOn { get; }
+
+ public UserUpdatedEvent(Guid userId, string fullName, string? phoneNumber)
+ {
+ UserId = userId;
+ FullName = fullName;
+ PhoneNumber = phoneNumber;
+ OccurredOn = DateTime.UtcNow;
+ }
+}
diff --git a/src/Domain/README.md b/src/Domain/README.md
new file mode 100644
index 0000000..272c336
--- /dev/null
+++ b/src/Domain/README.md
@@ -0,0 +1,129 @@
+# 🏛️ Domain Layer
+
+**Đây là lõi của ứng dụng - chứa business logic thuần túy, không phụ thuộc vào bất kỳ layer nào khác.**
+
+## 📁 Cấu trúc Folders
+
+```
+Domain/
+├── Entities/ # 🏛️ Pure Domain Entities
+│ ├── User.cs # User entity (data + basic behavior)
+│ ├── Conversation.cs # Conversation entity
+│ └── Message.cs # Message entity
+│
+├── Aggregates/ # 🏗️ Aggregate Roots (Business Logic)
+│ ├── User/
+│ │ └── UserAggregate.cs # User business operations
+│ └── Conversation/
+│ └── ConversationAggregate.cs # Conversation business operations
+│
+├── Common/ # 🔗 Base Classes & Shared Types
+│ ├── BaseEntity.cs # Base entity với Id, timestamps
+│ ├── BaseAggregateRoot.cs # Base cho aggregates + domain events
+│ ├── IDomainEvent.cs # Interface cho domain events
+│ ├── Result.cs # Result pattern cho error handling
+│ └── Error.cs # Error types và codes
+│
+├── Events/ # 📢 Domain Events
+│ ├── User/
+│ │ ├── UserCreatedEvent.cs # Khi user được tạo
+│ │ ├── UserUpdatedEvent.cs # Khi user được cập nhật
+│ │ └── UserDeactivatedEvent.cs # Khi user bị vô hiệu hóa
+│ ├── Conversation/
+│ └── Examples/ # Ví dụ cách sử dụng domain events
+│
+├── Enums/ # 📋 Domain Enumerations
+│ └── UserRole.cs # Roles: Admin, User, LegalExpert, etc.
+│
+├── Services/ # 🛠️ Domain Services
+│ ├── UserDomainService.cs # Business logic không thuộc 1 entity
+│ └── ConversationDomainService.cs # Logic liên quan conversation
+│
+├── Specifications/ # 🔍 Specification Pattern
+│ ├── ISpecification.cs # Base interface
+│ ├── CompositeSpecifications.cs # And, Or, Not operations
+│ ├── UserSpecifications.cs # User-specific queries
+│ └── ConversationSpecifications.cs
+│
+└── ValueObjects/ # 💎 Value Objects
+ ├── Email.cs # Email với validation
+ └── Password.cs # Password với hashing rules
+```
+
+## 🎯 Mục đích từng folder
+
+### 🏛️ **Entities/** - Pure Domain Objects
+- **Domain Entities** - Objects với identity và basic behavior
+- **Data + Basic Logic** - Properties và simple methods
+- **Navigation Properties** - Relationships với other entities
+- **Validation** - Basic domain validation
+
+**Ví dụ:** `User.HasRole()`, `Conversation.IsAccessibleBy()`, `Message.CanBeEditedBy()`
+
+### 🏗️ **Aggregates/** - Business Logic Orchestrators
+- **Aggregate Roots** - Manage business operations và consistency
+- **Complex Business Logic** - Operations spanning multiple entities
+- **Domain Events** - Raise events khi business actions occur
+- **Invariants** - Enforce complex business rules
+
+**Ví dụ:** `UserAggregate.Create()`, `ConversationAggregate.AddMessage()`, `UserAggregate.Deactivate()`
+
+### 📢 **Events/**
+- **Domain Events** - Thông báo khi có sự kiện quan trọng xảy ra
+- **Immutable** - Events không thể thay đổi sau khi tạo
+- **Past Tense** - Named theo sự kiện đã xảy ra
+
+**Khi nào dùng:** Khi cần trigger side effects hoặc notify other contexts
+
+### 🛠️ **Services/**
+- **Domain Services** - Logic không thuộc về 1 entity cụ thể
+- **Coordination** - Phối hợp giữa nhiều aggregates
+- **Complex Business Rules** - Rules phức tạp cần nhiều inputs
+
+**Ví dụ:** Check permissions, calculate complex business values
+
+### 🔍 **Specifications/**
+- **Query Logic** - Encapsulate complex query conditions
+- **Reusable** - Có thể combine và reuse
+- **Testable** - Dễ test business query logic
+
+**Ví dụ:** `ActiveUserSpecification`, `AdminUserSpecification`
+
+### 💎 **ValueObjects/**
+- **Immutable** - Không thể thay đổi sau khi tạo
+- **Validation** - Built-in validation rules
+- **Equality** - So sánh theo value, không theo reference
+
+**Ví dụ:** Email, Money, Address, Phone Number
+
+## 🚫 Điều KHÔNG ĐƯỢC làm trong Domain
+
+- ❌ **Không reference** Infrastructure hoặc UI layers
+- ❌ **Không có** database concerns (EF, SQL, etc.)
+- ❌ **Không có** HTTP, JSON, hoặc external service calls
+- ❌ **Không có** logging frameworks (chỉ có abstractions)
+- ❌ **Không có** dependency injection containers
+
+## ✅ Điều NÊN làm trong Domain
+
+- ✅ **Pure business logic** - Logic nghiệp vụ thuần túy
+- ✅ **Domain events** - Thông báo business events
+- ✅ **Validation rules** - Business validation
+- ✅ **Invariants** - Business constraints
+- ✅ **Factory methods** - Tạo objects đúng cách
+
+## 🔗 Dependencies
+
+**Domain layer KHÔNG phụ thuộc vào layer nào khác!**
+
+Chỉ có thể reference:
+- .NET BCL (Base Class Library)
+- MediatR (chỉ cho INotification interface)
+
+## 💡 Best Practices
+
+1. **Rich Domain Models** - Entities có behavior, không chỉ data
+2. **Tell, Don't Ask** - Gọi methods thay vì manipulate properties
+3. **Aggregate Boundaries** - Giữ aggregates nhỏ và focused
+4. **Domain Events** - Communicate giữa aggregates qua events
+5. **Ubiquitous Language** - Dùng ngôn ngữ của business domain
diff --git a/src/Domain/Services/ConversationDomainService.cs b/src/Domain/Services/ConversationDomainService.cs
new file mode 100644
index 0000000..00eed73
--- /dev/null
+++ b/src/Domain/Services/ConversationDomainService.cs
@@ -0,0 +1,71 @@
+namespace Domain.Services;
+
+///
+/// Domain service for conversation-related business logic
+///
+public sealed class ConversationDomainService
+{
+ ///
+ /// Check if user can access conversation
+ ///
+ public bool CanUserAccessConversation(
+ Guid userId,
+ string[] userRoles,
+ Guid conversationOwnerId,
+ bool isConversationPrivate)
+ {
+ // Business rule: Owner can always access
+ if (userId == conversationOwnerId)
+ {
+ return true;
+ }
+
+ // Business rule: Admin can access all conversations
+ if (userRoles.Contains("Admin"))
+ {
+ return true;
+ }
+
+ // Business rule: Legal experts can access non-private conversations
+ if (userRoles.Contains("LegalExpert") && !isConversationPrivate)
+ {
+ return true;
+ }
+
+ // Business rule: Regular users can't access others' private conversations
+ return false;
+ }
+
+ ///
+ /// Calculate conversation priority based on business rules
+ ///
+ public int CalculateConversationPriority(
+ string[] userRoles,
+ string conversationTopic,
+ DateTime createdAt)
+ {
+ var priority = 0;
+
+ // Business rule: Premium users get higher priority
+ if (userRoles.Contains("Premium"))
+ {
+ priority += 10;
+ }
+
+ // Business rule: Legal emergencies get highest priority
+ if (conversationTopic.Contains("urgent", StringComparison.OrdinalIgnoreCase) ||
+ conversationTopic.Contains("emergency", StringComparison.OrdinalIgnoreCase))
+ {
+ priority += 20;
+ }
+
+ // Business rule: Older conversations get higher priority
+ var daysSinceCreated = (DateTime.UtcNow - createdAt).Days;
+ if (daysSinceCreated > 7)
+ {
+ priority += 5;
+ }
+
+ return priority;
+ }
+}
diff --git a/src/Domain/Services/UserDomainService.cs b/src/Domain/Services/UserDomainService.cs
new file mode 100644
index 0000000..5c3edfb
--- /dev/null
+++ b/src/Domain/Services/UserDomainService.cs
@@ -0,0 +1,52 @@
+namespace Domain.Services;
+
+///
+/// Domain service for user-related business logic
+///
+public sealed class UserDomainService
+{
+ ///
+ /// Check if user can be assigned to specific role
+ ///
+ public bool CanAssignRole(Domain.Aggregates.User.User user, string newRole)
+ {
+ // Business rule: Admin can't be demoted if they're the last admin
+ if (user.Roles.Contains("Admin") && newRole != "Admin")
+ {
+ // This would require checking other admins - complex business logic
+ // that doesn't belong to User entity alone
+ return true; // Simplified for example
+ }
+
+ // Business rule: System role can't be assigned manually
+ if (newRole == "System")
+ {
+ return false;
+ }
+
+ return true;
+ }
+
+ ///
+ /// Calculate user's effective permissions based on roles
+ ///
+ public string[] CalculateEffectivePermissions(string[] roles)
+ {
+ var permissions = new List();
+
+ foreach (var role in roles)
+ {
+ permissions.AddRange(role switch
+ {
+ "Admin" => ["users.create", "users.read", "users.update", "users.delete",
+ "conversations.read", "conversations.delete", "system.admin"],
+ "Manager" => ["users.read", "conversations.read", "conversations.manage"],
+ "LegalExpert" => ["conversations.read", "legal.advice", "documents.review"],
+ "User" => ["conversations.create", "conversations.read.own"],
+ _ => []
+ });
+ }
+
+ return [.. permissions.Distinct()];
+ }
+}
diff --git a/src/Domain/Specifications/CompositeSpecifications.cs b/src/Domain/Specifications/CompositeSpecifications.cs
new file mode 100644
index 0000000..8a202df
--- /dev/null
+++ b/src/Domain/Specifications/CompositeSpecifications.cs
@@ -0,0 +1,71 @@
+using System.Linq.Expressions;
+
+namespace Domain.Specifications;
+
+///
+/// AND specification
+///
+internal sealed class AndSpecification(ISpecification left, ISpecification right) : Specification
+{
+ public override Expression> ToExpression()
+ {
+ var leftExpression = left.ToExpression();
+ var rightExpression = right.ToExpression();
+
+ var parameter = Expression.Parameter(typeof(T));
+ var leftVisitor = new ReplaceExpressionVisitor(leftExpression.Parameters[0], parameter);
+ var rightVisitor = new ReplaceExpressionVisitor(rightExpression.Parameters[0], parameter);
+
+ var newLeft = leftVisitor.Visit(leftExpression.Body);
+ var newRight = rightVisitor.Visit(rightExpression.Body);
+
+ return Expression.Lambda>(Expression.AndAlso(newLeft!, newRight!), parameter);
+ }
+}
+
+///
+/// OR specification
+///
+internal sealed class OrSpecification(ISpecification left, ISpecification right) : Specification
+{
+ public override Expression> ToExpression()
+ {
+ var leftExpression = left.ToExpression();
+ var rightExpression = right.ToExpression();
+
+ var parameter = Expression.Parameter(typeof(T));
+ var leftVisitor = new ReplaceExpressionVisitor(leftExpression.Parameters[0], parameter);
+ var rightVisitor = new ReplaceExpressionVisitor(rightExpression.Parameters[0], parameter);
+
+ var newLeft = leftVisitor.Visit(leftExpression.Body);
+ var newRight = rightVisitor.Visit(rightExpression.Body);
+
+ return Expression.Lambda>(Expression.OrElse(newLeft!, newRight!), parameter);
+ }
+}
+
+///
+/// NOT specification
+///
+internal sealed class NotSpecification(ISpecification specification) : Specification
+{
+ public override Expression> ToExpression()
+ {
+ var expression = specification.ToExpression();
+ var parameter = expression.Parameters[0];
+ var body = Expression.Not(expression.Body);
+
+ return Expression.Lambda>(body, parameter);
+ }
+}
+
+///
+/// Helper class to replace expression parameters
+///
+internal sealed class ReplaceExpressionVisitor(Expression oldValue, Expression newValue) : ExpressionVisitor
+{
+ public override Expression? Visit(Expression? node)
+ {
+ return node == oldValue ? newValue : base.Visit(node);
+ }
+}
diff --git a/src/Domain/Specifications/ConversationSpecifications.cs b/src/Domain/Specifications/ConversationSpecifications.cs
new file mode 100644
index 0000000..c67867c
--- /dev/null
+++ b/src/Domain/Specifications/ConversationSpecifications.cs
@@ -0,0 +1,86 @@
+using System.Linq.Expressions;
+
+namespace Domain.Specifications;
+
+///
+/// Placeholder conversation entity for specifications
+///
+public class Conversation
+{
+ public Guid Id { get; set; }
+ public Guid OwnerId { get; set; }
+ public string Title { get; set; } = string.Empty;
+ public bool IsPrivate { get; set; }
+ public DateTime CreatedAt { get; set; }
+ public bool IsDeleted { get; set; }
+ public string[] Tags { get; set; } = [];
+ public int Priority { get; set; }
+}
+
+///
+/// Specification for active conversations
+///
+public sealed class ActiveConversationSpecification : Specification
+{
+ public override Expression> ToExpression()
+ {
+ return conv => !conv.IsDeleted;
+ }
+}
+
+///
+/// Specification for conversations owned by user
+///
+public sealed class ConversationOwnedBySpecification(Guid userId) : Specification
+{
+ public override Expression> ToExpression()
+ {
+ return conv => conv.OwnerId == userId;
+ }
+}
+
+///
+/// Specification for public conversations
+///
+public sealed class PublicConversationSpecification : Specification
+{
+ public override Expression> ToExpression()
+ {
+ return conv => !conv.IsPrivate;
+ }
+}
+
+///
+/// Specification for high priority conversations
+///
+public sealed class HighPriorityConversationSpecification : Specification
+{
+ public override Expression> ToExpression()
+ {
+ return conv => conv.Priority >= 10;
+ }
+}
+
+///
+/// Specification for conversations with specific tag
+///
+public sealed class ConversationWithTagSpecification(string tag) : Specification
+{
+ public override Expression> ToExpression()
+ {
+ return conv => conv.Tags.Contains(tag);
+ }
+}
+
+///
+/// Factory class for common conversation specifications
+///
+public static class ConversationSpecs
+{
+ public static ActiveConversationSpecification Active => new();
+ public static PublicConversationSpecification Public => new();
+ public static HighPriorityConversationSpecification HighPriority => new();
+
+ public static ConversationOwnedBySpecification OwnedBy(Guid userId) => new ConversationOwnedBySpecification(userId);
+ public static ConversationWithTagSpecification WithTag(string tag) => new ConversationWithTagSpecification(tag);
+}
diff --git a/src/Domain/Specifications/Examples/SpecificationUsageExamples.cs b/src/Domain/Specifications/Examples/SpecificationUsageExamples.cs
new file mode 100644
index 0000000..6f8f820
--- /dev/null
+++ b/src/Domain/Specifications/Examples/SpecificationUsageExamples.cs
@@ -0,0 +1,119 @@
+using Domain.Aggregates.User;
+
+namespace Domain.Specifications.Examples;
+
+///
+/// Examples of how to use specifications
+///
+public static class SpecificationUsageExamples
+{
+ ///
+ /// Example: Simple specification usage
+ ///
+ public static void SimpleUsage()
+ {
+ var users = GetSampleUsers();
+
+ // Find active users
+ var activeUsers = users.Where(UserSpecs.Active.ToExpression().Compile());
+
+ // Find admin users
+ var adminUsers = users.Where(UserSpecs.Admin.ToExpression().Compile());
+ }
+
+ ///
+ /// Example: Combining specifications with operators
+ ///
+ public static void CombinedSpecifications()
+ {
+ var users = GetSampleUsers();
+
+ // Active AND Admin users
+ var activeAdmins = UserSpecs.Active & UserSpecs.Admin;
+ var result1 = users.Where(activeAdmins.ToExpression().Compile());
+
+ // Users with legal access OR admin role
+ var legalOrAdmin = UserSpecs.LegalAccess | UserSpecs.Admin;
+ var result2 = users.Where(legalOrAdmin.ToExpression().Compile());
+
+ // NOT deleted users (same as active)
+ var notDeleted = !new UserHasRoleSpecification("Deleted");
+ var result3 = users.Where(notDeleted.ToExpression().Compile());
+ }
+
+ ///
+ /// Example: Complex business rules
+ ///
+ public static void ComplexBusinessRules()
+ {
+ var users = GetSampleUsers();
+
+ // Business rule: "Recent active legal experts"
+ var recentActiveLegalExperts = UserSpecs.Active &
+ UserSpecs.HasRole("LegalExpert") &
+ UserSpecs.CreatedAfter(DateTime.UtcNow.AddMonths(-6));
+
+ var result = users.Where(recentActiveLegalExperts.ToExpression().Compile());
+
+ // Business rule: "Company users excluding admins"
+ var companyNonAdmins = UserSpecs.EmailDomain("company.com") &
+ UserSpecs.Active &
+ !UserSpecs.Admin;
+
+ var result2 = users.Where(companyNonAdmins.ToExpression().Compile());
+ }
+
+ ///
+ /// Example: Validation using specifications
+ ///
+ public static void ValidationExample()
+ {
+ var user = GetSampleUser();
+
+ // Validate business rules
+ if (!UserSpecs.Active.IsSatisfiedBy(user))
+ {
+ throw new InvalidOperationException("User must be active");
+ }
+
+ if (!UserSpecs.LegalAccess.IsSatisfiedBy(user))
+ {
+ throw new UnauthorizedAccessException("User doesn't have legal access");
+ }
+ }
+
+ ///
+ /// Example: Dynamic query building
+ ///
+ public static void DynamicQueryBuilding()
+ {
+ var users = GetSampleUsers();
+ var querySpec = UserSpecs.Active; // Start with base requirement
+
+ // Add conditions based on user input
+ var requireLegalAccess = true;
+ var excludeAdmins = false;
+ var emailDomain = "company.com";
+
+ if (requireLegalAccess)
+ {
+ querySpec = (ActiveUserSpecification)(querySpec & UserSpecs.LegalAccess);
+ }
+
+ if (excludeAdmins)
+ {
+ querySpec = (ActiveUserSpecification)(querySpec & !UserSpecs.Admin);
+ }
+
+ if (!string.IsNullOrEmpty(emailDomain))
+ {
+ querySpec = (ActiveUserSpecification)(querySpec & UserSpecs.EmailDomain(emailDomain));
+ }
+
+ var result = users.Where(querySpec.ToExpression().Compile());
+ }
+
+ // Helper methods
+ private static List GetSampleUsers() => [];
+ private static User GetSampleUser() => new() { Email = "test@test.com", FullName = "Test", PasswordHash = "hash", Roles = ["User"] };
+}
diff --git a/src/Domain/Specifications/ISpecification.cs b/src/Domain/Specifications/ISpecification.cs
new file mode 100644
index 0000000..44bc76a
--- /dev/null
+++ b/src/Domain/Specifications/ISpecification.cs
@@ -0,0 +1,50 @@
+using System.Linq.Expressions;
+
+namespace Domain.Specifications;
+
+///
+/// Base specification interface
+///
+/// Entity type
+public interface ISpecification
+{
+ ///
+ /// Expression that represents the specification
+ ///
+ Expression> ToExpression();
+
+ ///
+ /// Check if entity satisfies the specification
+ ///
+ bool IsSatisfiedBy(T entity);
+}
+
+///
+/// Base specification implementation
+///
+/// Entity type
+public abstract class Specification : ISpecification
+{
+ public abstract Expression> ToExpression();
+
+ public bool IsSatisfiedBy(T entity)
+ {
+ var predicate = ToExpression().Compile();
+ return predicate(entity);
+ }
+
+ ///
+ /// AND operator
+ ///
+ public static Specification operator &(Specification left, Specification right) => new AndSpecification(left, right);
+
+ ///
+ /// OR operator
+ ///
+ public static Specification operator |(Specification left, Specification right) => new OrSpecification(left, right);
+
+ ///
+ /// NOT operator
+ ///
+ public static Specification operator !(Specification specification) => new NotSpecification(specification);
+}
diff --git a/src/Domain/Specifications/UserSpecifications.cs b/src/Domain/Specifications/UserSpecifications.cs
new file mode 100644
index 0000000..8f0d924
--- /dev/null
+++ b/src/Domain/Specifications/UserSpecifications.cs
@@ -0,0 +1,86 @@
+using System.Linq.Expressions;
+using Domain.Aggregates.User;
+
+namespace Domain.Specifications;
+
+///
+/// Specification for active users
+///
+public sealed class ActiveUserSpecification : Specification
+{
+ public override Expression> ToExpression()
+ {
+ return user => !user.IsDeleted;
+ }
+}
+
+///
+/// Specification for users with specific role
+///
+public sealed class UserHasRoleSpecification(string role) : Specification
+{
+ public override Expression> ToExpression()
+ {
+ return user => user.Roles.Contains(role);
+ }
+}
+
+///
+/// Specification for users created after specific date
+///
+public sealed class UserCreatedAfterSpecification(DateTime date) : Specification
+{
+ public override Expression> ToExpression()
+ {
+ return user => user.CreatedAt > date;
+ }
+}
+
+///
+/// Specification for users with email domain
+///
+public sealed class UserEmailDomainSpecification(string domain) : Specification
+{
+ public override Expression> ToExpression()
+ {
+ return user => user.Email.EndsWith($"@{domain}");
+ }
+}
+
+///
+/// Specification for admin users
+///
+public sealed class AdminUserSpecification : Specification
+{
+ public override Expression> ToExpression()
+ {
+ return user => user.Roles.Contains("Admin");
+ }
+}
+
+///
+/// Specification for users that can access legal features
+///
+public sealed class LegalAccessUserSpecification : Specification
+{
+ public override Expression> ToExpression()
+ {
+ return user => user.Roles.Contains("Admin") ||
+ user.Roles.Contains("LegalExpert") ||
+ user.Roles.Contains("Manager");
+ }
+}
+
+///
+/// Factory class for common user specifications
+///
+public static class UserSpecs
+{
+ public static ActiveUserSpecification Active => new();
+ public static AdminUserSpecification Admin => new();
+ public static LegalAccessUserSpecification LegalAccess => new();
+
+ public static UserHasRoleSpecification HasRole(string role) => new(role);
+ public static UserCreatedAfterSpecification CreatedAfter(DateTime date) => new(date);
+ public static UserEmailDomainSpecification EmailDomain(string domain) => new(domain);
+}
diff --git a/src/Domain/ValueObjects/Email.cs b/src/Domain/ValueObjects/Email.cs
new file mode 100644
index 0000000..ef17006
--- /dev/null
+++ b/src/Domain/ValueObjects/Email.cs
@@ -0,0 +1,35 @@
+using System.Text.RegularExpressions;
+
+namespace Domain.ValueObjects;
+
+///
+/// Email value object
+///
+public sealed record Email
+{
+ private static readonly Regex EmailRegex = new(
+ @"^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$",
+ RegexOptions.Compiled | RegexOptions.IgnoreCase);
+
+ public string Value { get; }
+
+ public Email(string value)
+ {
+ if (string.IsNullOrWhiteSpace(value))
+ {
+ throw new ArgumentException("Email cannot be empty", nameof(value));
+ }
+
+ if (!EmailRegex.IsMatch(value))
+ {
+ throw new ArgumentException("Invalid email format", nameof(value));
+ }
+
+ Value = value.ToUpperInvariant().Trim();
+ }
+
+ public static implicit operator string(Email email) => email.Value;
+ public static implicit operator Email(string email) => new(email);
+
+ public override string ToString() => Value;
+}
diff --git a/src/Domain/ValueObjects/Password.cs b/src/Domain/ValueObjects/Password.cs
new file mode 100644
index 0000000..af7f7ec
--- /dev/null
+++ b/src/Domain/ValueObjects/Password.cs
@@ -0,0 +1,53 @@
+namespace Domain.ValueObjects;
+
+///
+/// Password value object
+///
+public sealed record Password
+{
+ public string Hash { get; }
+
+ public Password(string hash)
+ {
+ if (string.IsNullOrWhiteSpace(hash))
+ {
+ throw new ArgumentException("Password hash cannot be empty", nameof(hash));
+ }
+
+ Hash = hash;
+ }
+
+ ///
+ /// Create password from plain text (for validation purposes)
+ ///
+ public static void ValidatePlainText(string plainText)
+ {
+ if (string.IsNullOrWhiteSpace(plainText))
+ {
+ throw new ArgumentException("Password cannot be empty");
+ }
+
+ if (plainText.Length < 8)
+ {
+ throw new ArgumentException("Password must be at least 8 characters long");
+ }
+
+ if (!plainText.Any(char.IsUpper))
+ {
+ throw new ArgumentException("Password must contain at least one uppercase letter");
+ }
+
+ if (!plainText.Any(char.IsLower))
+ {
+ throw new ArgumentException("Password must contain at least one lowercase letter");
+ }
+
+ if (!plainText.Any(char.IsDigit))
+ {
+ throw new ArgumentException("Password must contain at least one digit");
+ }
+ }
+
+ public static implicit operator string(Password password) => password.Hash;
+ public static implicit operator Password(string hash) => new(hash);
+}
diff --git a/src/Infrastructure/Data/Configurations/BaseEntityConfiguration.cs b/src/Infrastructure/Data/Configurations/BaseEntityConfiguration.cs
new file mode 100644
index 0000000..f324a2f
--- /dev/null
+++ b/src/Infrastructure/Data/Configurations/BaseEntityConfiguration.cs
@@ -0,0 +1,27 @@
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Metadata.Builders;
+
+namespace Infrastructure.Data.Configurations;
+
+///
+/// Base configuration cho tất cả entities
+///
+/// Entity type
+public abstract class BaseEntityConfiguration : IEntityTypeConfiguration
+ where TEntity : class
+{
+ public virtual void Configure(EntityTypeBuilder builder)
+ {
+ // Cấu hình chung cho tất cả entities
+ // Ví dụ: CreatedAt, UpdatedAt fields nếu có base entity
+
+ ConfigureEntity(builder);
+ }
+
+ ///
+ /// Cấu hình cụ thể cho từng entity
+ ///
+ /// Entity type builder
+ protected abstract void ConfigureEntity(EntityTypeBuilder builder);
+}
+
diff --git a/src/Infrastructure/Data/Contexts/DataContext.cs b/src/Infrastructure/Data/Contexts/DataContext.cs
new file mode 100644
index 0000000..5725473
--- /dev/null
+++ b/src/Infrastructure/Data/Contexts/DataContext.cs
@@ -0,0 +1,35 @@
+using Microsoft.EntityFrameworkCore;
+
+namespace Infrastructure.Data.Contexts;
+
+///
+/// Database context cho Legal Assistant application
+///
+public sealed class DataContext : DbContext, IDataContext
+{
+ public DataContext(DbContextOptions options) : base(options)
+ {
+ }
+
+ protected override void OnModelCreating(ModelBuilder modelBuilder)
+ {
+ base.OnModelCreating(modelBuilder);
+
+ // Áp dụng tất cả configurations từ assembly hiện tại
+ modelBuilder.ApplyConfigurationsFromAssembly(typeof(DataContext).Assembly);
+
+ // Cấu hình schema mặc định
+ modelBuilder.HasDefaultSchema(Schemas.Default);
+ }
+
+ protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
+ {
+ base.OnConfiguring(optionsBuilder);
+
+ // Cấu hình logging cho Entity Framework (chỉ trong Development)
+#if DEBUG
+ optionsBuilder.EnableSensitiveDataLogging();
+ optionsBuilder.EnableDetailedErrors();
+#endif
+ }
+}
diff --git a/src/Infrastructure/Data/Contexts/IDataContext.cs b/src/Infrastructure/Data/Contexts/IDataContext.cs
new file mode 100644
index 0000000..1285cfc
--- /dev/null
+++ b/src/Infrastructure/Data/Contexts/IDataContext.cs
@@ -0,0 +1,24 @@
+using Microsoft.EntityFrameworkCore;
+
+namespace Infrastructure.Data.Contexts;
+
+///
+/// Interface cho Legal Assistant Database Context
+///
+public interface IDataContext
+{
+ ///
+ /// Lưu các thay đổi vào database
+ ///
+ Task SaveChangesAsync(CancellationToken cancellationToken = default);
+
+ ///
+ /// Lưu các thay đổi vào database (synchronous)
+ ///
+ int SaveChanges();
+
+ ///
+ /// Dispose context
+ ///
+ ValueTask DisposeAsync();
+}
diff --git a/src/Infrastructure/Data/Contexts/Schemas.cs b/src/Infrastructure/Data/Contexts/Schemas.cs
new file mode 100644
index 0000000..e15eba3
--- /dev/null
+++ b/src/Infrastructure/Data/Contexts/Schemas.cs
@@ -0,0 +1,6 @@
+namespace Infrastructure.Data.Contexts;
+
+internal static class Schemas
+{
+ public const string Default = "public";
+}
diff --git a/src/Infrastructure/Infrastructure.csproj b/src/Infrastructure/Infrastructure.csproj
new file mode 100644
index 0000000..663d7cb
--- /dev/null
+++ b/src/Infrastructure/Infrastructure.csproj
@@ -0,0 +1,27 @@
+
+
+
+ net8.0
+ enable
+ enable
+
+
+
+
+
+
+ all
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+
+
+ all
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+
+
+
+
+
+
+
+
+
diff --git a/src/Infrastructure/README.md b/src/Infrastructure/README.md
new file mode 100644
index 0000000..5ba85db
--- /dev/null
+++ b/src/Infrastructure/README.md
@@ -0,0 +1,218 @@
+# 🗄️ Infrastructure Layer
+
+**Implements external concerns và technical details. Provides concrete implementations cho Application interfaces.**
+
+## 📁 Cấu trúc Folders
+
+```
+Infrastructure/
+├── Data/ # 🗃️ Database & Data Access
+│ ├── Contexts/
+│ │ ├── DataContext.cs # Main EF DbContext
+│ │ ├── IDataContext.cs # DbContext interface
+│ │ └── Schemas.cs # Database schema constants
+│ │
+│ └── Configurations/ # EF Entity Configurations
+│ └── BaseEntityConfiguration.cs # Base entity config
+│
+├── Repositories/ # 📚 Repository Implementations
+│ ├── UserRepository.cs # IUserRepository implementation
+│ └── BaseRepository.cs # Base repository với common operations
+│
+├── Services/ # 🛠️ External Service Implementations
+│ ├── Email/
+│ │ ├── EmailService.cs # IEmailService implementation
+│ │ └── EmailOptions.cs # Email configuration
+│ │
+│ ├── FileStorage/
+│ │ ├── FileStorageService.cs # File upload/download
+│ │ └── AzureBlobStorage.cs # Azure Blob implementation
+│ │
+│ ├── PasswordHasher.cs # IPasswordHasher implementation
+│ ├── TokenService.cs # ITokenService implementation
+│ └── DomainEventDispatcher.cs # Domain event dispatching
+│
+├── Authentication/ # 🔐 Authentication & Authorization
+│ ├── JwtTokenService.cs # JWT token generation/validation
+│ ├── PasswordHasher.cs # Password hashing với BCrypt
+│ └── AuthenticationOptions.cs # Auth configuration
+│
+├── Caching/ # 💾 Caching Implementations
+│ ├── MemoryCacheService.cs # In-memory caching
+│ ├── RedisCacheService.cs # Redis distributed cache
+│ └── CacheOptions.cs # Cache configuration
+│
+├── External/ # 🌐 External API Integrations
+│ ├── PaymentGateway/
+│ │ ├── StripePaymentService.cs
+│ │ └── PaymentOptions.cs
+│ │
+│ └── LegalDatabase/
+│ ├── LegalApiClient.cs
+│ └── LegalApiOptions.cs
+│
+├── Messaging/ # 📨 Message Queues & Events
+│ ├── ServiceBus/
+│ │ ├── ServiceBusPublisher.cs
+│ │ └── ServiceBusOptions.cs
+│ │
+│ └── EventBus/
+│ ├── InMemoryEventBus.cs
+│ └── RabbitMQEventBus.cs
+│
+└── Persistence/ # 📊 Database Specific Concerns
+ ├── Migrations/ # EF Database migrations
+ │ └── 20240101_InitialMigration.cs
+ │
+ └── Seeders/ # Database seed data
+ ├── UserSeeder.cs
+ └── RoleSeeder.cs
+```
+
+## 🎯 Mục đích từng folder
+
+### 🗃️ **Data/** - Database Layer
+**Entity Framework Core** setup và configuration
+
+**DataContext:**
+- DbContext chính của application
+- Entity configurations
+- Database connection management
+
+**Configurations:**
+- EF entity mappings
+- Relationship configurations
+- Index definitions
+- Constraints
+
+### 📚 **Repositories/** - Data Access Pattern
+**Implement Application interfaces** cho data access
+
+```csharp
+public class UserRepository : IUserRepository
+{
+ private readonly DataContext _context;
+
+ public async Task GetByEmailAsync(string email)
+ {
+ return await _context.Users
+ .FirstOrDefaultAsync(u => u.Email == email);
+ }
+}
+```
+
+### 🛠️ **Services/** - External Service Implementations
+**Concrete implementations** của Application interfaces
+
+**Categories:**
+- **Authentication** - JWT, OAuth, etc.
+- **Communication** - Email, SMS, Push notifications
+- **Storage** - File upload, cloud storage
+- **Integration** - External APIs, webhooks
+
+### 🔐 **Authentication/** - Security Implementation
+**Authentication và Authorization** mechanics
+
+- **JWT Token Service** - Create/validate tokens
+- **Password Hashing** - Secure password storage
+- **Claims Management** - User permissions
+- **Session Management** - User sessions
+
+### 💾 **Caching/** - Performance Optimization
+**Caching strategies** implementation
+
+- **Memory Cache** - In-process caching
+- **Distributed Cache** - Redis, SQL Server cache
+- **Cache Policies** - TTL, eviction strategies
+
+### 🌐 **External/** - Third-party Integrations
+**External service integrations**
+
+**Examples:**
+- Payment gateways (Stripe, PayPal)
+- Legal databases
+- Document services
+- Analytics services
+- Cloud services (AWS, Azure)
+
+### 📨 **Messaging/** - Event & Message Processing
+**Asynchronous communication**
+
+- **Service Bus** - Azure Service Bus, RabbitMQ
+- **Event Sourcing** - Event store implementation
+- **Background Jobs** - Hangfire, Quartz.NET
+- **Real-time** - SignalR
+
+### 📊 **Persistence/** - Database Operations
+**Database-specific operations**
+
+**Migrations:**
+- Schema changes
+- Data migrations
+- Version control
+
+**Seeders:**
+- Initial data setup
+- Test data creation
+- Reference data
+
+## 🔌 Dependency Injection Setup
+
+Infrastructure layer cung cấp **service registration extensions**:
+
+```csharp
+public static class InfrastructureServiceExtensions
+{
+ public static IServiceCollection AddInfrastructure(
+ this IServiceCollection services,
+ IConfiguration configuration)
+ {
+ // Database
+ services.AddDbContext(options =>
+ options.UseSqlServer(connectionString));
+
+ // Repositories
+ services.AddScoped();
+
+ // Services
+ services.AddScoped();
+ services.AddScoped();
+
+ // External Services
+ services.AddScoped();
+
+ return services;
+ }
+}
+```
+
+## 🚫 Điều KHÔNG ĐƯỢC làm
+
+- ❌ **No business logic** - Business rules thuộc Domain
+- ❌ **No UI concerns** - Presentation logic thuộc Web layer
+- ❌ **Direct domain manipulation** - Always qua Application layer
+
+## ✅ Điều NÊN làm
+
+- ✅ **Implement interfaces** - Từ Application layer
+- ✅ **Handle technical concerns** - Database, external APIs
+- ✅ **Configuration management** - Options pattern
+- ✅ **Error handling** - Technical error wrapping
+- ✅ **Performance optimization** - Caching, connection pooling
+
+## 🔗 Dependencies
+
+- **Application Layer** - Interfaces to implement
+- **Domain Layer** - Domain entities và aggregates
+- **Entity Framework Core** - ORM
+- **External packages** - Specific to implementation
+
+## 💡 Best Practices
+
+1. **Configuration** - Use IOptions pattern
+2. **Connection Management** - Proper disposal
+3. **Error Handling** - Wrap external exceptions
+4. **Logging** - Log all external calls
+5. **Resilience** - Retry policies, circuit breakers
+6. **Testing** - Integration tests cho repositories
+7. **Security** - Secure external communications
diff --git a/src/Infrastructure/Services/DomainEventDispatcher.cs b/src/Infrastructure/Services/DomainEventDispatcher.cs
new file mode 100644
index 0000000..2e4a9ac
--- /dev/null
+++ b/src/Infrastructure/Services/DomainEventDispatcher.cs
@@ -0,0 +1,51 @@
+using Application.Common;
+using Domain.Common;
+using MediatR;
+using Microsoft.Extensions.Logging;
+
+namespace Infrastructure.Services;
+
+///
+/// Implementation of domain event dispatcher using MediatR
+///
+public sealed class DomainEventDispatcher(
+ IMediator mediator,
+ ILogger logger) : IDomainEventDispatcher
+{
+ public async Task DispatchEventsAsync(IEnumerable entities, CancellationToken cancellationToken = default)
+ {
+ var entitiesList = entities.ToList();
+ var domainEvents = entitiesList
+ .SelectMany(entity => entity.DomainEvents)
+ .ToList();
+
+ // Clear events from entities before dispatching
+ foreach (var entity in entitiesList)
+ {
+ entity.ClearDomainEvents();
+ }
+
+ // Dispatch all events
+ foreach (var domainEvent in domainEvents)
+ {
+ await DispatchEventAsync(domainEvent, cancellationToken);
+ }
+ }
+
+ public async Task DispatchEventAsync(IDomainEvent domainEvent, CancellationToken cancellationToken = default)
+ {
+ try
+ {
+ logger.LogDebug("Dispatching domain event: {EventType}", domainEvent.GetType().Name);
+
+ await mediator.Publish(domainEvent, cancellationToken);
+
+ logger.LogDebug("Successfully dispatched domain event: {EventType}", domainEvent.GetType().Name);
+ }
+ catch (Exception ex)
+ {
+ logger.LogError(ex, "Error dispatching domain event: {EventType}", domainEvent.GetType().Name);
+ throw;
+ }
+ }
+}
diff --git a/src/Infrastructure/Services/Email/.gitkeep b/src/Infrastructure/Services/Email/.gitkeep
new file mode 100644
index 0000000..e69de29
diff --git a/src/Web.Api/Configurations/Options/JwtOptions.cs b/src/Web.Api/Configurations/Options/JwtOptions.cs
new file mode 100644
index 0000000..07b2c24
--- /dev/null
+++ b/src/Web.Api/Configurations/Options/JwtOptions.cs
@@ -0,0 +1,37 @@
+namespace Web.Api.Configurations.Options;
+
+///
+/// JWT configuration options
+///
+public sealed class JwtOptions
+{
+ ///
+ /// Configuration section name
+ ///
+ public const string SectionName = "Jwt";
+
+ ///
+ /// JWT secret key
+ ///
+ public string SecretKey { get; init; } = string.Empty;
+
+ ///
+ /// JWT issuer
+ ///
+ public string Issuer { get; init; } = string.Empty;
+
+ ///
+ /// JWT audience
+ ///
+ public string Audience { get; init; } = string.Empty;
+
+ ///
+ /// Access token expiration time in minutes
+ ///
+ public int AccessTokenExpirationMinutes { get; init; } = 60;
+
+ ///
+ /// Refresh token expiration time in days
+ ///
+ public int RefreshTokenExpirationDays { get; init; } = 7;
+}
diff --git a/src/Web.Api/Controllers/V1/AuthController.cs b/src/Web.Api/Controllers/V1/AuthController.cs
new file mode 100644
index 0000000..ee26040
--- /dev/null
+++ b/src/Web.Api/Controllers/V1/AuthController.cs
@@ -0,0 +1,34 @@
+using Application.Features.Auth.Login;
+using MediatR;
+using Microsoft.AspNetCore.Mvc;
+using Web.Api.Models.Responses;
+
+namespace Web.Api.Controllers.V1;
+
+///
+/// Authentication controller
+///
+[Route("api/v{version:apiVersion}/[controller]")]
+public sealed class AuthController(IMediator mediator) : BaseController
+{
+ ///
+ /// User login
+ ///
+ /// Login credentials
+ /// Login result with tokens
+ [HttpPost("login")]
+ [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status200OK)]
+ [ProducesResponseType(typeof(ApiResponse