diff --git a/restaurant-reservations-clean-architecture/.editorconfig b/restaurant-reservations-clean-architecture/.editorconfig
new file mode 100755
index 0000000..7d3bb5a
--- /dev/null
+++ b/restaurant-reservations-clean-architecture/.editorconfig
@@ -0,0 +1,395 @@
+root = true
+
+# All files
+[*]
+indent_style = space
+
+# Xml files
+[*.xml]
+indent_size = 2
+
+# Xml project files
+[*.{csproj,fsproj,vbproj,proj,slnx}]
+indent_size = 2
+
+# Xml config files
+[*.{props,targets,config,nuspec}]
+indent_size = 2
+
+[*.json]
+indent_size = 2
+
+# C# files
+[*.cs]
+
+#### Core EditorConfig Options ####
+
+# Indentation and spacing
+indent_size = 4
+tab_width = 4
+
+# New line preferences
+insert_final_newline = false
+
+#### .NET Coding Conventions ####
+[*.{cs,vb}]
+
+# Organize usings
+dotnet_separate_import_directive_groups = true
+dotnet_sort_system_directives_first = true
+file_header_template = unset
+
+# this. and Me. preferences
+dotnet_style_qualification_for_event = false:silent
+dotnet_style_qualification_for_field = false:silent
+dotnet_style_qualification_for_method = false:silent
+dotnet_style_qualification_for_property = false:silent
+
+# Language keywords vs BCL types preferences
+dotnet_style_predefined_type_for_locals_parameters_members = true:silent
+dotnet_style_predefined_type_for_member_access = true:silent
+
+# Parentheses preferences
+dotnet_style_parentheses_in_arithmetic_binary_operators = always_for_clarity:silent
+dotnet_style_parentheses_in_other_binary_operators = always_for_clarity:silent
+dotnet_style_parentheses_in_other_operators = never_if_unnecessary:silent
+dotnet_style_parentheses_in_relational_binary_operators = always_for_clarity:silent
+
+# Modifier preferences
+dotnet_style_require_accessibility_modifiers = for_non_interface_members:silent
+
+# Expression-level preferences
+dotnet_style_coalesce_expression = true:suggestion
+dotnet_style_collection_initializer = true:suggestion
+dotnet_style_explicit_tuple_names = true:suggestion
+dotnet_style_null_propagation = true:suggestion
+dotnet_style_object_initializer = true:suggestion
+dotnet_style_operator_placement_when_wrapping = beginning_of_line
+dotnet_style_prefer_auto_properties = true:suggestion
+dotnet_style_prefer_collection_expression = when_types_loosely_match:suggestion
+dotnet_style_prefer_compound_assignment = true:suggestion
+dotnet_style_prefer_conditional_expression_over_assignment = true:suggestion
+dotnet_style_prefer_conditional_expression_over_return = true:suggestion
+dotnet_style_prefer_inferred_anonymous_type_member_names = true:suggestion
+dotnet_style_prefer_inferred_tuple_names = true:suggestion
+dotnet_style_prefer_is_null_check_over_reference_equality_method = true:suggestion
+dotnet_style_prefer_simplified_boolean_expressions = true:suggestion
+dotnet_style_prefer_simplified_interpolation = true:suggestion
+
+# Field preferences
+dotnet_style_readonly_field = true:warning
+
+# Parameter preferences
+dotnet_code_quality_unused_parameters = all:suggestion
+
+
+#### C# Coding Conventions ####
+[*.cs]
+
+# var preferences
+csharp_style_var_elsewhere = false:silent
+csharp_style_var_for_built_in_types = false:silent
+csharp_style_var_when_type_is_apparent = false:silent
+
+# Expression-bodied members
+csharp_style_expression_bodied_accessors = true:silent
+csharp_style_expression_bodied_constructors = false:silent
+csharp_style_expression_bodied_indexers = true:silent
+csharp_style_expression_bodied_lambdas = true:suggestion
+csharp_style_expression_bodied_local_functions = false:silent
+csharp_style_expression_bodied_methods = false:silent
+csharp_style_expression_bodied_operators = false:silent
+csharp_style_expression_bodied_properties = true:silent
+
+# Pattern matching preferences
+csharp_style_pattern_matching_over_as_with_null_check = true:suggestion
+csharp_style_pattern_matching_over_is_with_cast_check = true:suggestion
+csharp_style_prefer_extended_property_pattern = true:suggestion
+csharp_style_prefer_not_pattern = true:suggestion
+csharp_style_prefer_pattern_matching = true:silent
+csharp_style_prefer_switch_expression = true:suggestion
+
+# Null-checking preferences
+csharp_style_conditional_delegate_call = true:suggestion
+
+# Modifier preferences
+csharp_prefer_static_anonymous_function = true:suggestion
+csharp_prefer_static_local_function = true:warning
+csharp_preferred_modifier_order = public,private,protected,internal,file,const,static,extern,new,virtual,abstract,sealed,override,readonly,unsafe,required,volatile,async:suggestion
+csharp_style_prefer_readonly_struct = true:suggestion
+csharp_style_prefer_readonly_struct_member = true:suggestion
+
+# Code-block preferences
+csharp_prefer_braces = true:silent
+csharp_prefer_simple_using_statement = true:suggestion
+csharp_style_namespace_declarations = file_scoped:suggestion
+csharp_style_prefer_method_group_conversion = true:silent
+csharp_style_prefer_primary_constructors = true:suggestion
+csharp_style_prefer_top_level_statements = true:silent
+
+# Expression-level preferences
+csharp_prefer_simple_default_expression = true:suggestion
+csharp_style_deconstructed_variable_declaration = true:suggestion
+csharp_style_implicit_object_creation_when_type_is_apparent = true:suggestion
+csharp_style_inlined_variable_declaration = true:suggestion
+csharp_style_prefer_index_operator = true:suggestion
+csharp_style_prefer_local_over_anonymous_function = true:suggestion
+csharp_style_prefer_null_check_over_type_check = true:suggestion
+csharp_style_prefer_range_operator = true:suggestion
+csharp_style_prefer_tuple_swap = true:suggestion
+csharp_style_prefer_utf8_string_literals = true:suggestion
+csharp_style_throw_expression = true:suggestion
+csharp_style_unused_value_assignment_preference = discard_variable:suggestion
+csharp_style_unused_value_expression_statement_preference = discard_variable:silent
+
+# 'using' directive preferences
+csharp_using_directive_placement = outside_namespace:silent
+
+#### 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 = true
+csharp_new_line_before_members_in_object_initializers = true
+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 = true
+
+#### Naming styles ####
+
+
+dotnet_diagnostic.CA1873.severity = none
+
+
+# CS8618: Non-nullable field must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring as nullable.
+dotnet_diagnostic.CS8618.severity = none
+
+
+dotnet_diagnostic.CA2012.severity = none
+
+
+# CS0219: Variable is assigned but its value is never used
+dotnet_diagnostic.CS0219.severity = silent
+[*.{cs,vb}]
+
+# Naming rules
+
+dotnet_naming_rule.types_and_namespaces_should_be_pascalcase.severity = suggestion
+dotnet_naming_rule.types_and_namespaces_should_be_pascalcase.symbols = types_and_namespaces
+dotnet_naming_rule.types_and_namespaces_should_be_pascalcase.style = pascalcase
+
+dotnet_naming_rule.interfaces_should_be_ipascalcase.severity = suggestion
+dotnet_naming_rule.interfaces_should_be_ipascalcase.symbols = interfaces
+dotnet_naming_rule.interfaces_should_be_ipascalcase.style = ipascalcase
+
+dotnet_naming_rule.type_parameters_should_be_tpascalcase.severity = suggestion
+dotnet_naming_rule.type_parameters_should_be_tpascalcase.symbols = type_parameters
+dotnet_naming_rule.type_parameters_should_be_tpascalcase.style = tpascalcase
+
+dotnet_naming_rule.methods_should_be_pascalcase.severity = suggestion
+dotnet_naming_rule.methods_should_be_pascalcase.symbols = methods
+dotnet_naming_rule.methods_should_be_pascalcase.style = pascalcase
+
+dotnet_naming_rule.properties_should_be_pascalcase.severity = suggestion
+dotnet_naming_rule.properties_should_be_pascalcase.symbols = properties
+dotnet_naming_rule.properties_should_be_pascalcase.style = pascalcase
+
+dotnet_naming_rule.events_should_be_pascalcase.severity = suggestion
+dotnet_naming_rule.events_should_be_pascalcase.symbols = events
+dotnet_naming_rule.events_should_be_pascalcase.style = pascalcase
+
+dotnet_naming_rule.local_variables_should_be_camelcase.severity = suggestion
+dotnet_naming_rule.local_variables_should_be_camelcase.symbols = local_variables
+dotnet_naming_rule.local_variables_should_be_camelcase.style = camelcase
+
+dotnet_naming_rule.local_constants_should_be_camelcase.severity = suggestion
+dotnet_naming_rule.local_constants_should_be_camelcase.symbols = local_constants
+dotnet_naming_rule.local_constants_should_be_camelcase.style = camelcase
+
+dotnet_naming_rule.parameters_should_be_camelcase.severity = suggestion
+dotnet_naming_rule.parameters_should_be_camelcase.symbols = parameters
+dotnet_naming_rule.parameters_should_be_camelcase.style = camelcase
+
+dotnet_naming_rule.public_fields_should_be_pascalcase.severity = suggestion
+dotnet_naming_rule.public_fields_should_be_pascalcase.symbols = public_fields
+dotnet_naming_rule.public_fields_should_be_pascalcase.style = pascalcase
+
+dotnet_naming_rule.private_static_fields_should_be_s_camelcase.severity = suggestion
+dotnet_naming_rule.private_static_fields_should_be_s_camelcase.symbols = private_static_fields
+dotnet_naming_rule.private_static_fields_should_be_s_camelcase.style = s_camelcase
+
+dotnet_naming_rule.public_constant_fields_should_be_pascalcase.severity = suggestion
+dotnet_naming_rule.public_constant_fields_should_be_pascalcase.symbols = public_constant_fields
+dotnet_naming_rule.public_constant_fields_should_be_pascalcase.style = pascalcase
+
+dotnet_naming_rule.private_constant_fields_should_be_pascalcase.severity = suggestion
+dotnet_naming_rule.private_constant_fields_should_be_pascalcase.symbols = private_constant_fields
+dotnet_naming_rule.private_constant_fields_should_be_pascalcase.style = pascalcase
+
+dotnet_naming_rule.public_static_readonly_fields_should_be_pascalcase.severity = suggestion
+dotnet_naming_rule.public_static_readonly_fields_should_be_pascalcase.symbols = public_static_readonly_fields
+dotnet_naming_rule.public_static_readonly_fields_should_be_pascalcase.style = pascalcase
+
+dotnet_naming_rule.private_static_readonly_fields_should_be_pascalcase.severity = suggestion
+dotnet_naming_rule.private_static_readonly_fields_should_be_pascalcase.symbols = private_static_readonly_fields
+dotnet_naming_rule.private_static_readonly_fields_should_be_pascalcase.style = pascalcase
+
+dotnet_naming_rule.enums_should_be_pascalcase.severity = suggestion
+dotnet_naming_rule.enums_should_be_pascalcase.symbols = enums
+dotnet_naming_rule.enums_should_be_pascalcase.style = pascalcase
+
+dotnet_naming_rule.local_functions_should_be_pascalcase.severity = suggestion
+dotnet_naming_rule.local_functions_should_be_pascalcase.symbols = local_functions
+dotnet_naming_rule.local_functions_should_be_pascalcase.style = pascalcase
+
+dotnet_naming_rule.non_field_members_should_be_pascalcase.severity = suggestion
+dotnet_naming_rule.non_field_members_should_be_pascalcase.symbols = non_field_members
+dotnet_naming_rule.non_field_members_should_be_pascalcase.style = pascalcase
+
+# Symbol specifications
+
+dotnet_naming_symbols.interfaces.applicable_kinds = interface
+dotnet_naming_symbols.interfaces.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected
+dotnet_naming_symbols.interfaces.required_modifiers =
+
+dotnet_naming_symbols.enums.applicable_kinds = enum
+dotnet_naming_symbols.enums.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected
+dotnet_naming_symbols.enums.required_modifiers =
+
+dotnet_naming_symbols.events.applicable_kinds = event
+dotnet_naming_symbols.events.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected
+dotnet_naming_symbols.events.required_modifiers =
+
+dotnet_naming_symbols.methods.applicable_kinds = method
+dotnet_naming_symbols.methods.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected
+dotnet_naming_symbols.methods.required_modifiers =
+
+dotnet_naming_symbols.properties.applicable_kinds = property
+dotnet_naming_symbols.properties.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected
+dotnet_naming_symbols.properties.required_modifiers =
+
+dotnet_naming_symbols.public_fields.applicable_kinds = field
+dotnet_naming_symbols.public_fields.applicable_accessibilities = public, internal
+dotnet_naming_symbols.public_fields.required_modifiers =
+
+dotnet_naming_symbols.private_fields.applicable_kinds = field
+dotnet_naming_symbols.private_fields.applicable_accessibilities = private, protected, protected_internal, private_protected
+dotnet_naming_symbols.private_fields.required_modifiers =
+
+dotnet_naming_symbols.private_static_fields.applicable_kinds = field
+dotnet_naming_symbols.private_static_fields.applicable_accessibilities = private, protected, protected_internal, private_protected
+dotnet_naming_symbols.private_static_fields.required_modifiers = static
+
+dotnet_naming_symbols.types_and_namespaces.applicable_kinds = namespace, class, struct, interface, enum
+dotnet_naming_symbols.types_and_namespaces.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected
+dotnet_naming_symbols.types_and_namespaces.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 =
+
+dotnet_naming_symbols.type_parameters.applicable_kinds = namespace
+dotnet_naming_symbols.type_parameters.applicable_accessibilities = *
+dotnet_naming_symbols.type_parameters.required_modifiers =
+
+dotnet_naming_symbols.private_constant_fields.applicable_kinds = field
+dotnet_naming_symbols.private_constant_fields.applicable_accessibilities = private, protected, protected_internal, private_protected
+dotnet_naming_symbols.private_constant_fields.required_modifiers = const
+
+dotnet_naming_symbols.local_variables.applicable_kinds = local
+dotnet_naming_symbols.local_variables.applicable_accessibilities = local
+dotnet_naming_symbols.local_variables.required_modifiers =
+
+dotnet_naming_symbols.local_constants.applicable_kinds = local
+dotnet_naming_symbols.local_constants.applicable_accessibilities = local
+dotnet_naming_symbols.local_constants.required_modifiers = const
+
+dotnet_naming_symbols.parameters.applicable_kinds = parameter
+dotnet_naming_symbols.parameters.applicable_accessibilities = *
+dotnet_naming_symbols.parameters.required_modifiers =
+
+dotnet_naming_symbols.public_constant_fields.applicable_kinds = field
+dotnet_naming_symbols.public_constant_fields.applicable_accessibilities = public, internal
+dotnet_naming_symbols.public_constant_fields.required_modifiers = const
+
+dotnet_naming_symbols.public_static_readonly_fields.applicable_kinds = field
+dotnet_naming_symbols.public_static_readonly_fields.applicable_accessibilities = public, internal
+dotnet_naming_symbols.public_static_readonly_fields.required_modifiers = readonly, static
+
+dotnet_naming_symbols.private_static_readonly_fields.applicable_kinds = field
+dotnet_naming_symbols.private_static_readonly_fields.applicable_accessibilities = private, protected, protected_internal, private_protected
+dotnet_naming_symbols.private_static_readonly_fields.required_modifiers = readonly, static
+
+dotnet_naming_symbols.local_functions.applicable_kinds = local_function
+dotnet_naming_symbols.local_functions.applicable_accessibilities = *
+dotnet_naming_symbols.local_functions.required_modifiers =
+
+# Naming styles
+
+dotnet_naming_style.pascalcase.required_prefix =
+dotnet_naming_style.pascalcase.required_suffix =
+dotnet_naming_style.pascalcase.word_separator =
+dotnet_naming_style.pascalcase.capitalization = pascal_case
+
+dotnet_naming_style.ipascalcase.required_prefix = I
+dotnet_naming_style.ipascalcase.required_suffix =
+dotnet_naming_style.ipascalcase.word_separator =
+dotnet_naming_style.ipascalcase.capitalization = pascal_case
+
+dotnet_naming_style.tpascalcase.required_prefix = T
+dotnet_naming_style.tpascalcase.required_suffix =
+dotnet_naming_style.tpascalcase.word_separator =
+dotnet_naming_style.tpascalcase.capitalization = pascal_case
+
+dotnet_naming_style._camelcase.required_prefix = _
+dotnet_naming_style._camelcase.required_suffix =
+dotnet_naming_style._camelcase.word_separator =
+dotnet_naming_style._camelcase.capitalization = camel_case
+
+dotnet_naming_style.camelcase.required_prefix =
+dotnet_naming_style.camelcase.required_suffix =
+dotnet_naming_style.camelcase.word_separator =
+dotnet_naming_style.camelcase.capitalization = camel_case
+
+dotnet_naming_style.s_camelcase.required_prefix = s_
+dotnet_naming_style.s_camelcase.required_suffix =
+dotnet_naming_style.s_camelcase.word_separator =
+dotnet_naming_style.s_camelcase.capitalization = camel_case
+
diff --git a/restaurant-reservations-clean-architecture/.gitignore b/restaurant-reservations-clean-architecture/.gitignore
new file mode 100755
index 0000000..0808c4a
--- /dev/null
+++ b/restaurant-reservations-clean-architecture/.gitignore
@@ -0,0 +1,482 @@
+## Ignore Visual Studio temporary files, build results, and
+## files generated by popular Visual Studio add-ons.
+##
+## Get latest from `dotnet new gitignore`
+
+# dotenv files
+.env
+
+# 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
+project.lock.json
+project.fragment.lock.json
+artifacts/
+
+# Tye
+.tye/
+
+# 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
+# but not Directory.Build.rsp, as it configures directory-level build defaults
+!Directory.Build.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
+.idea/
+
+##
+## Visual studio for Mac
+##
+
+
+# globs
+Makefile.in
+*.userprefs
+*.usertasks
+config.make
+config.status
+aclocal.m4
+install-sh
+autom4te.cache/
+*.tar.gz
+tarballs/
+test-results/
+
+# content below from: https://github.com/github/gitignore/blob/main/Global/macOS.gitignore
+# General
+.DS_Store
+.AppleDouble
+.LSOverride
+
+# Icon must end with two \r
+Icon
+
+
+# Thumbnails
+._*
+
+# Files that might appear in the root of a volume
+.DocumentRevisions-V100
+.fseventsd
+.Spotlight-V100
+.TemporaryItems
+.Trashes
+.VolumeIcon.icns
+.com.apple.timemachine.donotpresent
+
+# Directories potentially created on remote AFP share
+.AppleDB
+.AppleDesktop
+Network Trash Folder
+Temporary Items
+.apdisk
+
+# content below from: https://github.com/github/gitignore/blob/main/Global/Windows.gitignore
+# Windows thumbnail cache files
+Thumbs.db
+ehthumbs.db
+ehthumbs_vista.db
+
+# Dump file
+*.stackdump
+
+# Folder config file
+[Dd]esktop.ini
+
+# Recycle Bin used on file shares
+$RECYCLE.BIN/
+
+# Windows Installer files
+*.cab
+*.msi
+*.msix
+*.msm
+*.msp
+
+# Windows shortcuts
+*.lnk
+
+# Vim temporary swap files
+*.swp
diff --git a/restaurant-reservations-clean-architecture/Directory.Build.props b/restaurant-reservations-clean-architecture/Directory.Build.props
new file mode 100755
index 0000000..fca2783
--- /dev/null
+++ b/restaurant-reservations-clean-architecture/Directory.Build.props
@@ -0,0 +1,7 @@
+
+
+
+
+
+
+
diff --git a/restaurant-reservations-clean-architecture/Directory.Packages.props b/restaurant-reservations-clean-architecture/Directory.Packages.props
new file mode 100755
index 0000000..5919eb5
--- /dev/null
+++ b/restaurant-reservations-clean-architecture/Directory.Packages.props
@@ -0,0 +1,48 @@
+
+
+
+ true
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+ all
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/restaurant-reservations-clean-architecture/Dockerfile b/restaurant-reservations-clean-architecture/Dockerfile
new file mode 100644
index 0000000..9210a14
--- /dev/null
+++ b/restaurant-reservations-clean-architecture/Dockerfile
@@ -0,0 +1,23 @@
+FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build-env
+WORKDIR /src
+
+COPY *.slnx ./
+COPY Directory.Packages.props ./
+
+COPY src/RestaurantReservation.Domain/*.csproj ./src/RestaurantReservation.Domain/
+COPY src/RestaurantReservation.Application/*.csproj ./src/RestaurantReservation.Application/
+COPY src/RestaurantReservation.Infrastructure/*.csproj ./src/RestaurantReservation.Infrastructure/
+COPY src/RestaurantReservation.Api/*.csproj ./src/RestaurantReservation.Api/
+
+RUN dotnet restore src/RestaurantReservation.Api/RestaurantReservation.Api.csproj
+
+COPY . .
+
+RUN dotnet publish src/RestaurantReservation.Api/RestaurantReservation.Api.csproj -c Release -o /app/out
+
+FROM mcr.microsoft.com/dotnet/aspnet:10.0 AS final
+WORKDIR /app
+COPY --from=build-env /app/out .
+
+EXPOSE 8080
+ENTRYPOINT ["dotnet", "RestaurantReservation.Api.dll"]
\ No newline at end of file
diff --git a/restaurant-reservations-clean-architecture/README.md b/restaurant-reservations-clean-architecture/README.md
new file mode 100644
index 0000000..d31cad1
--- /dev/null
+++ b/restaurant-reservations-clean-architecture/README.md
@@ -0,0 +1,110 @@
+# Restaurant Reservation API
+
+This is a backend API for managing restaurant reservations following the Clean Architecture Principles. This is a learning project from the [C# Academy Website](https://thecsharpacademy.com/project/100008/system-design-clean-architecture-reservations). It handles real-world business concerns such as seating parties across multiple tables if their party size exceeds the size of a single table, as well as preventing race-condition double bookings at the database level.
+
+## Technogies Used
+- .NET10/ASP.NET Core
+- PostgreSQL 17 - database/persistence
+- Entity Framework Core - ORM, Code-First migrations, Fluent API
+- MediatR 12 - CQRS-style command/query (request response) pipeline
+- FluentValidation - command validation
+- ASP.NET Core Identity - authentication, JWT bearer tokens
+- Redis/Hybrid Cache - caching to improve API response times
+- Seq - structured logging (via Serilog)
+- MailKit/Mailpit - for sending email notifications after reservation scheduling/rescheduling/cancellation/completion /local SMTP testing
+- Docker & Docker Compose - containerization
+- xUnit, FluentAssertions, TestContainers - for architecture/integration testing
+
+**Getting Started**
+You can run this project via Docker Compose, which spins up the API as well as PostgreSQL, Redis, Seq and Mailpit.
+Clone the repo and navigate to the solution root directory and run the following command:
+
+```bash
+docker compose up -d --build
+```
+
+***Once everything is up and running:***
+
+| Service | URL |
+|------------------------------------|---------------------------------|
+| API (Scalar for testing endpoints) | http://localhost:8080/scalar/v1 |
+| Seq (logs) | http://localhost:8081 |
+| Mailpit (email) | http://localhost:8025 |
+
+
+There is a default admin and customer user seeded into the database as well as restaurants/tables etc.
+
+***Default User Information***
+
+| Username | Password |
+|------------------------------------|------------------------|
+| admin@example.com | Pa$$w0rd |
+| customer@example.com | Pa$$w0rd |
+
+***Architecture Overview***
+This project follows Clean Architecture Principals (with some Domain-Driven-Design thrown in). This application is organized into four layers:
+- ***Domain*** - Core business entiries and rules (Restaurant, Table, TableGroup, Reservation etc.). This layer has no dependencies on any other layers. Business invariants (e.g. A table can only be added to a table group if they belong to the same restaurant) are enforced on this layer.
+- ***Application*** - Depends only on the Domain Layer. Use cases are implemented at this layer via MediatR commands and queries (CQRS).
+- ***Infrastructure*** - Depends on the Application and Domain layers. This layer handles Database persistence via EF Core as well as Email, Identity/authentication. This layer implements interfaces defined at the Application layer.
+- ***API*** - Depends on the Application and Infrastructure layers. This is the ASP.NET Core Web API layer. It is implemented using Minimal API endpoints, request/response mapping as well as mapping errors/failure responses to Problem Details. This layer also handles the main wiring of dependency injection.
+
+Dependencies flow inward:
+```mermaid
+graph TD
+ A[API Layer] --> B[Infrastructure Layer]
+ B --> C[Application Layer]
+ C --> D[Domain Layer]
+```
+Dependencies are enforeced via a dedicated architecture test suite.
+
+## Key Design Decisions
+***Multi-Table Reservations***
+The original design rejected any reservation if no single table could seat the full party — a party of 10 would be turned away even if two free 6-seat tables sat right next to each other. Real restaurants don't work this way: staff combine adjacent tables to seat larger parties.
+Rather than computing table combinations dynamically, this project models combinability as pre-approved, restaurant-configured TableGroups — a known, finite set of tables that are physically allowed to be pushed together. Availability search checks single tables first, and only falls back to checking TableGroups (requiring all member tables to be free) if no single table fits. This keeps the search cheap and predictable — a simple loop over a small list, while still matching how restaurants actually operate.
+
+***Preventing Double-Bookings at the Database Level***
+Checking table availability in application code before inserting a reservation leaves a race-condition window: two near-simultaneous requests can both see a table as free and both succeed, double-booking it. Rather than relying solely on optimistic concurrency (which protects against conflicting updates, not conflicting inserts), this project uses a PostgreSQL EXCLUDE constraint on the reservation_tables table, enforced via a GiST index, to reject any overlapping reservation for the same table at the database level. This was done by creating a new migration and editing the migration file with raw PostgreSQL:
+```postgresql
+CREATE EXTENSION IF NOT EXISTS btree_gist;
+ALTER TABLE reservation_tables
+ ADD CONSTRAINT no_overlapping_table_reservations
+ EXCLUDE USING gist (
+ table_id WITH =,
+ tsrange(
+ scheduled_reservation_reservation_day + scheduled_reservation_reservation_start,
+ scheduled_reservation_reservation_day + scheduled_reservation_reservation_end
+ ) WITH &&);
+```
+After which the migration was applied.
+This is verified by an integration test that fires two concurrent reservation attempts for the same table and confirms exactly one succeeds.
+
+***PostgreSQL over SQL Server***
+Beyond PostgreSQL's technical merits, this choice was also deliberate for a couple of reasons First, gaining experience with more than one database provider is something that I feel is very important for a developer, even though EF Core abstracts much of the actual SQL, I still felt it was important to use a different database provider than previous projects. Second, C# and .NET still carry a lingering reputation as a "Windows-only" ecosystem, despite .NET Core being open-source and cross-platform. This entire project was built on a GNU/Linux distribution, using .NET Core/ASP.NET Core and an open-source database to demonstrate that modern .NET applications aren't tied to any single vendor or operating system.
+
+***Mailpit over a Real Email Provider***
+Since this is a learning project and not an actual restaurant reservation system, using a real email provider (e.g., Gmail via an app password) would add setup friction without adding real value — every reviewer would need to configure their own credentials just to see email notifications work. Using MailKit: reservation confirmations and updates are sent exactly as they would be in production, but anyone running the project can simply open Mailpit's web UI and see the emails arrive in real time, with zero configuration required.
+
+## Testing
+This project uses both Architecture tests to ensure/enfore dependencies and Integration tests to test the entire application stack. The decision to exclude Unit Tests was deliberate for the reason being as I was initially writing them. I realized I was testing much of the same logic that was better tested via integration tests.
+
+# What Else Is Implemented
+- Pagination
+- Sorting
+- Filtering
+- Result Pattern
+
+# A Note About Scalar
+For those endpoints that require authention/authorization there will be a drop-down that says ***Auth Type*** please be sure to select ***Bearer*** and then paste the JWT Token where it says ***Bearer Token***.
+
+## Things I Learned
+- Clean Architecture - This was obviously the most important aspect of this project/application. I learned how dependencies should flow, and how separating different concerns (e.g. business logic, from database logic) makes for a much cleaner codebase.
+- Docker - This was perhaps the second most important aspect of this project, or possibly it can be considered as tied for first with Clean Architecture. This is the first project from the C# Academy that I have personally done that required the use of docker. At first this was challenging because it was admittedly new to me, but once I got the hang of it and actually dockerized my own application and ran it entirely containerized I saw both the value and power of using Docker. It is also true as I read that Docker solved the age-old problem of "It works on my machine but not on others".
+- Caching - Caching was not an explict require for this project but in my research I saw the value of caching and wanted to implement it in this project. I decided to use the Hybrid Cache approach available in .NET which allows you to combine local caching with an external caching service (Redis).
+- Problem Details - Learning about problem details and structuring standardized, consistent and easily parseable error messages to inform the end user of any issues with the system.
+- Domain Driven Design - In my study of Clean Architecture, I learned that it is at times combined with Domain Driven Design. I will not say that this project adhers strictly to DDD, but I tried to implement it as best as I could where I could.
+
+## Areas To Improve Upon
+There is always room from improvement especially in software development. While I have learned a tremendous amount and have made many improvements in my own skillset while implementing this project. There is always so much more to learn. I know there are areas of this project that may not be optimal or could have been implemented better. So as with each project I will take what I learned and apply it further and keep learning and improving.
+
+## Helpful Resources Used
+[codewithmukesh](https://codewithmukesh.com/courses/dotnet-webapi-zero-to-hero/lessons/)
diff --git a/restaurant-reservations-clean-architecture/RestaurantReservation.slnx b/restaurant-reservations-clean-architecture/RestaurantReservation.slnx
new file mode 100755
index 0000000..c8d05f7
--- /dev/null
+++ b/restaurant-reservations-clean-architecture/RestaurantReservation.slnx
@@ -0,0 +1,20 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/restaurant-reservations-clean-architecture/docker-compose.yml b/restaurant-reservations-clean-architecture/docker-compose.yml
new file mode 100755
index 0000000..3fb7f6b
--- /dev/null
+++ b/restaurant-reservations-clean-architecture/docker-compose.yml
@@ -0,0 +1,84 @@
+services:
+ api:
+ build:
+ context: .
+ dockerfile: Dockerfile
+ container_name: api
+ ports:
+ - "8080:8080"
+ environment:
+ ASPNETCORE_HTTP_PORTS: 8080
+ ASPNETCORE_ENVIRONMENT: Docker
+ ConnectionStrings__DefaultConnection: "Host=postgres;Port=5432;Database=reservationDB;Username=postgres;Password=postgres"
+ ConnectionStrings__Redis: "redis:6379,password=redis"
+ Seq__ServerUrl: "http://seq:5341"
+ Smtp__Host: "mailpit"
+ Smtp__Port: "1025"
+ depends_on:
+ postgres:
+ condition: "service_healthy"
+ redis:
+ condition: "service_started"
+ seq:
+ condition: "service_started"
+ mailpit:
+ condition: "service_started"
+ postgres:
+ image: postgres:17-alpine
+ container_name: postgres
+ restart: always
+ environment:
+ POSTGRES_USER: postgres
+ POSTGRES_PASSWORD: postgres
+ POSTGRES_DB: reservationDB
+ ports:
+ - "5432:5432"
+ volumes:
+ - postgres_data:/var/lib/postgresql/data
+ healthcheck:
+ test: ["CMD-SHELL", "pg_isready -U postgres -d reservationDB"]
+ interval: 10s
+ timeout: 5s
+ retries: 5
+
+ seq:
+ image: datalust/seq:latest
+ container_name: seq
+ environment:
+ ACCEPT_EULA: Y
+ SEQ_FIRSTRUN_NOAUTHENTICATION: "true"
+ ports:
+ - "5341:5341"
+ - "8081:80"
+ volumes:
+ - seq-data:/data
+
+ mailpit:
+ image: axllent/mailpit
+ container_name: mailpit
+ restart: unless-stopped
+ environment:
+ MP_MAX_MESSAGES: 5000
+ MP_DATABASE: /data/mailpit.db
+ MP_SMTP_AUTH_ACCEPT_ANY: 1
+ MP_SMTP_AUTH_ALLOW_INSECURE: 1
+ ports:
+ - "8025:8025"
+ - "1025:1025"
+ volumes:
+ - mailpit-data:/data
+
+ redis:
+ image: redis:latest
+ container_name: redis
+ ports:
+ - "6379:6379"
+ command: redis-server --requirepass redis --appendonly yes
+ volumes:
+ - redis-data:/data
+
+volumes:
+ postgres_data:
+ seq-data:
+ mailpit-data:
+ redis-data:
\ No newline at end of file
diff --git a/restaurant-reservations-clean-architecture/src/RestaurantReservation.Api/DependencyInjection.cs b/restaurant-reservations-clean-architecture/src/RestaurantReservation.Api/DependencyInjection.cs
new file mode 100755
index 0000000..8b76070
--- /dev/null
+++ b/restaurant-reservations-clean-architecture/src/RestaurantReservation.Api/DependencyInjection.cs
@@ -0,0 +1,95 @@
+using Microsoft.EntityFrameworkCore;
+using Microsoft.Extensions.Caching.Hybrid;
+using Microsoft.OpenApi;
+
+using RestaurantReservation.Api.Handlers;
+using RestaurantReservation.Application.Abstractions;
+using RestaurantReservation.Infrastructure.Persistence;
+
+using Serilog;
+
+namespace RestaurantReservation.Api;
+
+public static class DependencyInjection
+{
+ public static IServiceCollection AddPresentation(this IServiceCollection services, IConfiguration configuration)
+ {
+ services.AddProblemDetails(options =>
+ {
+ options.CustomizeProblemDetails = context =>
+ {
+ context.ProblemDetails.Extensions["traceId"] = context.HttpContext.TraceIdentifier;
+ context.ProblemDetails.Extensions["timestamp"] = DateTime.UtcNow;
+ context.ProblemDetails.Instance =
+ $"{context.HttpContext.Request.Method} {context.HttpContext.Request.Path}";
+ };
+ });
+
+ services.AddExceptionHandler();
+
+ services.AddSerilog((srvs, lc) => lc
+ .ReadFrom.Configuration(configuration)
+ .ReadFrom.Services(srvs));
+
+ services.AddDbContext(options =>
+ {
+ options.UseNpgsql(configuration.GetConnectionString("DefaultConnection"))
+ .UseSnakeCaseNamingConvention();
+ });
+
+ services.AddOpenApi(options =>
+ {
+ options.AddDocumentTransformer((document, _, _) =>
+ {
+ var info = document.Info;
+ info.Title = "Restaurant Reservation Api";
+ info.Description = "An API for Creating, Cancelling, or Updating Restaurant Reservations";
+ info.Contact = new OpenApiContact
+ {
+ Name = "Terrence L. Gee",
+ Email = "mrgee1978@proton.me",
+ Url = new Uri("https://github.com/TerrenceLGee?tab=repositories")
+ };
+ document.Info = info;
+
+ var components = document.Components ?? new OpenApiComponents();
+ components.SecuritySchemes ??= new Dictionary();
+ components.SecuritySchemes["Bearer"] = new OpenApiSecurityScheme
+ {
+ Type = SecuritySchemeType.Http,
+ Scheme = "bearer",
+ BearerFormat = "JWT",
+ Description = "Enter your JWT token"
+ };
+
+ document.Components = components;
+
+ var schemeReference = new OpenApiSecuritySchemeReference("Bearer");
+ var securityRequirement = new OpenApiSecurityRequirement { [schemeReference] = [] };
+
+ document.Security ??= [];
+ document.Security.Add(securityRequirement);
+ return Task.CompletedTask;
+ });
+ });
+
+ services.AddStackExchangeRedisCache(options =>
+ {
+ options.Configuration = configuration.GetConnectionString("Redis");
+ options.InstanceName = "restaurantreservationapi:";
+ });
+
+ services.AddHybridCache(options =>
+ {
+ options.DefaultEntryOptions = new HybridCacheEntryOptions
+ {
+ LocalCacheExpiration = TimeSpan.FromMinutes(5), Expiration = TimeSpan.FromMinutes(30)
+ };
+ options.MaximumPayloadBytes = 1024 * 1024;
+ });
+
+ services.Configure(
+ configuration.GetSection("Smtp"));
+ return services;
+ }
+}
\ No newline at end of file
diff --git a/restaurant-reservations-clean-architecture/src/RestaurantReservation.Api/Endpoints/AuthEndpoints.cs b/restaurant-reservations-clean-architecture/src/RestaurantReservation.Api/Endpoints/AuthEndpoints.cs
new file mode 100755
index 0000000..a3f7e6d
--- /dev/null
+++ b/restaurant-reservations-clean-architecture/src/RestaurantReservation.Api/Endpoints/AuthEndpoints.cs
@@ -0,0 +1,71 @@
+using MediatR;
+
+using RestaurantReservation.Api.Extensions;
+using RestaurantReservation.Application.Features.Auth.Command.Login;
+using RestaurantReservation.Application.Features.Auth.Command.Logout;
+using RestaurantReservation.Application.Features.Auth.Command.Register;
+using RestaurantReservation.Domain.Users.Events;
+
+namespace RestaurantReservation.Api.Endpoints;
+
+public static class AuthEndpoints
+{
+ public static void MapAuthEndpoints(this IEndpointRouteBuilder routes)
+ {
+ var api = routes.MapGroup(Constants.Auth.BaseUri)
+ .WithTags(Constants.Auth.Tag);
+
+ api.MapPost(Constants.Auth.Register, Register)
+ .WithName("RegisterAccount")
+ .WithSummary("Register a new user account");
+
+ api.MapPost(Constants.Auth.Login, Login)
+ .WithName("Login")
+ .WithSummary("Login to your user account");
+
+ api.MapPost(Constants.Auth.Logout, Logout)
+ .WithName("Logout")
+ .WithSummary("Logout from the system")
+ .RequireAuthorization();
+ }
+
+ private static async Task Register(
+ RegisterUserCommand command,
+ IMediator sender,
+ CancellationToken cancellationToken)
+ {
+ var result = await sender.Send(command, cancellationToken);
+
+ if (result.IsFailure) return result.ToProblemDetails();
+
+ await sender.Publish(new RegistrationSuccessfulEvent(
+ command.FirstName,
+ command.LastName,
+ command.EmailAddress,
+ DateOnly.FromDateTime(DateTime.Now)), cancellationToken);
+
+ return TypedResults.Ok("Registration successful!");
+ }
+
+ private static async Task Login(
+ LoginCommand command,
+ ISender sender,
+ CancellationToken cancellationToken)
+ {
+ var result = await sender.Send(command, cancellationToken);
+ return result.IsSuccess
+ ? TypedResults.Ok(result.Value)
+ : result.ToProblemDetails();
+ }
+
+ private static async Task Logout(
+ LogoutCommand command,
+ ISender sender,
+ CancellationToken cancellationToken)
+ {
+ var result = await sender.Send(command, cancellationToken);
+ return result.IsSuccess
+ ? TypedResults.Ok("Logout successful")
+ : result.ToProblemDetails();
+ }
+}
\ No newline at end of file
diff --git a/restaurant-reservations-clean-architecture/src/RestaurantReservation.Api/Endpoints/Constants/Auth.cs b/restaurant-reservations-clean-architecture/src/RestaurantReservation.Api/Endpoints/Constants/Auth.cs
new file mode 100755
index 0000000..0fe934e
--- /dev/null
+++ b/restaurant-reservations-clean-architecture/src/RestaurantReservation.Api/Endpoints/Constants/Auth.cs
@@ -0,0 +1,10 @@
+namespace RestaurantReservation.Api.Endpoints.Constants;
+
+public static class Auth
+{
+ public static string BaseUri => "/api/auth";
+ public static string Tag => "Auth";
+ public static string Register => "/register";
+ public static string Login => "/login";
+ public static string Logout => "/logout";
+}
\ No newline at end of file
diff --git a/restaurant-reservations-clean-architecture/src/RestaurantReservation.Api/Endpoints/Constants/Reservation.cs b/restaurant-reservations-clean-architecture/src/RestaurantReservation.Api/Endpoints/Constants/Reservation.cs
new file mode 100644
index 0000000..03e983e
--- /dev/null
+++ b/restaurant-reservations-clean-architecture/src/RestaurantReservation.Api/Endpoints/Constants/Reservation.cs
@@ -0,0 +1,14 @@
+namespace RestaurantReservation.Api.Endpoints.Constants;
+
+public static class Reservation
+{
+ public static string BaseUri => "/api/reservations";
+ public static string Tag => "Reservations";
+ public static string ScheduleReservation => "/schedulereservation";
+ public static string RescheduleReservation => "/reschedulereservation";
+ public static string CancelReservation => "/cancelreservation";
+ public static string CompleteReservation => "/completereservation";
+ public static string GetAll => "/";
+ public static string GetAllByRestaurant => "/restaurants/{restaurantId:guid}/";
+ public static string Get => "/{reservationId:guid}";
+}
\ No newline at end of file
diff --git a/restaurant-reservations-clean-architecture/src/RestaurantReservation.Api/Endpoints/Constants/Restaurant.cs b/restaurant-reservations-clean-architecture/src/RestaurantReservation.Api/Endpoints/Constants/Restaurant.cs
new file mode 100755
index 0000000..571bec6
--- /dev/null
+++ b/restaurant-reservations-clean-architecture/src/RestaurantReservation.Api/Endpoints/Constants/Restaurant.cs
@@ -0,0 +1,23 @@
+namespace RestaurantReservation.Api.Endpoints.Constants;
+
+public static class Restaurant
+{
+ public static string BaseUri => "/api/restaurants";
+ public static string Tag => "Restaurants";
+ public static string GetAll => "/";
+ public static string Get => "/{id:guid}";
+ public static string Add => "/add";
+ public static string Update => "/update/{id:guid}";
+ public static string Delete => "/delete/{id:guid}";
+ public static string AddTable => "/{id:guid}/tables/add";
+ public static string UpdateTable => "/{restaurantId:guid}/tables/update/{tableId:guid}";
+ public static string DeleteTable => "/{restaurantId:guid}/tables/delete/{tableId:guid}";
+ public static string GetTable => "/{restaurantId:guid}/tables/{tableId:guid}";
+ public static string GetTables => "/{restaurantId:guid}/tables";
+ public static string GetTablesByGroupName => "/{restaurantId:guid}/tables/{tableGroupName}";
+ public static string AddTableGroup => "/{restaurantId:guid}/tablegroups/add";
+ public static string UpdateTableGroup => "/{restaurantId:guid}/tablegroups/update/{tableGroupId:guid}";
+ public static string DeleteTableGroup => "/{restaurantId:guid}/tablegroups/delete/{tableGroupId:guid}";
+ public static string GetTableGroup => "{restaurantId:guid}/tablegroups/{tableGroupId:guid}";
+ public static string GetTableGroups => "{restaurantId:guid}/tablegroups";
+}
\ No newline at end of file
diff --git a/restaurant-reservations-clean-architecture/src/RestaurantReservation.Api/Endpoints/ReservationEndpoints.cs b/restaurant-reservations-clean-architecture/src/RestaurantReservation.Api/Endpoints/ReservationEndpoints.cs
new file mode 100644
index 0000000..7ea9a9a
--- /dev/null
+++ b/restaurant-reservations-clean-architecture/src/RestaurantReservation.Api/Endpoints/ReservationEndpoints.cs
@@ -0,0 +1,212 @@
+using MediatR;
+
+using RestaurantReservation.Api.Extensions;
+using RestaurantReservation.Application.Features.Reservations.Command.Cancel;
+using RestaurantReservation.Application.Features.Reservations.Command.Complete;
+using RestaurantReservation.Application.Features.Reservations.Command.Reschedule;
+using RestaurantReservation.Application.Features.Reservations.Command.Schedule;
+using RestaurantReservation.Application.Features.Reservations.Query.Get;
+using RestaurantReservation.Application.Features.Reservations.Query.GetAll;
+using RestaurantReservation.Domain.Reservations.Events;
+using RestaurantReservation.Domain.Users;
+
+namespace RestaurantReservation.Api.Endpoints;
+
+public static class ReservationEndpoints
+{
+ public static void MapReservationEndpoints(this IEndpointRouteBuilder app)
+ {
+ var api = app.MapGroup(Constants.Reservation.BaseUri)
+ .WithTags(Constants.Reservation.Tag)
+ .WithDescription("Schedule, Reschedule, Complete or Cancel a Reservation")
+ .RequireAuthorization();
+
+ api.MapPost(Constants.Reservation.ScheduleReservation, ScheduleReservation)
+ .WithName("ScheduleReservation")
+ .WithSummary("Schedule a reservation with one of our restaurants");
+
+ api.MapPut(Constants.Reservation.RescheduleReservation, RescheduleReservation)
+ .WithName("RescheduleReservation")
+ .WithSummary("Reschedule a reservation");
+
+ api.MapPut(Constants.Reservation.CancelReservation, CancelReservation)
+ .WithName("CancelReservation")
+ .WithSummary("Cancel a reservation");
+
+ api.MapPut(Constants.Reservation.CompleteReservation, CompleteReservation)
+ .WithName("CompleteReservation")
+ .WithSummary("Complete your reservation");
+
+ api.MapGet(Constants.Reservation.GetAll, GetAll)
+ .WithName("GetAllReservations")
+ .WithSummary("Get all your reservations");
+
+ api.MapGet(Constants.Reservation.GetAllByRestaurant, GetAllByRestaurant)
+ .WithName("GetAllRestaurantReservations")
+ .WithSummary("Get all reservations tied to a restaurant in the system (Admins Only)")
+ .RequireAuthorization(policy =>
+ policy.RequireRole(Roles.Admin));
+
+ api.MapGet(Constants.Reservation.Get, Get)
+ .WithName("GetReservation")
+ .WithSummary("Get one of your individual reservations");
+ }
+
+ private static async Task ScheduleReservation(
+ ScheduleReservationCommand command,
+ IMediator sender,
+ CancellationToken cancellationToken)
+ {
+ var result = await sender.Send(command, cancellationToken);
+
+ if (result.IsFailure) return result.ToProblemDetails();
+
+ await sender.Publish(new ReservationScheduledEvent(
+ result.Value.ReservationId,
+ result.Value.CustomerFirstName,
+ result.Value.CustomerLastName,
+ result.Value.CustomerEmail,
+ result.Value.RestaurantName,
+ result.Value.ReservationDate,
+ result.Value.ReservationStartTime,
+ result.Value.ReservationEndTime,
+ result.Value.NumberOfGuests),
+ cancellationToken);
+
+ return TypedResults.Created($"/api/reservations/{result.Value.ReservationId}", result.Value);
+ }
+
+ private static async Task RescheduleReservation(
+ RescheduleReservationCommand command,
+ IMediator sender,
+ CancellationToken cancellationToken)
+ {
+ var result = await sender.Send(command, cancellationToken);
+
+ if (result.IsFailure) return result.ToProblemDetails();
+
+ await sender.Publish(new ReservationRescheduledEvent(
+ result.Value.ReservationId,
+ result.Value.CustomerFirstName,
+ result.Value.CustomerLastName,
+ result.Value.CustomerEmail,
+ result.Value.RestaurantName,
+ result.Value.OriginalReservationDate,
+ result.Value.OriginalReservationStartTime,
+ result.Value.OriginalReservationEndTime,
+ result.Value.RescheduledReservationDate,
+ result.Value.RescheduledReservationStartTime,
+ result.Value.RescheduledReservationEndTime,
+ result.Value.OriginalNumberOfGuests),
+ cancellationToken);
+
+ return TypedResults.Ok(result.Value);
+ }
+
+ private static async Task CancelReservation(
+ CancelReservationCommand command,
+ IMediator sender,
+ CancellationToken cancellationToken)
+ {
+ var result = await sender.Send(command, cancellationToken);
+
+ if (result.IsFailure) return result.ToProblemDetails();
+
+ await sender.Publish(new ReservationCanceledEvent(
+ result.Value.ReservationId,
+ result.Value.CustomerFirstName,
+ result.Value.CustomerLastName,
+ result.Value.CustomerEmail,
+ result.Value.RestaurantName,
+ result.Value.ReservationDate,
+ result.Value.ReservationStartTime,
+ result.Value.ReservationEndTime,
+ result.Value.ReservationCanceledAtUtc!.Value),
+ cancellationToken);
+
+ return TypedResults.Ok(result.Value);
+ }
+
+ private static async Task CompleteReservation(
+ CompleteReservationCommand command,
+ IMediator sender,
+ CancellationToken cancellationToken)
+ {
+ var result = await sender.Send(command, cancellationToken);
+
+ if (result.IsFailure) return result.ToProblemDetails();
+
+ await sender.Publish(new ReservationCompletedEvent(
+ result.Value.ReservationId,
+ result.Value.CustomerFirstName,
+ result.Value.CustomerLastName,
+ result.Value.CustomerEmail,
+ result.Value.RestaurantName,
+ result.Value.ReservationDate,
+ result.Value.ReservationStartTime,
+ result.Value.ReservationEndTime,
+ result.Value.ReservationCompletedAtUtc!.Value),
+ cancellationToken);
+
+ return TypedResults.Ok(result.Value);
+ }
+
+ private static async Task GetAll(
+ int? page,
+ int? pageSize,
+ string? sortBy,
+ string? restaurantName,
+ string? status,
+ ISender sender,
+ CancellationToken cancellationToken)
+ {
+ var query = new GetAllReservationsQuery(
+ page ?? 1,
+ pageSize ?? 10,
+ sortBy,
+ restaurantName,
+ status);
+
+ var result = await sender.Send(query, cancellationToken);
+
+ return result.IsSuccess
+ ? TypedResults.Ok(result.Value)
+ : result.ToProblemDetails();
+ }
+
+ private static async Task GetAllByRestaurant(
+ Guid restaurantId,
+ int? page,
+ int? pageSize,
+ string? sortBy,
+ string? status,
+ ISender sender,
+ CancellationToken cancellationToken)
+ {
+ var query = new GetAllRestaurantReservationsQuery(
+ restaurantId,
+ page ?? 1,
+ pageSize ?? 10,
+ sortBy,
+ status);
+
+ var result = await sender.Send(query, cancellationToken);
+
+ return result.IsSuccess
+ ? TypedResults.Ok(result.Value)
+ : result.ToProblemDetails();
+ }
+
+ private static async Task Get(
+ Guid reservationId,
+ ISender sender,
+ CancellationToken cancellationToken)
+ {
+ var query = new GetReservationQuery(reservationId);
+ var result = await sender.Send(query, cancellationToken);
+
+ return result.IsSuccess
+ ? TypedResults.Ok(result.Value)
+ : result.ToProblemDetails();
+ }
+}
\ No newline at end of file
diff --git a/restaurant-reservations-clean-architecture/src/RestaurantReservation.Api/Endpoints/RestaurantEndpoints.cs b/restaurant-reservations-clean-architecture/src/RestaurantReservation.Api/Endpoints/RestaurantEndpoints.cs
new file mode 100755
index 0000000..00b671c
--- /dev/null
+++ b/restaurant-reservations-clean-architecture/src/RestaurantReservation.Api/Endpoints/RestaurantEndpoints.cs
@@ -0,0 +1,412 @@
+using MediatR;
+
+using RestaurantReservation.Api.Extensions;
+using RestaurantReservation.Application.Features.Restaurants.Command.Add;
+using RestaurantReservation.Application.Features.Restaurants.Command.Delete;
+using RestaurantReservation.Application.Features.Restaurants.Command.Update;
+using RestaurantReservation.Application.Features.Restaurants.Query.Get;
+using RestaurantReservation.Application.Features.Restaurants.Query.GetAll;
+using RestaurantReservation.Application.Features.Restaurants.TableGroups.Command.Add;
+using RestaurantReservation.Application.Features.Restaurants.TableGroups.Command.Delete;
+using RestaurantReservation.Application.Features.Restaurants.TableGroups.Command.Update;
+using RestaurantReservation.Application.Features.Restaurants.TableGroups.Query.Get;
+using RestaurantReservation.Application.Features.Restaurants.TableGroups.Query.GetAll;
+using RestaurantReservation.Application.Features.Restaurants.Tables.Command.Add;
+using RestaurantReservation.Application.Features.Restaurants.Tables.Command.Delete;
+using RestaurantReservation.Application.Features.Restaurants.Tables.Command.Update;
+using RestaurantReservation.Application.Features.Restaurants.Tables.Query.Get;
+using RestaurantReservation.Application.Features.Restaurants.Tables.Query.GetAll;
+using RestaurantReservation.Domain.Users;
+
+namespace RestaurantReservation.Api.Endpoints;
+
+public static class RestaurantEndpoints
+{
+ public static void MapRestaurantEndpoints(this IEndpointRouteBuilder app)
+ {
+ var api = app.MapGroup(Constants.Restaurant.BaseUri)
+ .WithTags(Constants.Restaurant.Tag)
+ .WithDescription("Available Restaurants");
+
+ api.MapGet(Constants.Restaurant.GetAll, GetAll)
+ .WithName("GetAllRestaurants")
+ .WithSummary("Get a listing of all available restaurants");
+
+ api.MapGet(Constants.Restaurant.Get, Get)
+ .WithName("GetRestaurantById")
+ .WithSummary("Get a restaurant by id");
+
+ api.MapPost(Constants.Restaurant.Add, Add)
+ .WithName("AddRestaurant")
+ .WithSummary("Add a new restaurant to the system (Admins Only)")
+ .RequireAuthorization(policy =>
+ policy.RequireRole(Roles.Admin));
+
+ api.MapPut(Constants.Restaurant.Update, Update)
+ .WithName("UpdateRestaurant")
+ .WithSummary("Update an existing restaurant in the system (Admins Only)")
+ .RequireAuthorization(policy =>
+ policy.RequireRole(Roles.Admin));
+
+ api.MapDelete(Constants.Restaurant.Delete, Delete)
+ .WithName("DeleteRestaurant")
+ .WithSummary("Delete an existing restaurant that's in the system (Admins Only)")
+ .RequireAuthorization(policy =>
+ policy.RequireRole(Roles.Admin));
+
+ api.MapPost(Constants.Restaurant.AddTable, AddTable)
+ .WithName("AddTable")
+ .WithSummary("Add a table to a restaurant in the system (Admins Only)")
+ .RequireAuthorization(policy =>
+ policy.RequireRole(Roles.Admin));
+
+ api.MapPut(Constants.Restaurant.UpdateTable, UpdateTable)
+ .WithName("UpdateTable")
+ .WithSummary("Update a table in a restaurant in the system (Admins Only)")
+ .RequireAuthorization(policy =>
+ policy.RequireRole(Roles.Admin));
+
+ api.MapDelete(Constants.Restaurant.DeleteTable, DeleteTable)
+ .WithName("DeleteTable")
+ .WithSummary("Delete a table that's in a restaurant in the system (Admins Only)")
+ .RequireAuthorization(policy =>
+ policy.RequireRole(Roles.Admin));
+
+ api.MapGet(Constants.Restaurant.GetTable, GetTable)
+ .WithName("GetTable")
+ .WithSummary("Get a table by id that's in a restaurant in the system (Admins Only)")
+ .RequireAuthorization(policy =>
+ policy.RequireRole(Roles.Admin));
+
+ api.MapGet(Constants.Restaurant.GetTables, GetTables)
+ .WithName("GetTables")
+ .WithSummary("Get all tables belonging to restaurant in the system (Admins Only)")
+ .RequireAuthorization(policy =>
+ policy.RequireRole(Roles.Admin));
+
+ api.MapGet(Constants.Restaurant.GetTablesByGroupName, GetTablesByGroupName)
+ .WithName("GetTablesByGroupName")
+ .WithSummary("Get all tables belonging to a table group in a restaurant in the system (Admins Only)")
+ .RequireAuthorization(policy =>
+ policy.RequireRole(Roles.Admin));
+
+ api.MapPost(Constants.Restaurant.AddTableGroup, AddTableGroup)
+ .WithName("AddTableGroup")
+ .WithSummary("Add a new table group to a restaurant in the system (Admins Only)")
+ .RequireAuthorization(policy =>
+ policy.RequireRole(Roles.Admin));
+
+ api.MapPut(Constants.Restaurant.UpdateTableGroup, UpdateTableGroup)
+ .WithName("UpdateTableGroup")
+ .WithSummary("Update an existing table group in a restaurant in the system (Admins Only)")
+ .RequireAuthorization(policy =>
+ policy.RequireRole(Roles.Admin));
+
+ api.MapDelete(Constants.Restaurant.DeleteTableGroup, DeleteTableGroup)
+ .WithName("DeleteTableGroup")
+ .WithSummary("Delete an existing table group in a restaurant in the system (Admins Only)")
+ .RequireAuthorization(policy =>
+ policy.RequireRole(Roles.Admin));
+
+ api.MapGet(Constants.Restaurant.GetTableGroup, GetTableGroup)
+ .WithName("GetTableGroup")
+ .WithSummary("Get a table group that is in a restaurant in the system (Admins Only)")
+ .RequireAuthorization(policy =>
+ policy.RequireRole(Roles.Admin));
+
+ api.MapGet(Constants.Restaurant.GetTableGroups, GetTableGroups)
+ .WithName("GetTableGroups")
+ .WithSummary("Get all table groups belonging to a restaurant in the system. (Admins Only)")
+ .RequireAuthorization(policy =>
+ policy.RequireRole(Roles.Admin));
+ }
+
+ private static async Task GetAll(
+ int? page,
+ int? pageSize,
+ string? sortBy,
+ string? name,
+ ISender sender,
+ CancellationToken cancellationToken)
+ {
+ var query = new GetAllRestaurantsQuery(
+ page ?? 1,
+ pageSize ?? 10,
+ sortBy,
+ name);
+ var result = await sender.Send(query, cancellationToken);
+
+ return result.IsSuccess
+ ? TypedResults.Ok(result.Value)
+ : result.ToProblemDetails();
+ }
+
+ private static async Task Get(
+ Guid id,
+ int? tablePage,
+ int? tablePageSize,
+ ISender sender,
+ CancellationToken cancellationToken)
+ {
+ var query = new GetRestaurantQuery(
+ id,
+ tablePage ?? 1,
+ tablePageSize ?? 10);
+ var result = await sender.Send(query, cancellationToken);
+
+ return result.IsSuccess
+ ? TypedResults.Ok(result.Value)
+ : result.ToProblemDetails();
+ }
+
+ private static async Task Add(
+ AddRestaurantCommand command,
+ ISender sender,
+ CancellationToken cancellationToken)
+ {
+ var result = await sender.Send(command, cancellationToken);
+
+ return result.IsSuccess
+ ? TypedResults.Created($"/restaurants/{result.Value.Id}", result.Value)
+ : result.ToProblemDetails();
+ }
+
+ private static async Task Update(
+ Guid id,
+ UpdateRestaurantQuery query,
+ ISender sender,
+ CancellationToken cancellationToken)
+ {
+ var command = new UpdateRestaurantCommand(
+ id,
+ query.Name,
+ query.Schedule);
+
+ var result = await sender.Send(command, cancellationToken);
+
+ return result.IsSuccess
+ ? TypedResults.NoContent()
+ : result.ToProblemDetails();
+ }
+
+ private static async Task Delete(
+ Guid id,
+ ISender sender,
+ CancellationToken cancellationToken)
+ {
+ var command = new DeleteRestaurantCommand(id);
+
+ var result = await sender.Send(command, cancellationToken);
+
+ return result.IsSuccess
+ ? TypedResults.NoContent()
+ : result.ToProblemDetails();
+ }
+
+ private static async Task AddTable(
+ Guid id,
+ AddTableQuery query,
+ ISender sender,
+ CancellationToken cancellationToken)
+ {
+ var command = new AddTableCommand(
+ id,
+ query.NumberOfSeats,
+ query.TableGroup);
+
+ var result = await sender.Send(command, cancellationToken);
+
+ return result.IsSuccess
+ ? TypedResults.Created($"/restaurants/{id}/tables/{result.Value.Id}", result.Value)
+ : result.ToProblemDetails();
+ }
+
+ private static async Task UpdateTable(
+ Guid restaurantId,
+ Guid tableId,
+ UpdateTableQuery query,
+ ISender sender,
+ CancellationToken cancellationToken)
+ {
+ var command = new UpdateTableCommand(
+ restaurantId,
+ tableId,
+ query.NumberOfSeats,
+ query.GroupName);
+
+ var result = await sender.Send(command, cancellationToken);
+
+ return result.IsSuccess
+ ? TypedResults.NoContent()
+ : result.ToProblemDetails();
+ }
+
+ private static async Task DeleteTable(
+ Guid restaurantId,
+ Guid tableId,
+ ISender sender,
+ CancellationToken cancellationToken)
+ {
+ var command = new DeleteTableCommand(
+ restaurantId,
+ tableId);
+
+ var result = await sender.Send(command, cancellationToken);
+
+ return result.IsSuccess
+ ? TypedResults.NoContent()
+ : result.ToProblemDetails();
+ }
+
+ private static async Task GetTable(
+ Guid restaurantId,
+ Guid tableId,
+ ISender sender,
+ CancellationToken cancellationToken)
+ {
+ var command = new GetTableQuery(restaurantId, tableId);
+
+ var result = await sender.Send(command, cancellationToken);
+
+ return result.IsSuccess
+ ? TypedResults.Ok(result.Value)
+ : result.ToProblemDetails();
+ }
+
+ private static async Task GetTables(
+ Guid restaurantId,
+ int? page,
+ int? pageSize,
+ string? sortBy,
+ ISender sender,
+ CancellationToken cancellationToken)
+ {
+ var query = new GetAllTablesByRestaurantQuery(restaurantId, page ?? 1, pageSize ?? 10, sortBy);
+
+ var result = await sender.Send(query, cancellationToken);
+
+ return result.IsSuccess
+ ? TypedResults.Ok(result.Value)
+ : result.ToProblemDetails();
+ }
+
+ private static async Task GetTablesByGroupName(
+ Guid restaurantId,
+ string tableGroupName,
+ int? page,
+ int? pageSize,
+ string? sortBy,
+ ISender sender,
+ CancellationToken cancellationToken)
+ {
+ var query = new GetAllTablesByTableGroupQuery(
+ restaurantId,
+ tableGroupName,
+ page ?? 1,
+ pageSize ?? 10,
+ sortBy);
+
+ var result = await sender.Send(query, cancellationToken);
+
+ return result.IsSuccess
+ ? TypedResults.Ok(result.Value)
+ : result.ToProblemDetails();
+ }
+
+ private static async Task AddTableGroup(
+ Guid restaurantId,
+ AddTableGroupQuery query,
+ ISender sender,
+ CancellationToken cancellationToken)
+ {
+ var command = new AddTableGroupCommand(
+ restaurantId,
+ query.Name,
+ query.NumberOfTables,
+ query.NumberOfSeats);
+
+ var result = await sender.Send(command, cancellationToken);
+
+ return result.IsSuccess
+ ? TypedResults.Created($"{restaurantId}/tablegroups/{result.Value.Id}", result.Value)
+ : result.ToProblemDetails();
+ }
+
+ private static async Task UpdateTableGroup(
+ Guid restaurantId,
+ Guid tableGroupId,
+ UpdateTableGroupQuery query,
+ ISender sender,
+ CancellationToken cancellationToken)
+ {
+ var command = new UpdateTableGroupCommand(
+ restaurantId,
+ tableGroupId,
+ query.GroupName,
+ query.NumberOfTablesToAdd,
+ query.NumberOfSeats);
+
+ var result = await sender.Send(command, cancellationToken);
+
+ return result.IsSuccess
+ ? TypedResults.NoContent()
+ : result.ToProblemDetails();
+ }
+
+ private static async Task DeleteTableGroup(
+ Guid restaurantId,
+ Guid tableGroupId,
+ ISender sender,
+ CancellationToken cancellationToken)
+ {
+ var command = new DeleteTableGroupCommand(
+ restaurantId,
+ tableGroupId);
+
+ var result = await sender.Send(command, cancellationToken);
+
+ return result.IsSuccess
+ ? TypedResults.NoContent()
+ : result.ToProblemDetails();
+ }
+
+ private static async Task GetTableGroup(
+ Guid restaurantId,
+ Guid tableGroupId,
+ ISender sender,
+ CancellationToken cancellationToken)
+ {
+ var command = new GetTableGroupQuery(
+ restaurantId,
+ tableGroupId);
+
+ var result = await sender.Send(command, cancellationToken);
+
+ return result.IsSuccess
+ ? TypedResults.Ok(result.Value)
+ : result.ToProblemDetails();
+ }
+
+ private static async Task GetTableGroups(
+ Guid restaurantId,
+ int? page,
+ int? pageSize,
+ string? sortBy,
+ string? name,
+ ISender sender,
+ CancellationToken cancellationToken)
+ {
+ var command = new GetAllTableGroupsQuery(
+ restaurantId,
+ page ?? 1,
+ pageSize ?? 10,
+ sortBy,
+ name);
+
+ var result = await sender.Send(command, cancellationToken);
+
+ return result.IsSuccess
+ ? TypedResults.Ok(result.Value)
+ : result.ToProblemDetails();
+ }
+}
\ No newline at end of file
diff --git a/restaurant-reservations-clean-architecture/src/RestaurantReservation.Api/Extensions/ResultExtensions.cs b/restaurant-reservations-clean-architecture/src/RestaurantReservation.Api/Extensions/ResultExtensions.cs
new file mode 100755
index 0000000..ef1b860
--- /dev/null
+++ b/restaurant-reservations-clean-architecture/src/RestaurantReservation.Api/Extensions/ResultExtensions.cs
@@ -0,0 +1,90 @@
+using RestaurantReservation.Domain.Common;
+
+namespace RestaurantReservation.Api.Extensions;
+
+public static class ResultExtensions
+{
+ public static IResult ToProblemDetails(this Result result)
+ {
+ result.CheckResult();
+
+ var errorType = ErrorType.None;
+ var description = string.Empty;
+ var errorCode = string.Empty;
+
+ if (result.Errors is not null && result.Errors.Count > 0)
+ {
+ errorType = result.Errors[0].ErrorType;
+ description = result.Errors[0].Description;
+ errorCode = result.Errors[0].Code;
+ }
+ else
+ {
+ errorType = result.Error.ErrorType;
+ description = result.Error.Description;
+ errorCode = result.Error.Code;
+ }
+
+ (int statusCode, string title) = MapErrorType(errorType);
+
+ return Results.Problem(
+ statusCode: statusCode,
+ title: title,
+ detail: description,
+ extensions: new Dictionary
+ {
+ ["errorCode"] = errorCode
+ });
+ }
+
+ public static IResult ToProblemDetails(this Result result)
+ {
+ result.CheckResult();
+
+ var errorType = ErrorType.None;
+ var description = string.Empty;
+ var errorCode = string.Empty;
+
+ if (result.Errors is not null && result.Errors.Count > 0)
+ {
+ errorType = result.Errors[0].ErrorType;
+ description = result.Errors[0].Description;
+ errorCode = result.Errors[0].Code;
+ }
+ else
+ {
+ errorType = result.Error.ErrorType;
+ description = result.Error.Description;
+ errorCode = result.Error.Code;
+ }
+
+ (int statusCode, string title) = MapErrorType(errorType);
+
+ return Results.Problem(
+ statusCode: statusCode,
+ title: title,
+ detail: description,
+ extensions: new Dictionary { ["errorCode"] = errorCode });
+ }
+
+
+ private static (int StatusCode, string Title) MapErrorType(ErrorType type) => type switch
+ {
+ ErrorType.Validation => (StatusCodes.Status400BadRequest, "Validation Error"),
+ ErrorType.BadRequest => (StatusCodes.Status400BadRequest, "Bad Request"),
+ ErrorType.NotFound => (StatusCodes.Status404NotFound, "Not Found"),
+ ErrorType.Conflict => (StatusCodes.Status409Conflict, "Conflict"),
+ ErrorType.Unauthorized => (StatusCodes.Status401Unauthorized, "Unauthorized"),
+ ErrorType.Forbidden => (StatusCodes.Status403Forbidden, "Forbidden"),
+ ErrorType.CapacityExceeded => (StatusCodes.Status400BadRequest, "Bad Request: Capacity Exceeded"),
+ _ => (StatusCodes.Status500InternalServerError, "Internal Server Error")
+ };
+
+ private static void CheckResult(this Result result)
+ {
+ if (result.IsSuccess || result.Error == DomainError.None)
+ {
+ throw new InvalidOperationException("Cannot convert a successful result to a problem detail");
+ }
+ }
+}
\ No newline at end of file
diff --git a/restaurant-reservations-clean-architecture/src/RestaurantReservation.Api/Handlers/GlobalExceptionHandler.cs b/restaurant-reservations-clean-architecture/src/RestaurantReservation.Api/Handlers/GlobalExceptionHandler.cs
new file mode 100755
index 0000000..100233e
--- /dev/null
+++ b/restaurant-reservations-clean-architecture/src/RestaurantReservation.Api/Handlers/GlobalExceptionHandler.cs
@@ -0,0 +1,66 @@
+using FluentValidation;
+
+using Microsoft.AspNetCore.Diagnostics;
+using Microsoft.AspNetCore.Mvc;
+
+namespace RestaurantReservation.Api.Handlers;
+
+public class GlobalExceptionHandler(
+ ILogger logger,
+ IProblemDetailsService problemDetailsService) : IExceptionHandler
+{
+ public async ValueTask TryHandleAsync(
+ HttpContext httpContext,
+ Exception exception,
+ CancellationToken cancellationToken)
+ {
+ logger.LogError(
+ exception,
+ "Unhandled exception occurred. TraceId: {TraceId}",
+ httpContext.TraceIdentifier);
+
+ (int statusCode, string title) = MapException(exception);
+
+ httpContext.Response.StatusCode = statusCode;
+
+ var problemDetails = new ProblemDetails
+ {
+ Status = statusCode,
+ Type = GetProblemType(statusCode),
+ Title = title,
+ Detail = httpContext.RequestServices
+ .GetRequiredService()
+ .IsDevelopment()
+ ? exception.Message
+ : "An unexpected error has occurred",
+ Instance = httpContext.Request.Path,
+ Extensions = { ["traceId"] = httpContext.TraceIdentifier, ["timestamp"] = DateTime.UtcNow }
+ };
+
+ return await problemDetailsService.TryWriteAsync(new ProblemDetailsContext
+ {
+ HttpContext = httpContext, ProblemDetails = problemDetails
+ });
+ }
+
+ private static (int StatusCode, string Title) MapException(Exception exception) => exception switch
+ {
+ ValidationException => (StatusCodes.Status400BadRequest, "Validation error occurred"),
+ ArgumentNullException => (StatusCodes.Status400BadRequest, "Invalid argument provided"),
+ ArgumentException => (StatusCodes.Status400BadRequest, "Invalid argument provided"),
+ InvalidOperationException => (StatusCodes.Status400BadRequest, "Invalid operation attempted"),
+ UnauthorizedAccessException => (StatusCodes.Status401Unauthorized, "Unauthorized"),
+ BadHttpRequestException => (StatusCodes.Status400BadRequest, "Invalid Http Request"),
+ _ => (StatusCodes.Status500InternalServerError, "An unexpected error occurred")
+ };
+
+ private static string GetProblemType(int statusCode) => statusCode switch
+ {
+ 400 => "https://tools.ietf.org/html/rfc9110#section-15.5.1",
+ 401 => "https://tools.ietf.org/html/rfc9110#section-15.5.2",
+ 403 => "https://tools.ietf.org/html/rfc9110#section-15.5.4",
+ 404 => "https://tools.ietf.org/html/rfc9110#section-15.5.5",
+ 409 => "https://tools.ietf.org/html/rfc9110#section-15.5.10",
+ _ => "https://tools.ietf.org/html/rfc9110#section-15.6.1"
+ };
+}
\ No newline at end of file
diff --git a/restaurant-reservations-clean-architecture/src/RestaurantReservation.Api/Program.cs b/restaurant-reservations-clean-architecture/src/RestaurantReservation.Api/Program.cs
new file mode 100755
index 0000000..4c53d4a
--- /dev/null
+++ b/restaurant-reservations-clean-architecture/src/RestaurantReservation.Api/Program.cs
@@ -0,0 +1,80 @@
+#pragma warning disable EXTEXP0018
+
+using Microsoft.AspNetCore.Identity;
+using Microsoft.EntityFrameworkCore;
+
+using RestaurantReservation.Api;
+using RestaurantReservation.Api.Endpoints;
+using RestaurantReservation.Application;
+using RestaurantReservation.Domain.Users;
+using RestaurantReservation.Infrastructure;
+using RestaurantReservation.Infrastructure.Persistence;
+using RestaurantReservation.Infrastructure.Persistence.Seeding;
+
+using Scalar.AspNetCore;
+
+using Serilog;
+
+Log.Logger = new LoggerConfiguration()
+ .WriteTo.Console()
+ .CreateBootstrapLogger();
+
+try
+{
+ var builder = WebApplication.CreateBuilder(args);
+
+ builder.Services.AddPresentation(builder.Configuration)
+ .AddInfrastructure(builder.Configuration)
+ .AddApplication();
+
+ var app = builder.Build();
+
+ app.UseStatusCodePages();
+ app.UseSerilogRequestLogging(options =>
+ {
+ options.EnrichDiagnosticContext = (diagnosticContext, httpContext) =>
+ {
+ diagnosticContext.Set("RequestHost", httpContext.Request.Host.Value);
+ diagnosticContext.Set("UserAgent", httpContext.Request.Headers.UserAgent.ToString());
+ };
+ });
+
+ app.UseExceptionHandler();
+ app.UseHttpsRedirection();
+ app.UseAuthentication();
+ app.UseAuthorization();
+
+ if (app.Environment.IsDevelopment() || app.Environment.IsEnvironment("Docker"))
+ {
+ app.MapOpenApi();
+ app.MapScalarApiReference(options =>
+ {
+ options.WithTitle("Restaurant Reservation API");
+ options.WithTheme(ScalarTheme.BluePlanet);
+ options.WithDefaultHttpClient(ScalarTarget.Shell, ScalarClient.Curl);
+ });
+ }
+
+ using (var scope = app.Services.CreateScope())
+ {
+ var userManager = scope.ServiceProvider.GetRequiredService>();
+ var roleManager = scope.ServiceProvider.GetRequiredService>();
+ var context = scope.ServiceProvider.GetRequiredService();
+
+ await context.Database.MigrateAsync();
+ await UserAndRoleSeeder.SeedUsersAndRolesAsync(userManager, roleManager);
+ }
+
+ app.MapAuthEndpoints();
+ app.MapRestaurantEndpoints();
+ app.MapReservationEndpoints();
+ app.Run();
+}
+catch (Exception ex)
+{
+ Log.Fatal(ex, "Server terminated unexpectedly");
+}
+finally
+{
+ Log.CloseAndFlush();
+}
diff --git a/restaurant-reservations-clean-architecture/src/RestaurantReservation.Api/Properties/launchSettings.json b/restaurant-reservations-clean-architecture/src/RestaurantReservation.Api/Properties/launchSettings.json
new file mode 100755
index 0000000..fd72725
--- /dev/null
+++ b/restaurant-reservations-clean-architecture/src/RestaurantReservation.Api/Properties/launchSettings.json
@@ -0,0 +1,25 @@
+{
+ "$schema": "https://json.schemastore.org/launchsettings.json",
+ "profiles": {
+ "http": {
+ "commandName": "Project",
+ "dotnetRunMessages": true,
+ "launchBrowser": true,
+ "launchUrl": "scalar/v1",
+ "applicationUrl": "http://localhost:5001",
+ "environmentVariables": {
+ "ASPNETCORE_ENVIRONMENT": "Development"
+ }
+ },
+ "https": {
+ "commandName": "Project",
+ "dotnetRunMessages": true,
+ "launchBrowser": true,
+ "launchUrl": "scalar/v1",
+ "applicationUrl": "https://localhost:7001;http://localhost:5001",
+ "environmentVariables": {
+ "ASPNETCORE_ENVIRONMENT": "Development"
+ }
+ }
+ }
+}
diff --git a/restaurant-reservations-clean-architecture/src/RestaurantReservation.Api/RestaurantReservation.Api.csproj b/restaurant-reservations-clean-architecture/src/RestaurantReservation.Api/RestaurantReservation.Api.csproj
new file mode 100755
index 0000000..41fcf25
--- /dev/null
+++ b/restaurant-reservations-clean-architecture/src/RestaurantReservation.Api/RestaurantReservation.Api.csproj
@@ -0,0 +1,31 @@
+
+
+
+
+
+
+
+
+
+
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+ all
+
+
+
+
+
+
+
+
+
+
+
+
+
+ net10.0
+ enable
+ enable
+
+
+
diff --git a/restaurant-reservations-clean-architecture/src/RestaurantReservation.Api/appsettings.Development.json b/restaurant-reservations-clean-architecture/src/RestaurantReservation.Api/appsettings.Development.json
new file mode 100755
index 0000000..0c208ae
--- /dev/null
+++ b/restaurant-reservations-clean-architecture/src/RestaurantReservation.Api/appsettings.Development.json
@@ -0,0 +1,8 @@
+{
+ "Logging": {
+ "LogLevel": {
+ "Default": "Information",
+ "Microsoft.AspNetCore": "Warning"
+ }
+ }
+}
diff --git a/restaurant-reservations-clean-architecture/src/RestaurantReservation.Api/appsettings.Docker.json b/restaurant-reservations-clean-architecture/src/RestaurantReservation.Api/appsettings.Docker.json
new file mode 100644
index 0000000..dd9cc68
--- /dev/null
+++ b/restaurant-reservations-clean-architecture/src/RestaurantReservation.Api/appsettings.Docker.json
@@ -0,0 +1,9 @@
+{
+ "Seq": {
+ "ServerUrl" : "http://seq:5341"
+ },
+ "Smtp": {
+ "Host": "mailpit",
+ "Port": 1025
+ }
+}
\ No newline at end of file
diff --git a/restaurant-reservations-clean-architecture/src/RestaurantReservation.Api/appsettings.json b/restaurant-reservations-clean-architecture/src/RestaurantReservation.Api/appsettings.json
new file mode 100755
index 0000000..e1148db
--- /dev/null
+++ b/restaurant-reservations-clean-architecture/src/RestaurantReservation.Api/appsettings.json
@@ -0,0 +1,64 @@
+{
+ "Serilog": {
+ "Using": [
+ "Serilog.Sinks.Console",
+ "Serilog.Sinks.File",
+ "Serilog.Sinks.Seq",
+ "Serilog.Enrichers.Environment",
+ "Serilog.Enrichers.Thread",
+ "Serilog.Enrichers.Process",
+ "Serilog.Exceptions"
+ ],
+ "MinimumLevel": {
+ "Default": "Information",
+ "Override": {
+ "Microsoft": "Warning",
+ "Microsoft.AspNetCore.Hosting.Diagnostics": "Error",
+ "Microsoft.Hosting.Lifetime": "Information",
+ "Microsoft.EntityFrameworkCore.Database.Command": "Information",
+ "System.Net.HttpClient": "Information"
+ }
+ },
+ "WriteTo": [
+ { "Name": "Console" },
+ {
+ "Name": "File",
+ "Args": {
+ "path": "logs/log-.json",
+ "rollingInterval": "Day",
+ "rollOnFileSizeLimit": true,
+ "fileSizeLimitBytes": 104857600,
+ "retainedFileCountLimit": 14,
+ "formatter": "Serilog.Formatting.Compact.CompactJsonFormatter, Serilog.Formatting.Compact"
+ }
+ },
+ {
+ "Name": "Seq",
+ "Args": { "serverUrl": "http://localhost:5341" }
+ }
+ ],
+ "Enrich": [
+ "FromLogContext",
+ "WithMachineName",
+ "WithProcessId",
+ "WithThreadId",
+ "WithExceptionDetails"
+ ]
+ },
+ "AllowedHosts": "*",
+ "ConnectionStrings": {
+ "DefaultConnection": "Host=localhost;Port=5432;Database=reservationDB;Username=postgres;Password=postgres",
+ "Redis": "localhost:6379,password=redis,abortConnect=false"
+ },
+ "JwtSettings": {
+ "Key": "sjdasi872323232/-asd0adaskdia2nkcxozwlpp8761132192efmcoadsuoadnamaca76-034342432adas_109343432",
+ "Issuer": "https://my-restaurant.com",
+ "Audience": "https://localhost:7001",
+ "ExpiryInMinutes": 30,
+ "RefreshTokenExpirationInDays": 7
+ },
+ "Smtp": {
+ "Host": "localhost",
+ "Port": 1025
+ }
+}
diff --git a/restaurant-reservations-clean-architecture/src/RestaurantReservation.Application/Abstractions/IApplicationDbContext.cs b/restaurant-reservations-clean-architecture/src/RestaurantReservation.Application/Abstractions/IApplicationDbContext.cs
new file mode 100755
index 0000000..a3b39d6
--- /dev/null
+++ b/restaurant-reservations-clean-architecture/src/RestaurantReservation.Application/Abstractions/IApplicationDbContext.cs
@@ -0,0 +1,19 @@
+using Microsoft.EntityFrameworkCore;
+
+using RestaurantReservation.Domain.Reservations;
+using RestaurantReservation.Domain.Restaurants;
+using RestaurantReservation.Domain.Tables;
+using RestaurantReservation.Domain.Users;
+
+namespace RestaurantReservation.Application.Abstractions;
+
+public interface IApplicationDbContext
+{
+ DbSet Restaurants { get; }
+ DbSet Tables { get; }
+ DbSet TableGroups { get; }
+ DbSet Reservations { get; }
+ DbSet RefreshTokens { get; }
+ DbSet ReservationTables { get; }
+ Task SaveChangesAsync(CancellationToken cancellationToken = default);
+}
\ No newline at end of file
diff --git a/restaurant-reservations-clean-architecture/src/RestaurantReservation.Application/Abstractions/ICurrentUser.cs b/restaurant-reservations-clean-architecture/src/RestaurantReservation.Application/Abstractions/ICurrentUser.cs
new file mode 100755
index 0000000..d1ecdeb
--- /dev/null
+++ b/restaurant-reservations-clean-architecture/src/RestaurantReservation.Application/Abstractions/ICurrentUser.cs
@@ -0,0 +1,9 @@
+namespace RestaurantReservation.Application.Abstractions;
+
+public interface ICurrentUser
+{
+ string? UserId { get; }
+ string? Email { get; }
+ string? Name { get; }
+ bool IsAuthenticated { get; }
+}
\ No newline at end of file
diff --git a/restaurant-reservations-clean-architecture/src/RestaurantReservation.Application/Abstractions/IEmailService.cs b/restaurant-reservations-clean-architecture/src/RestaurantReservation.Application/Abstractions/IEmailService.cs
new file mode 100755
index 0000000..fa0a043
--- /dev/null
+++ b/restaurant-reservations-clean-architecture/src/RestaurantReservation.Application/Abstractions/IEmailService.cs
@@ -0,0 +1,8 @@
+using RestaurantReservation.Application.Features;
+
+namespace RestaurantReservation.Application.Abstractions;
+
+public interface IEmailService
+{
+ Task SendEmailAsync(EmailInfo emailInfo, CancellationToken cancellationToken = default);
+}
\ No newline at end of file
diff --git a/restaurant-reservations-clean-architecture/src/RestaurantReservation.Application/Abstractions/ISmtpClientFactory.cs b/restaurant-reservations-clean-architecture/src/RestaurantReservation.Application/Abstractions/ISmtpClientFactory.cs
new file mode 100755
index 0000000..86f8338
--- /dev/null
+++ b/restaurant-reservations-clean-architecture/src/RestaurantReservation.Application/Abstractions/ISmtpClientFactory.cs
@@ -0,0 +1,8 @@
+using MailKit.Net.Smtp;
+
+namespace RestaurantReservation.Application.Abstractions;
+
+public interface ISmtpClientFactory
+{
+ ISmtpClient Create();
+}
\ No newline at end of file
diff --git a/restaurant-reservations-clean-architecture/src/RestaurantReservation.Application/Abstractions/ITokenService.cs b/restaurant-reservations-clean-architecture/src/RestaurantReservation.Application/Abstractions/ITokenService.cs
new file mode 100755
index 0000000..51f5beb
--- /dev/null
+++ b/restaurant-reservations-clean-architecture/src/RestaurantReservation.Application/Abstractions/ITokenService.cs
@@ -0,0 +1,17 @@
+using RestaurantReservation.Application.Features.Auth.Command.Login;
+using RestaurantReservation.Domain.Common;
+using RestaurantReservation.Domain.Users;
+
+namespace RestaurantReservation.Application.Abstractions;
+
+public interface ITokenService
+{
+ Task> CreateAuthenticationTokensAsync(
+ ApplicationUser user,
+ IEnumerable roles,
+ CancellationToken cancellationToken = default);
+
+ Result CreateRefreshToken();
+
+ Task RevokeRefreshTokenAsync(string refreshToken, CancellationToken cancellationToken = default);
+}
\ No newline at end of file
diff --git a/restaurant-reservations-clean-architecture/src/RestaurantReservation.Application/Abstractions/SmtpOptions.cs b/restaurant-reservations-clean-architecture/src/RestaurantReservation.Application/Abstractions/SmtpOptions.cs
new file mode 100644
index 0000000..893f2e5
--- /dev/null
+++ b/restaurant-reservations-clean-architecture/src/RestaurantReservation.Application/Abstractions/SmtpOptions.cs
@@ -0,0 +1,7 @@
+namespace RestaurantReservation.Application.Abstractions;
+
+public class SmtpOptions
+{
+ public string Host { get; set; } = string.Empty;
+ public int Port { get; set; }
+}
\ No newline at end of file
diff --git a/restaurant-reservations-clean-architecture/src/RestaurantReservation.Application/Behaviors/ValidationBehavior.cs b/restaurant-reservations-clean-architecture/src/RestaurantReservation.Application/Behaviors/ValidationBehavior.cs
new file mode 100755
index 0000000..401801b
--- /dev/null
+++ b/restaurant-reservations-clean-architecture/src/RestaurantReservation.Application/Behaviors/ValidationBehavior.cs
@@ -0,0 +1,36 @@
+using FluentValidation;
+
+using MediatR;
+
+using ValidationException = FluentValidation.ValidationException;
+
+namespace RestaurantReservation.Application.Behaviors;
+
+public class ValidationBehavior(IEnumerable> validators)
+ : IPipelineBehavior where TRequest : class
+{
+ public async Task Handle(
+ TRequest request,
+ RequestHandlerDelegate next,
+ CancellationToken cancellationToken)
+ {
+ if (validators.Any())
+ {
+ var context = new ValidationContext(request);
+
+ var results = await Task.WhenAll(validators.Select(v => v.ValidateAsync(context, cancellationToken)));
+
+ var failures = results
+ .SelectMany(r => r.Errors)
+ .Where(f => f is not null)
+ .ToList();
+
+ if (failures.Count > 0)
+ {
+ throw new ValidationException(failures);
+ }
+ }
+
+ return await next(cancellationToken);
+ }
+}
\ No newline at end of file
diff --git a/restaurant-reservations-clean-architecture/src/RestaurantReservation.Application/DependencyInjection.cs b/restaurant-reservations-clean-architecture/src/RestaurantReservation.Application/DependencyInjection.cs
new file mode 100755
index 0000000..1fa8ec2
--- /dev/null
+++ b/restaurant-reservations-clean-architecture/src/RestaurantReservation.Application/DependencyInjection.cs
@@ -0,0 +1,23 @@
+using FluentValidation;
+
+using Microsoft.Extensions.DependencyInjection;
+
+using RestaurantReservation.Application.Behaviors;
+
+namespace RestaurantReservation.Application;
+
+public static class DependencyInjection
+{
+ public static IServiceCollection AddApplication(this IServiceCollection services)
+ {
+ services.AddValidatorsFromAssembly(typeof(DependencyInjection).Assembly, includeInternalTypes: true);
+
+ services.AddMediatR(config =>
+ {
+ config.RegisterServicesFromAssembly(typeof(DependencyInjection).Assembly);
+ config.AddOpenBehavior(typeof(ValidationBehavior<,>));
+ });
+
+ return services;
+ }
+}
\ No newline at end of file
diff --git a/restaurant-reservations-clean-architecture/src/RestaurantReservation.Application/Extensions/QueryableExtensions.cs b/restaurant-reservations-clean-architecture/src/RestaurantReservation.Application/Extensions/QueryableExtensions.cs
new file mode 100755
index 0000000..afff5be
--- /dev/null
+++ b/restaurant-reservations-clean-architecture/src/RestaurantReservation.Application/Extensions/QueryableExtensions.cs
@@ -0,0 +1,114 @@
+using System.Linq.Dynamic.Core;
+using System.Reflection;
+
+using RestaurantReservation.Domain.Reservations;
+
+namespace RestaurantReservation.Application.Extensions;
+
+public static class QueryableExtensions
+{
+ public static IQueryable ApplyPagination(
+ this IQueryable query,
+ int pageNumber,
+ int pageSize)
+ {
+ return query
+ .Skip((pageNumber - 1) * pageSize)
+ .Take(pageSize);
+ }
+
+ public static IQueryable ApplySort(this IQueryable query, string? sortBy) where T : class
+ {
+ if (string.IsNullOrWhiteSpace(sortBy)) return query;
+
+ var sortExpressions = new List();
+
+ foreach (var part in sortBy.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries))
+ {
+ var tokens = part.Split(' ', StringSplitOptions.RemoveEmptyEntries);
+ if (tokens.Length == 0) continue;
+
+ var propertyPath = tokens[0];
+
+ if (!IsValidPropertyPath(typeof(T), propertyPath)) continue;
+
+ var direction = tokens.Length > 1 && tokens[1].Equals("desc", StringComparison.OrdinalIgnoreCase)
+ ? "descending"
+ : "ascending";
+
+ sortExpressions.Add($"{propertyPath} {direction}");
+ }
+
+ return sortExpressions.Count > 0
+ ? query.OrderBy(string.Join(", ", sortExpressions))
+ : query;
+ }
+
+ public static IQueryable ApplySearch(this IQueryable query, string? search) where T : class
+ {
+ if (string.IsNullOrWhiteSpace(search)) return query;
+
+ var searchTrimmed = search.Trim();
+
+ var isDate = DateOnly.TryParse(searchTrimmed, out var parsedData);
+ var isInt = int.TryParse(searchTrimmed, out var parsedInt);
+ var isStatusEnum = Enum.TryParse(searchTrimmed, out var parsedStatusEnum);
+
+ var allProperties = typeof(T)
+ .GetProperties(BindingFlags.Public | BindingFlags.Instance);
+ var searchExpressions = new List();
+ var parameters = new List