mirror of
https://github.com/meysamhadeli/booking-microservices.git
synced 2026-04-11 02:20:20 +08:00
init
This commit is contained in:
parent
23c459a89b
commit
31dc05f580
26
.dockerignore
Normal file
26
.dockerignore
Normal file
@ -0,0 +1,26 @@
|
||||
**/.classpath
|
||||
**/.dockerignore
|
||||
**/.env
|
||||
**/.git
|
||||
**/.gitignore
|
||||
**/.project
|
||||
**/.settings
|
||||
**/.toolstarget
|
||||
**/.vs
|
||||
**/.vscode
|
||||
**/*.*proj.user
|
||||
**/*.dbmdl
|
||||
**/*.jfm
|
||||
**/azds.yaml
|
||||
**/bin/
|
||||
**/charts
|
||||
**/docker-compose*
|
||||
**/Dockerfile*
|
||||
**/node_modules
|
||||
**/npm-debug.log
|
||||
**/obj/
|
||||
**/.tye/
|
||||
**/secrets.dev.yaml
|
||||
**/values.dev.yaml
|
||||
LICENSE
|
||||
README.md
|
||||
424
.editorconfig
Normal file
424
.editorconfig
Normal file
@ -0,0 +1,424 @@
|
||||
[*]
|
||||
indent_style = space
|
||||
indent_size = 2
|
||||
end_of_line = lf
|
||||
charset = utf-8
|
||||
trim_trailing_whitespace = true
|
||||
insert_final_newline = true
|
||||
|
||||
# C# files
|
||||
[*.{cs, cshtml}]
|
||||
indent_size = 4
|
||||
# New line preferences
|
||||
csharp_new_line_before_open_brace = all
|
||||
csharp_new_line_before_else = true
|
||||
csharp_new_line_before_catch = true
|
||||
csharp_new_line_before_finally = true
|
||||
csharp_new_line_before_members_in_object_initializers = true
|
||||
csharp_new_line_before_members_in_anonymous_types = true
|
||||
csharp_new_line_within_query_expression_clauses = true
|
||||
|
||||
# Indentation preferences
|
||||
csharp_indent_block_contents = true
|
||||
csharp_indent_braces = false
|
||||
csharp_indent_case_contents = true
|
||||
csharp_indent_switch_labels = true
|
||||
csharp_indent_labels = one_less_than_current
|
||||
|
||||
# avoid this. unless absolutely necessary
|
||||
dotnet_style_qualification_for_field = false:suggestion
|
||||
dotnet_style_qualification_for_property = false:suggestion
|
||||
dotnet_style_qualification_for_method = false:suggestion
|
||||
dotnet_style_qualification_for_event = false:suggestion
|
||||
|
||||
# only use var when it's obvious what the variable type is
|
||||
# csharp_style_var_for_built_in_types = false:none
|
||||
# csharp_style_var_when_type_is_apparent = false:none
|
||||
# csharp_style_var_elsewhere = false:suggestion
|
||||
|
||||
# use language keywords instead of BCL types
|
||||
dotnet_style_predefined_type_for_locals_parameters_members = true:suggestion
|
||||
dotnet_style_predefined_type_for_member_access = true:suggestion
|
||||
|
||||
# name all constant fields using PascalCase
|
||||
dotnet_naming_rule.constant_fields_should_be_pascal_case.severity = suggestion
|
||||
dotnet_naming_rule.constant_fields_should_be_pascal_case.symbols = constant_fields
|
||||
dotnet_naming_rule.constant_fields_should_be_pascal_case.style = pascal_case_style
|
||||
|
||||
dotnet_naming_symbols.constant_fields.applicable_kinds = field
|
||||
dotnet_naming_symbols.constant_fields.required_modifiers = const
|
||||
|
||||
dotnet_naming_style.pascal_case_style.capitalization = pascal_case
|
||||
|
||||
dotnet_naming_symbols.static_fields.applicable_kinds = field
|
||||
dotnet_naming_symbols.static_fields.required_modifiers = static
|
||||
|
||||
dotnet_naming_style.static_prefix_style.required_prefix = s_
|
||||
dotnet_naming_style.static_prefix_style.capitalization = camel_case
|
||||
|
||||
# internal and private fields should be _camelCase
|
||||
dotnet_naming_rule.camel_case_for_private_internal_fields.severity = suggestion
|
||||
dotnet_naming_rule.camel_case_for_private_internal_fields.symbols = private_internal_fields
|
||||
dotnet_naming_rule.camel_case_for_private_internal_fields.style = camel_case_underscore_style
|
||||
|
||||
dotnet_naming_symbols.private_internal_fields.applicable_kinds = field
|
||||
dotnet_naming_symbols.private_internal_fields.applicable_accessibilities = private, internal
|
||||
|
||||
dotnet_naming_style.camel_case_underscore_style.required_prefix = _
|
||||
dotnet_naming_style.camel_case_underscore_style.capitalization = camel_case
|
||||
|
||||
# Code style defaults
|
||||
dotnet_sort_system_directives_first = true
|
||||
csharp_preserve_single_line_blocks = true
|
||||
csharp_preserve_single_line_statements = false
|
||||
|
||||
# Expression-level preferences
|
||||
dotnet_style_object_initializer = true:suggestion
|
||||
dotnet_style_collection_initializer = true:suggestion
|
||||
dotnet_style_explicit_tuple_names = true:suggestion
|
||||
dotnet_style_coalesce_expression = true:suggestion
|
||||
dotnet_style_null_propagation = true:suggestion
|
||||
|
||||
# Expression-bodied members
|
||||
csharp_style_expression_bodied_methods = false:none
|
||||
csharp_style_expression_bodied_constructors = false:none
|
||||
csharp_style_expression_bodied_operators = false:none
|
||||
csharp_style_expression_bodied_properties = true:none
|
||||
csharp_style_expression_bodied_indexers = true:none
|
||||
csharp_style_expression_bodied_accessors = true:none
|
||||
|
||||
# Pattern matching
|
||||
csharp_style_pattern_matching_over_is_with_cast_check = true:suggestion
|
||||
csharp_style_pattern_matching_over_as_with_null_check = true:suggestion
|
||||
csharp_style_inlined_variable_declaration = true:suggestion
|
||||
|
||||
# Null checking preferences
|
||||
csharp_style_throw_expression = true:suggestion
|
||||
csharp_style_conditional_delegate_call = true:suggestion
|
||||
|
||||
# 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 = do_not_ignore
|
||||
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
|
||||
|
||||
##################################################################################
|
||||
## https://docs.microsoft.com/en-us/dotnet/fundamentals/code-analysis/categories
|
||||
## Microsoft Rules
|
||||
##
|
||||
|
||||
## CA1305
|
||||
dotnet_diagnostic.CA1305.severity = None
|
||||
|
||||
##################################################################################
|
||||
## https://github.com/DotNetAnalyzers/StyleCopAnalyzers/tree/master/documentation
|
||||
## StyleCop.Analyzers
|
||||
##
|
||||
|
||||
# Using directive should appear within a namespace declaration
|
||||
dotnet_diagnostic.SA1200.severity = None
|
||||
|
||||
# Generic type parameter documentation should have text.
|
||||
dotnet_diagnostic.SA1622.severity = None
|
||||
|
||||
# XML comment analysis is disabled due to project configuration
|
||||
dotnet_diagnostic.SA0001.severity = None
|
||||
|
||||
# The file header is missing or not located at the top of the file
|
||||
dotnet_diagnostic.SA1633.severity = None
|
||||
|
||||
# Use string.Empty for empty strings
|
||||
dotnet_diagnostic.SA1122.severity = None
|
||||
|
||||
# Variable '_' should begin with lower-case letter
|
||||
dotnet_diagnostic.SA1312.severity = None
|
||||
|
||||
# Parameter '_' should begin with lower-case letter
|
||||
dotnet_diagnostic.SA1313.severity = None
|
||||
|
||||
# Elements should be documented
|
||||
dotnet_diagnostic.SA1600.severity = None
|
||||
|
||||
# Prefix local calls with this
|
||||
dotnet_diagnostic.SA1101.severity = None
|
||||
|
||||
# 'public' members should come before 'private' members
|
||||
dotnet_diagnostic.SA1202.severity = None
|
||||
|
||||
# Comments should contain text
|
||||
dotnet_diagnostic.SA1120.severity = None
|
||||
|
||||
# Constant fields should appear before non-constant fields
|
||||
dotnet_diagnostic.SA1203.severity = None
|
||||
|
||||
# Field '_blah' should not begin with an underscore
|
||||
dotnet_diagnostic.SA1309.severity = None
|
||||
|
||||
# Use trailing comma in multi-line initializers
|
||||
dotnet_diagnostic.SA1413.severity = None
|
||||
|
||||
# A method should not follow a class
|
||||
dotnet_diagnostic.SA1201.severity = None
|
||||
|
||||
# Elements should be separated by blank line
|
||||
dotnet_diagnostic.SA1516.severity = None
|
||||
|
||||
# The parameter spans multiple lines
|
||||
dotnet_diagnostic.SA1118.severity = None
|
||||
|
||||
# Static members should appear before non-static members
|
||||
dotnet_diagnostic.SA1204.severity = None
|
||||
|
||||
# Put constructor initializers on their own line
|
||||
dotnet_diagnostic.SA1128.severity = None
|
||||
|
||||
# Opening braces should not be preceded by blank line
|
||||
dotnet_diagnostic.SA1509.severity = None
|
||||
|
||||
# The parameter should begin on the line after the previous parameter
|
||||
dotnet_diagnostic.SA1115.severity = None
|
||||
|
||||
# File name should match first type name
|
||||
dotnet_diagnostic.SA1649.severity = None
|
||||
|
||||
# File may only contain a single type
|
||||
dotnet_diagnostic.SA1402.severity = None
|
||||
|
||||
# Enumeration items should be documented
|
||||
dotnet_diagnostic.SA1602.severity = None
|
||||
|
||||
# Element should not be on a single line
|
||||
dotnet_diagnostic.SA1502.severity = None
|
||||
|
||||
# Closing parenthesis should not be preceded by a space
|
||||
dotnet_diagnostic.SA1009.severity = None
|
||||
|
||||
# Closing parenthesis should be on line of last parameter
|
||||
dotnet_diagnostic.SA1111.severity = None
|
||||
|
||||
# Braces should not be ommitted
|
||||
dotnet_diagnostic.SA1503.severity = None
|
||||
|
||||
dotnet_diagnostic.SA1401.severity = None
|
||||
|
||||
# The parameters to a C# method or indexer call or declaration are not all on the same line or each on a separate line.
|
||||
# dotnet_diagnostic.SA1117.severity = Suggestion
|
||||
|
||||
# The parameters to a C# method or indexer call or declaration span across multiple lines, but the first parameter does not start on the line after the opening bracket.
|
||||
dotnet_diagnostic.SA1116.severity = None
|
||||
|
||||
# A C# partial element is missing a documentation header.
|
||||
dotnet_diagnostic.SA1601.severity = None
|
||||
|
||||
# A C# element is missing documentation for its return value.
|
||||
dotnet_diagnostic.SA1615.severity = None
|
||||
|
||||
##################################################################################
|
||||
##
|
||||
## SonarAnalyzers.CSharp
|
||||
##
|
||||
|
||||
# Update this method so that its implementation is not identical to 'blah'
|
||||
dotnet_diagnostic.S4144.severity = None
|
||||
|
||||
# Update this implementation of 'ISerializable' to conform to the recommended serialization pattern
|
||||
dotnet_diagnostic.S3925.severity = None
|
||||
|
||||
# Rename class 'IOCActivator' to match pascal case naming rules, consider using 'IocActivator'
|
||||
dotnet_diagnostic.S101.severity = None
|
||||
|
||||
# Extract this nested code block into a separate method
|
||||
dotnet_diagnostic.S1199.severity = None
|
||||
|
||||
# Remove unassigned auto-property 'Blah', or set its value
|
||||
dotnet_diagnostic.S3459.severity = None
|
||||
|
||||
# Remove the unused private set accessor in property 'Version'
|
||||
dotnet_diagnostic.S1144.severity = None
|
||||
|
||||
# Remove this commented out code
|
||||
dotnet_diagnostic.S125.severity = None
|
||||
|
||||
# 'System.Exception' should not be thrown by user code
|
||||
dotnet_diagnostic.S112.severity = None
|
||||
|
||||
dotnet_diagnostic.S3903.severity = None
|
||||
|
||||
##################################################################################
|
||||
## https://github.com/meziantou/Meziantou.Analyzer/tree/main/docs
|
||||
## Meziantou.Analyzer
|
||||
##
|
||||
|
||||
#
|
||||
# MA0004: Use Task.ConfigureAwait(false)
|
||||
dotnet_diagnostic.MA0004.severity = Suggestion
|
||||
|
||||
# MA0049: Type name should not match containing namespace
|
||||
dotnet_diagnostic.MA0049.severity = Suggestion
|
||||
|
||||
# MA0048: File name must match type name
|
||||
dotnet_diagnostic.MA0048.severity = Suggestion
|
||||
|
||||
# MA0051: Method is too long
|
||||
dotnet_diagnostic.MA0051.severity = Suggestion
|
||||
|
||||
# https://www.meziantou.net/string-comparisons-are-harder-than-it-seems.htm
|
||||
# MA0006 - Use String.Equals instead of equality operator
|
||||
dotnet_diagnostic.MA0006.severity = Suggestion
|
||||
|
||||
# MA0002 - IEqualityComparer<string> or IComparer<string> is missing
|
||||
dotnet_diagnostic.MA0002.severity = Suggestion
|
||||
|
||||
# MA0001 - StringComparison is missing
|
||||
dotnet_diagnostic.MA0001.severity = Suggestion
|
||||
|
||||
# https://cezarypiatek.github.io/post/async-analyzers-p2/#13-pass-cancellation-token
|
||||
# MA0040: Specify a cancellation token
|
||||
dotnet_diagnostic.MA0032.severity = Suggestion
|
||||
|
||||
# https://cezarypiatek.github.io/post/async-analyzers-p2/#13-pass-cancellation-token
|
||||
# MA0040: Flow the cancellation token when available
|
||||
dotnet_diagnostic.MA0040.severity = Suggestion
|
||||
|
||||
# https://cezarypiatek.github.io/post/async-analyzers-p2/#14-using-cancellation-token-with-iasyncenumerable
|
||||
# MA0079: Use a cancellation token using .WithCancellation()
|
||||
dotnet_diagnostic.MA0079.severity = Suggestion
|
||||
|
||||
# https://cezarypiatek.github.io/post/async-analyzers-p2/#14-using-cancellation-token-with-iasyncenumerable
|
||||
# MA0080: Use a cancellation token using .WithCancellation()
|
||||
dotnet_diagnostic.MA0080.severity = Suggestion
|
||||
|
||||
##################################################################################
|
||||
## http://pihrt.net/Roslynator/Analyzers
|
||||
## http://pihrt.net/Roslynator/Refactorings
|
||||
## Roslynator
|
||||
##
|
||||
|
||||
# RCS1036 - Remove redundant empty line.
|
||||
dotnet_diagnostic.RCS1036.severity = None
|
||||
|
||||
# RCS1037 - Remove trailing white-space.
|
||||
dotnet_diagnostic.RCS1037.severity = None
|
||||
|
||||
# RCS1194: Implement exception constructors
|
||||
dotnet_diagnostic.RCS1194.severity = None
|
||||
|
||||
# https://cezarypiatek.github.io/post/async-analyzers-p1/#1-redundant-asyncawait
|
||||
# RCS1174: Remove redundant async/await.
|
||||
dotnet_diagnostic.RCS1174.severity = error
|
||||
|
||||
# https://cezarypiatek.github.io/post/async-analyzers-p2/#10-returning-null-from-a-task-returning-method
|
||||
# RCS1210: Return Task.FromResult instead of returning null.
|
||||
dotnet_diagnostic.RCS1210.severity = error
|
||||
|
||||
# https://cezarypiatek.github.io/post/async-analyzers-p2/#9-missing-configureawaitbool
|
||||
# RCS1090: Call 'ConfigureAwait(false)'.
|
||||
dotnet_diagnostic.RCS1090.severity = Suggestion
|
||||
|
||||
# https://cezarypiatek.github.io/post/async-analyzers-p2/#11-asynchronous-method-names-should-end-with-async
|
||||
#RCS1046: Asynchronous method name should end with 'Async'.
|
||||
dotnet_diagnostic.RCS1046.severity = Suggestion
|
||||
|
||||
# https://cezarypiatek.github.io/post/async-analyzers-p2/#12-non-asynchronous-method-names-shouldnt-end-with-async
|
||||
# RCS1047: Non-asynchronous method name should not end with 'Async'.
|
||||
dotnet_diagnostic.RCS1047.severity = error
|
||||
|
||||
##################################################################################
|
||||
## https://github.com/semihokur/asyncfixer
|
||||
## AsyncFixer01
|
||||
##
|
||||
|
||||
# https://cezarypiatek.github.io/post/async-analyzers-p1/#1-redundant-asyncawait
|
||||
# AsyncFixer01: Unnecessary async/await usage
|
||||
dotnet_diagnostic.AsyncFixer01.severity = Suggestion
|
||||
|
||||
# https://cezarypiatek.github.io/post/async-analyzers-p1/#2-calling-synchronous-method-inside-the-async-method
|
||||
# AsyncFixer02: Long-running or blocking operations inside an async method
|
||||
dotnet_diagnostic.AsyncFixer02.severity = error
|
||||
|
||||
# https://cezarypiatek.github.io/post/async-analyzers-p1/#3-async-void-method
|
||||
# AsyncFixer03: Fire & forget async void methods
|
||||
dotnet_diagnostic.AsyncFixer03.severity = error
|
||||
|
||||
# https://cezarypiatek.github.io/post/async-analyzers-p1/#6-not-awaited-task-inside-the-using-block
|
||||
# AsyncFixer04: Fire & forget async call inside a using block
|
||||
dotnet_diagnostic.AsyncFixer04.severity = error
|
||||
|
||||
|
||||
##################################################################################
|
||||
## https://github.com/microsoft/vs-threading
|
||||
## Microsoft.VisualStudio.Threading.Analyzers
|
||||
##
|
||||
|
||||
# https://cezarypiatek.github.io/post/async-analyzers-p1/#2-calling-synchronous-method-inside-the-async-method
|
||||
# VSTHRD103: Call async methods when in an async method
|
||||
dotnet_diagnostic.VSTHRD103.severity = Suggestion
|
||||
|
||||
|
||||
# https://cezarypiatek.github.io/post/async-analyzers-p1/#3-async-void-method
|
||||
# VSTHRD100: Avoid async void methods
|
||||
dotnet_diagnostic.VSTHRD100.severity = error
|
||||
|
||||
# https://cezarypiatek.github.io/post/async-analyzers-p1/#4-unsupported-async-delegates
|
||||
# VSTHRD101: Avoid unsupported async delegates
|
||||
dotnet_diagnostic.VSTHRD101.severity = error
|
||||
|
||||
# https://cezarypiatek.github.io/post/async-analyzers-p1/#5-not-awaited-task-within-using-expression
|
||||
# VSTHRD107: Await Task within using expression
|
||||
dotnet_diagnostic.VSTHRD107.severity = error
|
||||
|
||||
# https://cezarypiatek.github.io/post/async-analyzers-p1/#7-unobserved-result-of-asynchronous-method
|
||||
# VSTHRD110: Observe result of async calls
|
||||
dotnet_diagnostic.VSTHRD110.severity = error
|
||||
|
||||
# https://cezarypiatek.github.io/post/async-analyzers-p2/#8-synchronous-waits
|
||||
# VSTHRD002: Avoid problematic synchronous waits
|
||||
dotnet_diagnostic.VSTHRD002.severity = Suggestion
|
||||
|
||||
# https://cezarypiatek.github.io/post/async-analyzers-p2/#9-missing-configureawaitbool
|
||||
# VSTHRD111: Use ConfigureAwait(bool)
|
||||
dotnet_diagnostic.VSTHRD111.severity = Suggestion
|
||||
|
||||
# https://cezarypiatek.github.io/post/async-analyzers-p2/#10-returning-null-from-a-task-returning-method
|
||||
# VSTHRD114: Avoid returning a null Task
|
||||
dotnet_diagnostic.VSTHRD114.severity = error
|
||||
|
||||
# https://cezarypiatek.github.io/post/async-analyzers-p2/#11-asynchronous-method-names-should-end-with-async
|
||||
# VSTHRD200: Use "Async" suffix for async methods
|
||||
dotnet_diagnostic.VSTHRD200.severity = Suggestion
|
||||
|
||||
# https://cezarypiatek.github.io/post/async-analyzers-p2/#12-non-asynchronous-method-names-shouldnt-end-with-async
|
||||
# VSTHRD200: Use "Async" suffix for async methods
|
||||
dotnet_diagnostic.VSTHRD200.severity = Suggestion
|
||||
|
||||
|
||||
##################################################################################
|
||||
## https://github.com/hvanbakel/Asyncify-CSharp
|
||||
## Asyncify
|
||||
##
|
||||
|
||||
# https://cezarypiatek.github.io/post/async-analyzers-p2/#8-synchronous-waits
|
||||
# AsyncifyInvocation: Use Task Async
|
||||
dotnet_diagnostic.AsyncifyInvocation.severity = error
|
||||
|
||||
# https://cezarypiatek.github.io/post/async-analyzers-p2/#8-synchronous-waits
|
||||
# AsyncifyVariable: Use Task Async
|
||||
dotnet_diagnostic.AsyncifyVariable.severity = error
|
||||
85
.gitignore
vendored
85
.gitignore
vendored
@ -348,3 +348,88 @@ MigrationBackup/
|
||||
|
||||
# Ionide (cross platform F# VS Code tools) working folder
|
||||
.ionide/
|
||||
|
||||
# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider
|
||||
# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
|
||||
|
||||
# User-specific stuff
|
||||
.idea/**/workspace.xml
|
||||
.idea/**/tasks.xml
|
||||
.idea/**/usage.statistics.xml
|
||||
.idea/**/dictionaries
|
||||
.idea/**/shelf
|
||||
|
||||
# AWS User-specific
|
||||
.idea/**/aws.xml
|
||||
|
||||
# Generated files
|
||||
.idea/**/contentModel.xml
|
||||
|
||||
# Sensitive or high-churn files
|
||||
.idea/**/dataSources/
|
||||
.idea/**/dataSources.ids
|
||||
.idea/**/dataSources.local.xml
|
||||
.idea/**/sqlDataSources.xml
|
||||
.idea/**/dynamic.xml
|
||||
.idea/**/uiDesigner.xml
|
||||
.idea/**/dbnavigator.xml
|
||||
|
||||
# Gradle
|
||||
.idea/**/gradle.xml
|
||||
.idea/**/libraries
|
||||
|
||||
# Gradle and Maven with auto-import
|
||||
# When using Gradle or Maven with auto-import, you should exclude module files,
|
||||
# since they will be recreated, and may cause churn. Uncomment if using
|
||||
# auto-import.
|
||||
# .idea/artifacts
|
||||
# .idea/compiler.xml
|
||||
# .idea/jarRepositories.xml
|
||||
# .idea/modules.xml
|
||||
# .idea/*.iml
|
||||
# .idea/modules
|
||||
# *.iml
|
||||
# *.ipr
|
||||
|
||||
# CMake
|
||||
cmake-build-*/
|
||||
|
||||
# Mongo Explorer plugin
|
||||
.idea/**/mongoSettings.xml
|
||||
|
||||
# File-based project format
|
||||
*.iws
|
||||
|
||||
# IntelliJ
|
||||
out/
|
||||
|
||||
# mpeltonen/sbt-idea plugin
|
||||
.idea_modules/
|
||||
|
||||
# JIRA plugin
|
||||
atlassian-ide-plugin.xml
|
||||
|
||||
# Cursive Clojure plugin
|
||||
.idea/replstate.xml
|
||||
|
||||
# SonarLint plugin
|
||||
.idea/sonarlint/
|
||||
|
||||
# Crashlytics plugin (for Android Studio and IntelliJ)
|
||||
com_crashlytics_export_strings.xml
|
||||
crashlytics.properties
|
||||
crashlytics-build.properties
|
||||
fabric.properties
|
||||
|
||||
# Editor-based Rest Client
|
||||
.idea/httpRequests
|
||||
|
||||
# Android studio 3.1+ serialized cache file
|
||||
.idea/caches/build_file_checksums.ser
|
||||
|
||||
# JetBrains Rider
|
||||
.idea/
|
||||
*.sln.iml
|
||||
|
||||
# Tye
|
||||
.tye/
|
||||
64
Directory.Build.props
Normal file
64
Directory.Build.props
Normal file
@ -0,0 +1,64 @@
|
||||
<Project>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Meziantou.Analyzer" Version="1.0.688" Condition="$(MSBuildProjectExtension) == '.csproj'" PrivateAssets="all">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference
|
||||
Include="StyleCop.Analyzers"
|
||||
Version="1.2.0-beta.354"
|
||||
PrivateAssets="all"
|
||||
Condition="$(MSBuildProjectExtension) == '.csproj'"
|
||||
/>
|
||||
<PackageReference
|
||||
Include="SonarAnalyzer.CSharp"
|
||||
Version="8.29.0.36737"
|
||||
PrivateAssets="all"
|
||||
Condition="$(MSBuildProjectExtension) == '.csproj'"
|
||||
/>
|
||||
|
||||
<PackageReference
|
||||
Include="Roslynator.Analyzers"
|
||||
Version="3.3.0"
|
||||
PrivateAssets="all"
|
||||
Condition="$(MSBuildProjectExtension) == '.csproj'"
|
||||
/>
|
||||
<PackageReference
|
||||
Include="Roslynator.CodeAnalysis.Analyzers"
|
||||
Version="3.3.0"
|
||||
PrivateAssets="all"
|
||||
Condition="$(MSBuildProjectExtension) == '.csproj'"
|
||||
/>
|
||||
<PackageReference
|
||||
Include="Roslynator.Formatting.Analyzers"
|
||||
Version="3.3.0"
|
||||
PrivateAssets="all"
|
||||
Condition="$(MSBuildProjectExtension) == '.csproj'"
|
||||
/>
|
||||
<PackageReference
|
||||
Include="Microsoft.VisualStudio.Threading.Analyzers"
|
||||
Version="17.0.64"
|
||||
PrivateAssets="all"
|
||||
Condition="$(MSBuildProjectExtension) == '.csproj'"
|
||||
/>
|
||||
<PackageReference
|
||||
Include="AsyncFixer"
|
||||
Version="1.5.1"
|
||||
PrivateAssets="all"
|
||||
Condition="$(MSBuildProjectExtension) == '.csproj'"
|
||||
/>
|
||||
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="6.0.1">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="6.0.1">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
162
README.md
Normal file
162
README.md
Normal file
@ -0,0 +1,162 @@
|
||||
[](https://opensource.org/licenses/MIT)
|
||||
# Booking-Microservices-Sample
|
||||
|
||||
Booking Microservices is a Sample application for booking ticket. This application based on different software architecture and technologies like .Net Core, CQRS, DDD, Vertical Slice Architecture, Docker, kubernetes, tye, masstransit, RabbitMQ, Grpc, yarp reverse proxy, Identity Server, Redis, SqlServer, Entity Framework Core, Event Sourcing and different level of testing.
|
||||
|
||||
|
||||
🌀 Keep in mind this repository is work in progress and will be complete over time 🚀
|
||||
|
||||
# Table of Contents
|
||||
|
||||
- [The Goals of This Project](#the-goals-of-this-project)
|
||||
- [Plan](#plan)
|
||||
- [Technologies - Libraries](#technologies---libraries)
|
||||
- [The Domain and Bounded Context - Service Boundary](#the-domain-and-bounded-context---service-boundary)
|
||||
- [Structure of Project](#structure-of-project)
|
||||
- [Prerequisites](#prerequisites)
|
||||
- [How to Run](#how-to-run)
|
||||
- [Docker Compose](#docker-compose)
|
||||
- [Kubernetes](#kubernetes)
|
||||
- [Support](#support)
|
||||
- [Contribution](#contribution)
|
||||
|
||||
## The Goals of This Project
|
||||
|
||||
- The microservices base on `Domain Driven Design (DDD)` implementation.
|
||||
- Correct `separation of bounded contexts` for each microservice.
|
||||
- Communications between bounded contexts through asynchronous `MessageBus` and `events`.
|
||||
- Simple `CQRS` implementation and event driven architecture.
|
||||
- Using `Best Practice` and `New Technologies` and `Design Patterns`.
|
||||
- Using `Docker-Compose` and `Kubernetes` for our deployment mechanism.
|
||||
- Implementing various type of testing like `Unit Testing`, `Integration Testing`.
|
||||
|
||||
## Plan
|
||||
> This project is in progress, New features will be added over time.
|
||||
|
||||
I will try to register some [Issues](https://github.com/meysamhadeli/booking-microservices-sample/issues) for my `TODO` works, just to not forget and also for tracking my works in future.
|
||||
|
||||
High-level plan is represented in the table
|
||||
|
||||
| Feature | Status |
|
||||
| ------- | ------ |
|
||||
| API Gateway | Completed ✔️ |
|
||||
| Identity Service | Completed ✔️ |
|
||||
| Flight Service | Completed ✔️ |
|
||||
| Passenger Service | Completed ✔️ |
|
||||
| Booking Service | Completed ✔️ |
|
||||
| Building Blocks | In Progress 👷♂️ |
|
||||
|
||||
|
||||
## Technologies - Libraries
|
||||
- ✔️ **[`.NET 6`](https://dotnet.microsoft.com/download)** - .NET Framework and .NET Core, including ASP.NET and ASP.NET Core
|
||||
- ✔️ **[`MVC Versioning API`](https://github.com/microsoft/aspnet-api-versioning)** - Set of libraries which add service API versioning to ASP.NET Web API, OData with ASP.NET Web API, and ASP.NET Core
|
||||
- ✔️ **[`EF Core`](https://github.com/dotnet/efcore)** - Modern object-database mapper for .NET. It supports LINQ queries, change tracking, updates, and schema migrations
|
||||
- ✔️ **[`Masstransit`](https://github.com/MassTransit/MassTransit)** - Distributed Application Framework for .NET.
|
||||
- ✔️ **[`MediatR`]()** - Simple, unambitious mediator implementation in .NET.
|
||||
- ✔️ **[`FluentValidation`](https://github.com/FluentValidation/FluentValidation)** - Popular .NET validation library for building strongly-typed validation rules
|
||||
- ✔️ **[`Swagger & Swagger UI`](https://github.com/domaindrivendev/Swashbuckle.AspNetCore)** - Swagger tools for documenting API's built on ASP.NET Core
|
||||
- ✔️ **[`Serilog`](https://github.com/serilog/serilog)** - Simple .NET logging with fully-structured events
|
||||
- ✔️ **[`Polly`](https://github.com/App-vNext/Polly)** - Polly is a .NET resilience and transient-fault-handling library that allows developers to express policies such as Retry, Circuit Breaker, Timeout, Bulkhead Isolation, and Fallback in a fluent and thread-safe manner
|
||||
- ✔️ **[`Scrutor`](https://github.com/khellang/Scrutor)** - Assembly scanning and decoration extensions for Microsoft.Extensions.DependencyInjection
|
||||
- ✔️ **[`Opentelemetry-dotnet`](https://github.com/open-telemetry/opentelemetry-dotnet)** - The OpenTelemetry .NET Client
|
||||
- ✔️ **[`DuendeSoftware IdentityServer`](https://github.com/DuendeSoftware/IdentityServer)** - The most flexible and standards-compliant OpenID Connect and OAuth 2.x framework for ASP.NET Core
|
||||
- ✔️ **[`EasyCaching`](https://github.com/dotnetcore/EasyCaching)** - Open source caching library that contains basic usages and some advanced usages of caching which can help us to handle caching more easier.
|
||||
- ✔️ **[`Mapster`](https://github.com/MapsterMapper/Mapster)** - Convention-based object-object mapper in .NET.
|
||||
- ✔️ **[`Hellang.Middleware.ProblemDetails`](https://github.com/khellang/Middleware/tree/master/src/ProblemDetails)** - A middleware for handling exception in .Net Core
|
||||
- ✔️ **[`IdGen`](https://github.com/RobThree/IdGen)** - Twitter Snowflake-alike ID generator for .Net
|
||||
- ✔️ **[`Yarp`](https://github.com/microsoft/reverse-proxy)** - Reverse proxy toolkit for building fast proxy servers in .NET
|
||||
- ✔️ **[`Tye`](https://github.com/dotnet/tye)** - Developer tool that makes developing, testing, and deploying microservices and distributed applications easier
|
||||
- ✔️ **[`MagicOnion`](https://github.com/Cysharp/MagicOnion)** - gRPC based HTTP/2 RPC Streaming Framework for .NET, .NET Core and Unity.
|
||||
- ✔️ **[`EventStore`](https://github.com/EventStore/EventStore)** - The open-source, functional database with Complex Event Processing.
|
||||
- ✔️ **[`MongoDB.Driver`](https://github.com/mongodb/mongo-csharp-driver)** - .NET Driver for MongoDB.
|
||||
|
||||
## The Domain And Bounded Context - Service Boundary
|
||||
|
||||
- `Identity Service`: The Identity Service is a bounded context for authenticate and authorize users through with [Identity Server](https://github.com/DuendeSoftware/IdentityServer). Also, this service is responsible for creating users and their corresponding roles and permission with using [.Net Core Identity](https://docs.microsoft.com/en-us/aspnet/core/security/authentication/identity) and Jwt authentication and authorization.
|
||||
|
||||
- `Flight Service`: The Flight Service is a bounded context for all operation related to flight and get available filght and seat.
|
||||
|
||||
- `Passenger Service`: The Passenger Service is a bounded context for managing our passengers information, track the activities and subscribing to get notification for out of stock products
|
||||
|
||||
- `Booking Service`: The Booking Service is a bounded context for managing all operation related to booking ticket.
|
||||
|
||||
|
||||
## Structure of Project
|
||||
|
||||
I used `yarp` reverse proxy for routes synchronous and asynchronous request to the corresponding microservice. and each microservices has own business and dependencies such as databases, files and etc. and each microservices is decuple from other microservices and develop and deploy separately. and these microservices talk to each other with synchronous call like Rest or gRpc and use RabbitMq or Kafka for asynchronous call.
|
||||
|
||||
We have separate microservice ([IdentityServer](https://github.com/DuendeSoftware/IdentityServer)) for authentication and authorization and each request go to API Gateway and then route to Identity microservices and after authentication and authorization back API Gateway and then route to expected microservices.
|
||||
|
||||
Also here I used `RabbitMQ` as my MessageBroker for async communication between the microservices with using eventually consistency mechanism. and top of that I use `MassTransit` provides many requirements in microservice projects such as messaging, availability, reliability and etc.
|
||||
|
||||
Microservices are `event based` which means they can publish and/or subscribe to any events occurring in the setup. By using this approach for communicating between services, each microservice does not need to know about the other services or handle errors occurred in other microservices.
|
||||
|
||||
Also I used a [mediator pattern](https://dotnetcoretutorials.com/2019/04/30/the-mediator-pattern-in-net-core-part-1-whats-a-mediator/) with using [MediatR](https://github.com/jbogard/MediatR) library in my controllers for a clean and [thin controller](https://codeopinion.com/thin-controllers-cqrs-mediatr/), also instead of using a `application service` class because after some times our controller will depends to different services and this breaks single responsibility principle. We use mediator pattern to manage the delivery of messages to handlers. One of the advantages behind the [mediator pattern](https://lostechies.com/jimmybogard/2014/09/09/tackling-cross-cutting-concerns-with-a-mediator-pipeline/) is that it allows the application code to define a pipeline of activities for requests . For example in our controllers we create a command and send it to mediator and mediator will route our command to a specific command handler in application layer.
|
||||
|
||||
To support [Single Responsibility Principle](https://en.wikipedia.org/wiki/Single_responsibility_principle) and [Don't Repeat Yourself principles](https://en.wikipedia.org/wiki/Don%27t_repeat_yourself), the implementation of cross-cutting concerns is done using the mediatr [pipeline behaviors](https://github.com/jbogard/MediatR/wiki/Behaviors) or creating a [mediatr decorators](https://lostechies.com/jimmybogard/2014/09/09/tackling-cross-cutting-concerns-with-a-mediator-pipeline/).
|
||||
|
||||
Also in this project I used mix of [clean architecture](https://jasontaylor.dev/clean-architecture-getting-started/) and [vertical slice architecture](https://jimmybogard.com/vertical-slice-architecture/) and also I used [feature folder structure](http://www.kamilgrzybek.com/design/feature-folders/) in this project.
|
||||
|
||||
Also here I used cqrs for decompose my features to very small parts that make our application
|
||||
|
||||
- maximize performance, scalability and simplicity.
|
||||
- adding new feature to this mechanism is very easy without any breaking change in other part of our codes. New features only add code, we're not changing shared code and worrying about side effects.
|
||||
- easy to maintain and any changes only affect on one command or query and avoid any breaking changes on other parts
|
||||
- it gives us better separation of concerns and cross cutting concern (with help of mediatr behavior pipelines) in our code instead of a big service class for doing a lot of things.
|
||||
|
||||
I treat each request as a distinct use case or slice, encapsulating and grouping all concerns from front-end to back.
|
||||
When adding or changing a feature in an application in n-tire architecture, we are typically touching many different "layers" in an application. we are changing the user interface, adding fields to models, modifying validation, and so on. Instead of coupling across a layer, we couple vertically along a slice. we `Minimize coupling` `between slices`, and `maximize coupling` `in a slice`.
|
||||
|
||||
With this approach, each of our vertical slices can decide for itself how to best fulfill the request. New features only add code, we're not changing shared code and worrying about side effects.
|
||||
|
||||

|
||||
|
||||
With using CQRS pattern, we cut each business functionality into some vertical slices, and inner each of this slices we have [technical folders structure](http://www.kamilgrzybek.com/design/feature-folders) specific to that feature (command, handlers, infrastructure, repository, controllers, ...). In Our CQRS pattern each command/query handler is a separate slice. This is where you can reduce coupling between layers. Each handler can be a separated code unit, even copy/pasted. Thanks to that, we can tune down the specific method to not follow general conventions (e.g. use custom SQL query or even different storage). In a traditional layered architecture, when we change the core generic mechanism in one layer, it can impact all methods.
|
||||
|
||||
|
||||
## How to Run
|
||||
|
||||
### Config Certificate
|
||||
|
||||
Runt the following commands for [Config SSL](microsoft.com/en-us/aspnet/core/security/docker-compose-https?view=aspnetcore-6.0) in your system
|
||||
``` bash
|
||||
dotnet dev-certs https -ep %USERPROFILE%\.aspnet\https\aspnetapp.pfx -p {password here}
|
||||
dotnet dev-certs https --trust
|
||||
```
|
||||
>Note: for running this command in `powershell` use `$env:USERPROFILE` instead of `%USERPROFILE%`
|
||||
|
||||
### Docker Compose
|
||||
|
||||
Run this app on docker with this [docker-compose.yaml](./deployments/docker-compose/docker-compose.yaml) file with bellow command in root of application:
|
||||
|
||||
``` bash
|
||||
docker-compose -f ./deployments/docker-compose/docker-compose.yaml up -d
|
||||
```
|
||||
|
||||
### Documention Apis
|
||||
|
||||
For testing apis I used [REST Client](https://github.com/Huachao/vscode-restclient) plugin of VSCode and this file [booking.rest](./booking.rest) is in root of project.
|
||||
Also after running api you have access to swagger open api for all microservices in /swagger route path.
|
||||
|
||||
# Support
|
||||
If you like my work, feel free to:
|
||||
|
||||
- ⭐ this repository. And we will be happy together :)
|
||||
|
||||
|
||||
Thanks a bunch for supporting me!
|
||||
|
||||
## Contribution
|
||||
|
||||
Contributions are always welcome! Please take a look at the [contribution guidelines](https://github.com/meysamhadeli/booking-microservices-sample/blob/master/contributing.md) pages first.
|
||||
|
||||
Thanks to all [contributors](https://github.com/meysamhadeli/booking-microservices-sample/graphs/contributors), you're awesome and wouldn't be possible without you! The goal is to build a categorized community-driven collection of very well-known resources.
|
||||
|
||||
## Project Refrences & Credits
|
||||
- [https://github.com/jbogard/ContosoUniversityDotNetCore-Pages](https://github.com/jbogard/ContosoUniversityDotNetCore-Pages)
|
||||
- [https://github.com/kgrzybek/modular-monolith-with-ddd](https://github.com/kgrzybek/modular-monolith-with-ddd)
|
||||
- [https://github.com/oskardudycz/EventSourcing.NetCore](https://github.com/oskardudycz/EventSourcing.NetCore)
|
||||
- [https://github.com/thangchung/clean-architecture-dotnet](https://github.com/thangchung/clean-architecture-dotnet)
|
||||
- [https://github.com/jasontaylordev/CleanArchitecture](https://github.com/jasontaylordev/CleanArchitecture)
|
||||
- [https://github.com/pdevito3/MessageBusTestingInMemHarness](https://github.com/pdevito3/MessageBusTestingInMemHarness)
|
||||
- [https://github.com/devmentors/FeedR](https://github.com/devmentors/FeedR)
|
||||
BIN
assets/Vertical-Slice-Architecture.jpg
Normal file
BIN
assets/Vertical-Slice-Architecture.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 30 KiB |
140
booking-microservices-sample.sln
Normal file
140
booking-microservices-sample.sln
Normal file
@ -0,0 +1,140 @@
|
||||
|
||||
Microsoft Visual Studio Solution File, Format Version 12.00
|
||||
# Visual Studio Version 16
|
||||
VisualStudioVersion = 16.0.30114.105
|
||||
MinimumVisualStudioVersion = 10.0.40219.1
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "BuildingBlocks", "BuildingBlocks", "{5B69EDFD-4B09-457A-AAAF-D816D402D595}"
|
||||
EndProject
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Services", "Services", "{9010E0B5-9C42-4256-ADE4-E290434F2CEF}"
|
||||
EndProject
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "ApiGateway", "ApiGateway", "{3E38DD17-9EEE-4815-9D5B-BEB5549020A0}"
|
||||
EndProject
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{776BDF43-0DEA-44A3-AF72-99408CE544EE}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ApiGateway", "src\ApiGateway\src\ApiGateway.csproj", "{A2D7C5C4-5148-4C3E-BB12-B7A197A290F5}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BuildingBlocks", "src\BuildingBlocks\BuildingBlocks.csproj", "{E42BB533-4144-4D78-BCCE-50BA00BCADBE}"
|
||||
EndProject
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Flight", "Flight", "{5F0996AB-F8DB-4240-BD4A-DFDD70638A73}"
|
||||
EndProject
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Passenger", "Passenger", "{1A2ABCD9-493B-4848-9C69-919CDBCA61F3}"
|
||||
EndProject
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Booking", "Booking", "{22447274-717D-4321-87F3-868BAF93CBEC}"
|
||||
EndProject
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Identity", "Identity", "{55BE6759-95AA-434D-925D-A8D32F274E66}"
|
||||
EndProject
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{E2637D6D-04A5-4DE4-8AAF-E015C65DE8E1}"
|
||||
EndProject
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{5185D5C5-0EAD-49D5-B405-93B939F3639B}"
|
||||
EndProject
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{53D0AA09-F5FA-4721-8C1B-375CBD15B4E8}"
|
||||
EndProject
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{C6EE337B-91EA-472A-87C7-E9528408CE59}"
|
||||
EndProject
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{F39D8F09-6233-4495-ACD0-F98904993B7E}"
|
||||
EndProject
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{295284BA-D4E4-40AA-A2C2-BE36343F7DE6}"
|
||||
EndProject
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{85DA00E5-CC11-463C-8577-C34967C328F7}"
|
||||
EndProject
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{C1EBE17D-BFAD-47DA-88EB-BB073B84593E}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Booking", "src\Services\Booking\src\Booking\Booking.csproj", "{B2BAA061-C005-409F-9D3E-BDCBE5B1B136}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Booking.Api", "src\Services\Booking\src\Booking.Api\Booking.Api.csproj", "{4E8FB852-4317-43D2-8EFC-14E3ECCFDA2C}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Flight", "src\Services\Flight\src\Flight\Flight.csproj", "{574222F8-9C26-4015-8F35-C1E5D41A505F}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Flight.Api", "src\Services\Flight\src\Flight.Api\Flight.Api.csproj", "{B8F734F5-873C-4367-9EBD-38EA420CD868}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Identity", "src\Services\Identity\src\Identity\Identity.csproj", "{65C1BB58-2A2E-44FF-B15D-2B023CF088D4}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Identity.Api", "src\Services\Identity\src\Identity.Api\Identity.Api.csproj", "{BEE7A9D7-1BFC-477E-B070-4BE63C0361AA}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Passenger", "src\Services\Passenger\src\Passenger\Passenger.csproj", "{6D7BCECE-D77D-4C57-A296-CA6E728E94B7}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Passenger.Api", "src\Services\Passenger\src\Passenger.Api\Passenger.Api.csproj", "{4F29C4B6-A7DA-4A92-9CDB-42FE98238837}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Integration.Test", "src\Services\Flight\tests\Integration.Test.csproj", "{92E4D21C-2904-46F5-947D-74138003B19F}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
Debug|Any CPU = Debug|Any CPU
|
||||
Release|Any CPU = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(SolutionProperties) = preSolution
|
||||
HideSolutionNode = FALSE
|
||||
EndGlobalSection
|
||||
GlobalSection(NestedProjects) = preSolution
|
||||
{776BDF43-0DEA-44A3-AF72-99408CE544EE} = {3E38DD17-9EEE-4815-9D5B-BEB5549020A0}
|
||||
{A2D7C5C4-5148-4C3E-BB12-B7A197A290F5} = {776BDF43-0DEA-44A3-AF72-99408CE544EE}
|
||||
{E42BB533-4144-4D78-BCCE-50BA00BCADBE} = {5B69EDFD-4B09-457A-AAAF-D816D402D595}
|
||||
{5F0996AB-F8DB-4240-BD4A-DFDD70638A73} = {9010E0B5-9C42-4256-ADE4-E290434F2CEF}
|
||||
{1A2ABCD9-493B-4848-9C69-919CDBCA61F3} = {9010E0B5-9C42-4256-ADE4-E290434F2CEF}
|
||||
{22447274-717D-4321-87F3-868BAF93CBEC} = {9010E0B5-9C42-4256-ADE4-E290434F2CEF}
|
||||
{55BE6759-95AA-434D-925D-A8D32F274E66} = {9010E0B5-9C42-4256-ADE4-E290434F2CEF}
|
||||
{E2637D6D-04A5-4DE4-8AAF-E015C65DE8E1} = {22447274-717D-4321-87F3-868BAF93CBEC}
|
||||
{5185D5C5-0EAD-49D5-B405-93B939F3639B} = {22447274-717D-4321-87F3-868BAF93CBEC}
|
||||
{53D0AA09-F5FA-4721-8C1B-375CBD15B4E8} = {5F0996AB-F8DB-4240-BD4A-DFDD70638A73}
|
||||
{C6EE337B-91EA-472A-87C7-E9528408CE59} = {5F0996AB-F8DB-4240-BD4A-DFDD70638A73}
|
||||
{F39D8F09-6233-4495-ACD0-F98904993B7E} = {55BE6759-95AA-434D-925D-A8D32F274E66}
|
||||
{295284BA-D4E4-40AA-A2C2-BE36343F7DE6} = {55BE6759-95AA-434D-925D-A8D32F274E66}
|
||||
{85DA00E5-CC11-463C-8577-C34967C328F7} = {1A2ABCD9-493B-4848-9C69-919CDBCA61F3}
|
||||
{C1EBE17D-BFAD-47DA-88EB-BB073B84593E} = {1A2ABCD9-493B-4848-9C69-919CDBCA61F3}
|
||||
{B2BAA061-C005-409F-9D3E-BDCBE5B1B136} = {E2637D6D-04A5-4DE4-8AAF-E015C65DE8E1}
|
||||
{4E8FB852-4317-43D2-8EFC-14E3ECCFDA2C} = {E2637D6D-04A5-4DE4-8AAF-E015C65DE8E1}
|
||||
{574222F8-9C26-4015-8F35-C1E5D41A505F} = {53D0AA09-F5FA-4721-8C1B-375CBD15B4E8}
|
||||
{B8F734F5-873C-4367-9EBD-38EA420CD868} = {53D0AA09-F5FA-4721-8C1B-375CBD15B4E8}
|
||||
{65C1BB58-2A2E-44FF-B15D-2B023CF088D4} = {F39D8F09-6233-4495-ACD0-F98904993B7E}
|
||||
{BEE7A9D7-1BFC-477E-B070-4BE63C0361AA} = {F39D8F09-6233-4495-ACD0-F98904993B7E}
|
||||
{6D7BCECE-D77D-4C57-A296-CA6E728E94B7} = {85DA00E5-CC11-463C-8577-C34967C328F7}
|
||||
{4F29C4B6-A7DA-4A92-9CDB-42FE98238837} = {85DA00E5-CC11-463C-8577-C34967C328F7}
|
||||
{92E4D21C-2904-46F5-947D-74138003B19F} = {C6EE337B-91EA-472A-87C7-E9528408CE59}
|
||||
EndGlobalSection
|
||||
GlobalSection(ProjectConfigurationPlatforms) = postSolution
|
||||
{A2D7C5C4-5148-4C3E-BB12-B7A197A290F5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{A2D7C5C4-5148-4C3E-BB12-B7A197A290F5}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{A2D7C5C4-5148-4C3E-BB12-B7A197A290F5}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{A2D7C5C4-5148-4C3E-BB12-B7A197A290F5}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{E42BB533-4144-4D78-BCCE-50BA00BCADBE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{E42BB533-4144-4D78-BCCE-50BA00BCADBE}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{E42BB533-4144-4D78-BCCE-50BA00BCADBE}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{E42BB533-4144-4D78-BCCE-50BA00BCADBE}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{B2BAA061-C005-409F-9D3E-BDCBE5B1B136}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{B2BAA061-C005-409F-9D3E-BDCBE5B1B136}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{B2BAA061-C005-409F-9D3E-BDCBE5B1B136}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{B2BAA061-C005-409F-9D3E-BDCBE5B1B136}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{4E8FB852-4317-43D2-8EFC-14E3ECCFDA2C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{4E8FB852-4317-43D2-8EFC-14E3ECCFDA2C}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{4E8FB852-4317-43D2-8EFC-14E3ECCFDA2C}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{4E8FB852-4317-43D2-8EFC-14E3ECCFDA2C}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{574222F8-9C26-4015-8F35-C1E5D41A505F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{574222F8-9C26-4015-8F35-C1E5D41A505F}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{574222F8-9C26-4015-8F35-C1E5D41A505F}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{574222F8-9C26-4015-8F35-C1E5D41A505F}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{B8F734F5-873C-4367-9EBD-38EA420CD868}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{B8F734F5-873C-4367-9EBD-38EA420CD868}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{B8F734F5-873C-4367-9EBD-38EA420CD868}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{B8F734F5-873C-4367-9EBD-38EA420CD868}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{65C1BB58-2A2E-44FF-B15D-2B023CF088D4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{65C1BB58-2A2E-44FF-B15D-2B023CF088D4}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{65C1BB58-2A2E-44FF-B15D-2B023CF088D4}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{65C1BB58-2A2E-44FF-B15D-2B023CF088D4}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{BEE7A9D7-1BFC-477E-B070-4BE63C0361AA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{BEE7A9D7-1BFC-477E-B070-4BE63C0361AA}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{BEE7A9D7-1BFC-477E-B070-4BE63C0361AA}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{BEE7A9D7-1BFC-477E-B070-4BE63C0361AA}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{6D7BCECE-D77D-4C57-A296-CA6E728E94B7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{6D7BCECE-D77D-4C57-A296-CA6E728E94B7}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{6D7BCECE-D77D-4C57-A296-CA6E728E94B7}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{6D7BCECE-D77D-4C57-A296-CA6E728E94B7}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{4F29C4B6-A7DA-4A92-9CDB-42FE98238837}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{4F29C4B6-A7DA-4A92-9CDB-42FE98238837}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{4F29C4B6-A7DA-4A92-9CDB-42FE98238837}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{4F29C4B6-A7DA-4A92-9CDB-42FE98238837}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{92E4D21C-2904-46F5-947D-74138003B19F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{92E4D21C-2904-46F5-947D-74138003B19F}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{92E4D21C-2904-46F5-947D-74138003B19F}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{92E4D21C-2904-46F5-947D-74138003B19F}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
EndGlobalSection
|
||||
EndGlobal
|
||||
229
booking.rest
Normal file
229
booking.rest
Normal file
@ -0,0 +1,229 @@
|
||||
# https://github.com/Huachao/vscode-restclient
|
||||
@api-gateway=https://localhost:5001
|
||||
@identity-api=https://localhost:5005
|
||||
@flight-api=https://localhost:5003
|
||||
@passenger-api=https://localhost:5012
|
||||
@booking-api=https://localhost:5010
|
||||
@contentType = application/json
|
||||
@flightid = 1
|
||||
@passengerId = 1
|
||||
|
||||
################################# Identity API #################################
|
||||
|
||||
###
|
||||
# @name ApiRoot_Identity
|
||||
GET {{identity-api}}
|
||||
###
|
||||
|
||||
|
||||
###
|
||||
# @name Authenticate
|
||||
POST {{api-gateway}}/connect/token
|
||||
Content-Type: application/x-www-form-urlencoded
|
||||
|
||||
grant_type=password
|
||||
&client_id=client
|
||||
&client_secret=secret
|
||||
&username=meysamh
|
||||
&password=Admin@123456
|
||||
&scope=flight-api
|
||||
###
|
||||
|
||||
|
||||
|
||||
###
|
||||
# @name Register_New_User
|
||||
POST {{api-gateway}}/identity/register-user
|
||||
accept: application/json
|
||||
Content-Type: application/json
|
||||
authorization: bearer {{Authenticate.response.body.access_token}}
|
||||
|
||||
{
|
||||
"firstName": "John6",
|
||||
"lastName": "Doe6",
|
||||
"username": "admin6",
|
||||
"passportNumber": "1234567896",
|
||||
"email": "admin6@admin.com",
|
||||
"password": "Admin6@12345",
|
||||
"confirmPassword": "Admin6@12345"
|
||||
}
|
||||
###
|
||||
|
||||
################################# Flight API #################################
|
||||
|
||||
###
|
||||
# @name ApiRoot_Flight
|
||||
GET {{flight-api}}
|
||||
###
|
||||
|
||||
|
||||
###
|
||||
# @name Reserve_Seat
|
||||
Post {{api-gateway}}/api/v1/flight/reserve-seat
|
||||
accept: application/json
|
||||
Content-Type: application/json
|
||||
authorization: bearer {{Authenticate.response.body.access_token}}
|
||||
|
||||
{
|
||||
"flightId": 1,
|
||||
"seatNumber": "12C"
|
||||
}
|
||||
###
|
||||
|
||||
|
||||
###
|
||||
# @name Get_Available_Seats
|
||||
GET {{api-gateway}}/api/v1/flight/get-available-seats/{{flightid}}
|
||||
accept: application/json
|
||||
Content-Type: application/json
|
||||
authorization: bearer {{Authenticate.response.body.access_token}}
|
||||
###
|
||||
|
||||
|
||||
###
|
||||
# @name Get_Flight_By_Id
|
||||
GET {{api-gateway}}/api/v1/flight/{{flightid}}
|
||||
accept: application/json
|
||||
Content-Type: application/json
|
||||
authorization: bearer {{Authenticate.response.body.access_token}}
|
||||
###
|
||||
|
||||
|
||||
###
|
||||
# @name Get_Available_Flights
|
||||
GET {{api-gateway}}/api/v1/flight/get-available-flights
|
||||
accept: application/json
|
||||
Content-Type: application/json
|
||||
authorization: bearer {{Authenticate.response.body.access_token}}
|
||||
###
|
||||
|
||||
|
||||
###
|
||||
# @name Create_Flights
|
||||
POST {{api-gateway}}/api/v1/flight
|
||||
accept: application/json
|
||||
Content-Type: application/json
|
||||
authorization: bearer {{Authenticate.response.body.access_token}}
|
||||
|
||||
{
|
||||
"flightNumber": "12BB44",
|
||||
"aircraftId": 1,
|
||||
"departureAirportId": 1,
|
||||
"departureDate": "2022-03-01T14:55:41.255Z",
|
||||
"arriveDate": "2022-03-01T14:55:41.255Z",
|
||||
"arriveAirportId": 2,
|
||||
"durationMinutes": 120,
|
||||
"flightDate": "2022-03-01T14:55:41.255Z",
|
||||
"status": 1,
|
||||
"price": 8000
|
||||
}
|
||||
###
|
||||
|
||||
|
||||
###
|
||||
# @name Update_Flights
|
||||
PUT {{api-gateway}}/api/v1/flight
|
||||
accept: application/json
|
||||
Content-Type: application/json
|
||||
authorization: bearer {{Authenticate.response.body.access_token}}
|
||||
|
||||
{
|
||||
"id": 1,
|
||||
"flightNumber": "BD467",
|
||||
"aircraftId": 1,
|
||||
"departureAirportId": 1,
|
||||
"departureDate": "2022-04-23T12:17:45.140Z",
|
||||
"arriveDate": "2022-04-23T12:17:45.140Z",
|
||||
"arriveAirportId": 2,
|
||||
"durationMinutes": 120,
|
||||
"flightDate": "2022-04-23T12:17:45.140Z",
|
||||
"status": 4,
|
||||
"isDeleted": false,
|
||||
"price": 99000
|
||||
}
|
||||
###
|
||||
|
||||
###
|
||||
# @name Create_Airport
|
||||
POST {{api-gateway}}/api/v1/flight/airport
|
||||
accept: application/json
|
||||
Content-Type: application/json
|
||||
authorization: bearer {{Authenticate.response.body.access_token}}
|
||||
|
||||
{
|
||||
"name": "mehrabad",
|
||||
"address": "tehran",
|
||||
"code": "12YD"
|
||||
}
|
||||
###
|
||||
|
||||
|
||||
|
||||
###
|
||||
# @name Create_Aircraft
|
||||
POST {{api-gateway}}/api/v1/flight/aircraft
|
||||
accept: application/json
|
||||
Content-Type: application/json
|
||||
authorization: bearer {{Authenticate.response.body.access_token}}
|
||||
|
||||
{
|
||||
"name": "airbus",
|
||||
"model": "320",
|
||||
"manufacturingYear": 2010
|
||||
}
|
||||
###
|
||||
|
||||
|
||||
################################# Passenger API #################################
|
||||
|
||||
###
|
||||
# @name ApiRoot_Passenger
|
||||
GET {{passenger-api}}
|
||||
###
|
||||
|
||||
|
||||
###
|
||||
# @name Complete_Registration_Passenger
|
||||
POST {{api-gateway}}/api/v1/passenger/complete-registration
|
||||
accept: application/json
|
||||
Content-Type: application/json
|
||||
authorization: bearer {{Authenticate.response.body.access_token}}
|
||||
|
||||
{
|
||||
"passportNumber": "1234567896",
|
||||
"passengerType": 1,
|
||||
"age": 30
|
||||
}
|
||||
###
|
||||
|
||||
|
||||
###
|
||||
# @name Get_Passenger_By_Id
|
||||
GET {{api-gateway}}/api/v1/passenger/{{passengerId}}
|
||||
accept: application/json
|
||||
Content-Type: application/json
|
||||
authorization: bearer {{Authenticate.response.body.access_token}}
|
||||
###
|
||||
|
||||
|
||||
################################# Booking API #################################
|
||||
|
||||
###
|
||||
# @name ApiRoot_Booking
|
||||
GET {{booking-api}}
|
||||
###
|
||||
|
||||
|
||||
###
|
||||
# @name Create_Booking
|
||||
POST {{api-gateway}}/api/v1/booking
|
||||
accept: application/json
|
||||
Content-Type: application/json
|
||||
authorization: bearer {{Authenticate.response.body.access_token}}
|
||||
|
||||
{
|
||||
"passengerId": 1,
|
||||
"flightId": 1,
|
||||
"description": "I want to fly to iran"
|
||||
}
|
||||
###
|
||||
172
deployments/docker-compose/docker-compose.yaml
Normal file
172
deployments/docker-compose/docker-compose.yaml
Normal file
@ -0,0 +1,172 @@
|
||||
version: "3.3"
|
||||
services:
|
||||
|
||||
#######################################################
|
||||
# Gateway
|
||||
#######################################################
|
||||
gateway:
|
||||
image: gateway
|
||||
build:
|
||||
args:
|
||||
Version: "1"
|
||||
context: ../../
|
||||
dockerfile: src/ApiGateway/Dockerfile
|
||||
container_name: booking-gateway
|
||||
ports:
|
||||
- "5000:80"
|
||||
- "5001:443"
|
||||
depends_on:
|
||||
- db
|
||||
- rabbitmq
|
||||
# - mongo
|
||||
links:
|
||||
- db
|
||||
- rabbitmq
|
||||
# - mongo
|
||||
volumes:
|
||||
- '${USERPROFILE}\.aspnet\https:/https/'
|
||||
environment:
|
||||
- 'ASPNETCORE_URLS=https://+;http://+'
|
||||
- ASPNETCORE_HTTPS_PORT=5001
|
||||
- ASPNETCORE_Kestrel__Certificates__Default__Password=password
|
||||
- ASPNETCORE_Kestrel__Certificates__Default__Path=/https/aspnetapp.pfx
|
||||
networks:
|
||||
- booking
|
||||
|
||||
|
||||
#######################################################
|
||||
# Flight
|
||||
#######################################################
|
||||
flight:
|
||||
image: flight
|
||||
build:
|
||||
args:
|
||||
Version: "1"
|
||||
context: ../../
|
||||
dockerfile: src/Services/Flight/Dockerfile
|
||||
container_name: flight
|
||||
ports:
|
||||
- 5004:80
|
||||
- 5003:443
|
||||
depends_on:
|
||||
- db
|
||||
- rabbitmq
|
||||
# - mongo
|
||||
volumes:
|
||||
- '${USERPROFILE}\.aspnet\https:/https/'
|
||||
environment:
|
||||
- 'ASPNETCORE_URLS=https://+;http://+'
|
||||
- ASPNETCORE_HTTPS_PORT=5003
|
||||
- ASPNETCORE_Kestrel__Certificates__Default__Password=password
|
||||
- ASPNETCORE_Kestrel__Certificates__Default__Path=/https/aspnetapp.pfx
|
||||
networks:
|
||||
- booking
|
||||
|
||||
|
||||
#######################################################
|
||||
# Identity
|
||||
#######################################################
|
||||
identity:
|
||||
image: identity
|
||||
build:
|
||||
args:
|
||||
Version: "1"
|
||||
context: ../../
|
||||
dockerfile: src/Services/Identity/Dockerfile
|
||||
container_name: identity
|
||||
ports:
|
||||
- 6005:80
|
||||
- 5005:443
|
||||
depends_on:
|
||||
- db
|
||||
- rabbitmq
|
||||
# - mongo
|
||||
links:
|
||||
- db
|
||||
- rabbitmq
|
||||
# - mongo
|
||||
volumes:
|
||||
- '${USERPROFILE}\.aspnet\https:/https/'
|
||||
environment:
|
||||
- 'ASPNETCORE_URLS=https://+;http://+'
|
||||
- ASPNETCORE_HTTPS_PORT=5005
|
||||
- ASPNETCORE_Kestrel__Certificates__Default__Password=password
|
||||
- ASPNETCORE_Kestrel__Certificates__Default__Path=/https/aspnetapp.pfx
|
||||
networks:
|
||||
- booking
|
||||
|
||||
|
||||
#######################################################
|
||||
# Passenger
|
||||
#######################################################
|
||||
passenger:
|
||||
image: passenger
|
||||
build:
|
||||
args:
|
||||
Version: "1"
|
||||
context: ../../
|
||||
dockerfile: src/Services/Passenger/Dockerfile
|
||||
container_name: passenger
|
||||
ports:
|
||||
- 6012:80
|
||||
- 5012:443
|
||||
depends_on:
|
||||
- db
|
||||
- rabbitmq
|
||||
# - mongo
|
||||
links:
|
||||
- db
|
||||
- rabbitmq
|
||||
# - mongo
|
||||
volumes:
|
||||
- '${USERPROFILE}\.aspnet\https:/https/'
|
||||
environment:
|
||||
- 'ASPNETCORE_URLS=https://+;http://+'
|
||||
- ASPNETCORE_HTTPS_PORT=5012
|
||||
- ASPNETCORE_Kestrel__Certificates__Default__Password=password
|
||||
- ASPNETCORE_Kestrel__Certificates__Default__Path=/https/aspnetapp.pfx
|
||||
networks:
|
||||
- booking
|
||||
|
||||
|
||||
#######################################################
|
||||
# Booking
|
||||
#######################################################
|
||||
booking:
|
||||
image: booking
|
||||
build:
|
||||
args:
|
||||
Version: "1"
|
||||
context: ../../
|
||||
dockerfile: src/Services/Booking/Dockerfile
|
||||
container_name: booking
|
||||
ports:
|
||||
- 6010:80
|
||||
- 5010:443
|
||||
depends_on:
|
||||
- db
|
||||
- rabbitmq
|
||||
# - mongo
|
||||
links:
|
||||
- db
|
||||
- rabbitmq
|
||||
# - mongo
|
||||
volumes:
|
||||
- '${USERPROFILE}\.aspnet\https:/https/'
|
||||
environment:
|
||||
- 'ASPNETCORE_URLS=https://+;http://+'
|
||||
- ASPNETCORE_HTTPS_PORT=5010
|
||||
- ASPNETCORE_Kestrel__Certificates__Default__Password=password
|
||||
- ASPNETCORE_Kestrel__Certificates__Default__Path=/https/aspnetapp.pfx
|
||||
networks:
|
||||
- booking
|
||||
|
||||
|
||||
networks:
|
||||
booking:
|
||||
name: booking
|
||||
|
||||
volumes:
|
||||
db-data:
|
||||
external: false
|
||||
|
||||
102
deployments/docker-compose/infrastructure.yaml
Normal file
102
deployments/docker-compose/infrastructure.yaml
Normal file
@ -0,0 +1,102 @@
|
||||
version: "3.7"
|
||||
|
||||
services:
|
||||
|
||||
#######################################################
|
||||
# Rabbitmq
|
||||
#######################################################
|
||||
rabbitmq:
|
||||
image: rabbitmq:3-management
|
||||
container_name: rabbitmq
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- 5672:5672
|
||||
- 15672:15672
|
||||
networks:
|
||||
- booking
|
||||
|
||||
|
||||
#######################################################
|
||||
# SqlServer
|
||||
#######################################################
|
||||
db:
|
||||
container_name: sqldb
|
||||
image: mcr.microsoft.com/mssql/server:2017-latest
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "1433:1433"
|
||||
environment:
|
||||
SA_PASSWORD: "@Aa123456"
|
||||
ACCEPT_EULA: "Y"
|
||||
networks:
|
||||
- booking
|
||||
|
||||
|
||||
#######################################################
|
||||
# Jaeger
|
||||
#######################################################
|
||||
jaeger:
|
||||
image: jaegertracing/all-in-one
|
||||
container_name: jaeger
|
||||
restart: unless-stopped
|
||||
networks:
|
||||
- booking
|
||||
ports:
|
||||
- 5775:5775/udp
|
||||
- 5778:5778
|
||||
- 6831:6831/udp
|
||||
- 6832:6832/udp
|
||||
- 9411:9411
|
||||
- 14268:14268
|
||||
- 16686:16686
|
||||
|
||||
|
||||
#######################################################
|
||||
# EventStoreDB
|
||||
#######################################################
|
||||
eventstore.db:
|
||||
image: eventstore/eventstore:21.2.0-buster-slim
|
||||
restart: on-failure
|
||||
environment:
|
||||
- EVENTSTORE_CLUSTER_SIZE=1
|
||||
- EVENTSTORE_RUN_PROJECTIONS=All
|
||||
- EVENTSTORE_START_STANDARD_PROJECTIONS=true
|
||||
- EVENTSTORE_EXT_TCP_PORT=1010
|
||||
- EVENTSTORE_EXT_HTTP_PORT=2113
|
||||
- EVENTSTORE_INSECURE=true
|
||||
- EVENTSTORE_ENABLE_EXTERNAL_TCP=true
|
||||
- EVENTSTORE_ENABLE_ATOM_PUB_OVER_HTTP=true
|
||||
ports:
|
||||
- '1010:1113'
|
||||
- '2113:2113'
|
||||
networks:
|
||||
- booking
|
||||
|
||||
|
||||
#######################################################
|
||||
# Mongo
|
||||
#######################################################
|
||||
mongo:
|
||||
image: mongo
|
||||
container_name: mongo
|
||||
restart: unless-stopped
|
||||
# environment:
|
||||
# - MONGO_INITDB_ROOT_USERNAME=root
|
||||
# - MONGO_INITDB_ROOT_PASSWORD=secret
|
||||
networks:
|
||||
- booking
|
||||
ports:
|
||||
- 27017:27017
|
||||
volumes:
|
||||
- mongo:/data/db
|
||||
|
||||
|
||||
networks:
|
||||
booking:
|
||||
name: booking
|
||||
|
||||
volumes:
|
||||
db-data:
|
||||
external: false
|
||||
mongo:
|
||||
driver: local
|
||||
45
deployments/tye/tye.yml
Normal file
45
deployments/tye/tye.yml
Normal file
@ -0,0 +1,45 @@
|
||||
name: Booking
|
||||
services:
|
||||
- name: booking-gateway
|
||||
project: ./../../src/ApiGateway/src/ApiGateway.csproj
|
||||
bindings:
|
||||
- port: 5001
|
||||
env:
|
||||
- name: ASPNETCORE_ENVIRONMENT
|
||||
value: development
|
||||
|
||||
|
||||
- name: flight
|
||||
project: ./../../src/Services/Flight/src/Flight.Api/Flight.Api.csproj
|
||||
bindings:
|
||||
- port: 5003
|
||||
env:
|
||||
- name: ASPNETCORE_ENVIRONMENT
|
||||
value: development
|
||||
|
||||
|
||||
- name: identity
|
||||
project: ./../../src/Services/Identity/src/Identity.Api/Identity.Api.csproj
|
||||
bindings:
|
||||
- port: 5005
|
||||
env:
|
||||
- name: ASPNETCORE_ENVIRONMENT
|
||||
value: development
|
||||
|
||||
|
||||
- name: passenger
|
||||
project: ./../../src/Services/Passenger/src/Passenger.Api/Passenger.Api.csproj
|
||||
bindings:
|
||||
- port: 5012
|
||||
env:
|
||||
- name: ASPNETCORE_ENVIRONMENT
|
||||
value: development
|
||||
|
||||
|
||||
- name: booking
|
||||
project: ./../../src/Services/Booking/src/Booking.Api/Booking.Api.csproj
|
||||
bindings:
|
||||
- port: 5010
|
||||
env:
|
||||
- name: ASPNETCORE_ENVIRONMENT
|
||||
value: development
|
||||
37
src/ApiGateway/Dockerfile
Normal file
37
src/ApiGateway/Dockerfile
Normal file
@ -0,0 +1,37 @@
|
||||
FROM mcr.microsoft.com/dotnet/sdk:6.0 AS builder
|
||||
WORKDIR /src
|
||||
|
||||
# Setup working directory for the project
|
||||
WORKDIR /src
|
||||
COPY ./src/BuildingBlocks/BuildingBlocks.csproj ./BuildingBlocks/
|
||||
COPY ./src/ApiGateway/src/ApiGateway.csproj ./ApiGateway/src/
|
||||
|
||||
|
||||
# Restore nuget packages
|
||||
RUN dotnet restore ./ApiGateway/src/ApiGateway.csproj
|
||||
|
||||
# Copy project files
|
||||
COPY ./src/BuildingBlocks ./BuildingBlocks/
|
||||
COPY ./src/ApiGateway/src ./ApiGateway/src/
|
||||
|
||||
# Build project with Release configuration
|
||||
# and no restore, as we did it already
|
||||
|
||||
RUN ls
|
||||
RUN dotnet build -c Release --no-restore ./ApiGateway/src/ApiGateway.csproj
|
||||
|
||||
WORKDIR /src/ApiGateway/src
|
||||
|
||||
# Publish project to output folder
|
||||
# and no build, as we did it already
|
||||
RUN dotnet publish -c Release --no-build -o out
|
||||
|
||||
FROM mcr.microsoft.com/dotnet/aspnet:6.0
|
||||
|
||||
# Setup working directory for the project
|
||||
WORKDIR /app
|
||||
COPY --from=builder /src/ApiGateway/src/out .
|
||||
EXPOSE 80
|
||||
EXPOSE 443
|
||||
ENTRYPOINT ["dotnet", "ApiGateway.dll"]
|
||||
|
||||
18
src/ApiGateway/src/ApiGateway.csproj
Normal file
18
src/ApiGateway/src/ApiGateway.csproj
Normal file
@ -0,0 +1,18 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net6.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.2.3" />
|
||||
<PackageReference Include="Yarp.ReverseProxy" Version="1.0.0" />
|
||||
<PackageReference Include="Microsoft.Identity.Web" Version="1.22.3" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\BuildingBlocks\BuildingBlocks.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
49
src/ApiGateway/src/Program.cs
Normal file
49
src/ApiGateway/src/Program.cs
Normal file
@ -0,0 +1,49 @@
|
||||
using BuildingBlocks.Jwt;
|
||||
using BuildingBlocks.Logging;
|
||||
using BuildingBlocks.Utils;
|
||||
using BuildingBlocks.Web;
|
||||
using Figgle;
|
||||
using Microsoft.AspNetCore.Authentication;
|
||||
using Serilog;
|
||||
|
||||
var builder = WebApplication.CreateBuilder(args);
|
||||
var configuration = builder.Configuration;
|
||||
|
||||
var appOptions = builder.Services.GetOptions<AppOptions>("AppOptions");
|
||||
Console.WriteLine(FiggleFonts.Standard.Render(appOptions.Name));
|
||||
|
||||
builder.AddCustomSerilog();
|
||||
builder.Services.AddJwt();
|
||||
builder.Services.AddControllers();
|
||||
builder.Services.AddHttpContextAccessor();
|
||||
|
||||
builder.Services.AddReverseProxy().LoadFromConfig(builder.Configuration.GetSection("Yarp"));
|
||||
|
||||
|
||||
var app = builder.Build();
|
||||
|
||||
app.UseSerilogRequestLogging();
|
||||
app.UseCorrelationId();
|
||||
app.UseRouting();
|
||||
app.UseHttpsRedirection();
|
||||
app.UseAuthentication();
|
||||
app.UseAuthorization();
|
||||
|
||||
app.UseEndpoints(endpoints =>
|
||||
{
|
||||
endpoints.MapControllers();
|
||||
endpoints.MapReverseProxy(proxyPipeline =>
|
||||
{
|
||||
proxyPipeline.Use(async (context, next) =>
|
||||
{
|
||||
var token = await context.GetTokenAsync("access_token");
|
||||
context.Request.Headers["Authorization"] = $"Bearer {token}";
|
||||
|
||||
await next().ConfigureAwait(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
app.MapGet("/", x => x.Response.WriteAsync(appOptions.Name));
|
||||
|
||||
app.Run();
|
||||
30
src/ApiGateway/src/Properties/launchSettings.json
Normal file
30
src/ApiGateway/src/Properties/launchSettings.json
Normal file
@ -0,0 +1,30 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/launchsettings.json",
|
||||
"iisSettings": {
|
||||
"windowsAuthentication": false,
|
||||
"anonymousAuthentication": true,
|
||||
"iisExpress": {
|
||||
"applicationUrl": "http://localhost:17191",
|
||||
"sslPort": 44352
|
||||
}
|
||||
},
|
||||
"profiles": {
|
||||
"ApiGateway": {
|
||||
"commandName": "Project",
|
||||
"dotnetRunMessages": true,
|
||||
"launchBrowser": true,
|
||||
"applicationUrl": "http://localhost:5000;https://localhost:5001",
|
||||
"environmentVariables": {
|
||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||
}
|
||||
},
|
||||
"IIS Express": {
|
||||
"commandName": "IISExpress",
|
||||
"launchBrowser": true,
|
||||
"launchUrl": "swagger",
|
||||
"environmentVariables": {
|
||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
8
src/ApiGateway/src/appsettings.Development.json
Normal file
8
src/ApiGateway/src/appsettings.Development.json
Normal file
@ -0,0 +1,8 @@
|
||||
{
|
||||
"Logging": {
|
||||
"LogLevel": {
|
||||
"Default": "Information",
|
||||
"Microsoft.AspNetCore": "Warning"
|
||||
}
|
||||
}
|
||||
}
|
||||
34
src/ApiGateway/src/appsettings.docker.json
Normal file
34
src/ApiGateway/src/appsettings.docker.json
Normal file
@ -0,0 +1,34 @@
|
||||
{
|
||||
"Yarp": {
|
||||
"clusters": {
|
||||
"flight": {
|
||||
"destinations": {
|
||||
"destination1": {
|
||||
"address" : "http://flight"
|
||||
}
|
||||
}
|
||||
},
|
||||
"identity": {
|
||||
"destinations": {
|
||||
"destination1": {
|
||||
"address" : "http://identity"
|
||||
}
|
||||
}
|
||||
},
|
||||
"passenger": {
|
||||
"destinations": {
|
||||
"destination1": {
|
||||
"address" : "http://passenger"
|
||||
}
|
||||
}
|
||||
},
|
||||
"booking": {
|
||||
"destinations": {
|
||||
"destination1": {
|
||||
"address" : "http://booking"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
96
src/ApiGateway/src/appsettings.json
Normal file
96
src/ApiGateway/src/appsettings.json
Normal file
@ -0,0 +1,96 @@
|
||||
{
|
||||
"AppOptions": {
|
||||
"Name": "ApiGateway"
|
||||
},
|
||||
"Serilog": {
|
||||
"MinimumLevel": {
|
||||
"Default": "Information",
|
||||
"Override": {
|
||||
"Microsoft": "Warning",
|
||||
"Microsoft.Hosting.Lifetime": "Information"
|
||||
}
|
||||
}
|
||||
},
|
||||
"Yarp": {
|
||||
"routes": {
|
||||
"identity": {
|
||||
"clusterId": "identity",
|
||||
"match": {
|
||||
"path": "{**catch-all}"
|
||||
},
|
||||
"transforms": [
|
||||
{
|
||||
"pathPattern": "{**catch-all}"
|
||||
}
|
||||
]
|
||||
},
|
||||
"flight": {
|
||||
"clusterId": "flight",
|
||||
"match": {
|
||||
"path": "api/{version}/flight/{**catch-all}"
|
||||
},
|
||||
"transforms": [
|
||||
{
|
||||
"pathPattern": "api/{version}/flight/{**catch-all}"
|
||||
}
|
||||
]
|
||||
},
|
||||
"passenger": {
|
||||
"clusterId": "passenger",
|
||||
"match": {
|
||||
"path": "api/{version}/passenger/{**catch-all}"
|
||||
},
|
||||
"transforms": [
|
||||
{
|
||||
"pathPattern": "api/{version}/passenger/{**catch-all}"
|
||||
}
|
||||
]
|
||||
},
|
||||
"booking": {
|
||||
"clusterId": "booking",
|
||||
"match": {
|
||||
"path": "api/{version}/booking/{**catch-all}"
|
||||
},
|
||||
"transforms": [
|
||||
{
|
||||
"pathPattern": "api/{version}/booking/{**catch-all}"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"clusters": {
|
||||
"flight": {
|
||||
"destinations": {
|
||||
"destination1": {
|
||||
"address": "https://localhost:5003"
|
||||
}
|
||||
}
|
||||
},
|
||||
"identity": {
|
||||
"destinations": {
|
||||
"destination1": {
|
||||
"address": "https://localhost:5005"
|
||||
}
|
||||
}
|
||||
},
|
||||
"passenger": {
|
||||
"destinations": {
|
||||
"destination1": {
|
||||
"address": "https://localhost:5012"
|
||||
}
|
||||
}
|
||||
},
|
||||
"booking": {
|
||||
"destinations": {
|
||||
"destination1": {
|
||||
"address": "https://localhost:5010"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"Jwt": {
|
||||
"Authority": "https://localhost:5005"
|
||||
},
|
||||
"AllowedHosts": "*"
|
||||
}
|
||||
120
src/BuildingBlocks/BuildingBlocks.csproj
Normal file
120
src/BuildingBlocks/BuildingBlocks.csproj
Normal file
@ -0,0 +1,120 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net6.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
</PropertyGroup>
|
||||
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Ardalis.GuardClauses" Version="3.3.0" />
|
||||
<PackageReference Include="AspNetCore.HealthChecks.UI.SQLite.Storage" Version="5.0.1" />
|
||||
<PackageReference Include="Ben.BlockingDetector" Version="0.0.4" />
|
||||
<PackageReference Include="EasyCaching.Core" Version="1.4.1" />
|
||||
<PackageReference Include="EasyCaching.InMemory" Version="1.4.1" />
|
||||
<PackageReference Include="EFCore.NamingConventions" Version="6.0.0" />
|
||||
<PackageReference Include="EntityFrameworkCore.Triggered" Version="3.0.0" />
|
||||
<PackageReference Include="Figgle" Version="0.4.0" />
|
||||
<PackageReference Include="FluentValidation" Version="10.3.6" />
|
||||
<PackageReference Include="FluentValidation.AspNetCore" Version="10.3.6" />
|
||||
<PackageReference Include="Grpc.Net.Client" Version="2.44.0" />
|
||||
<PackageReference Include="MagicOnion" Version="4.4.0" />
|
||||
<PackageReference Include="MagicOnion.Abstractions" Version="4.4.0" />
|
||||
<PackageReference Include="MagicOnion.Client" Version="4.4.0" />
|
||||
<PackageReference Include="MagicOnion.Server" Version="4.4.0" />
|
||||
<PackageReference Include="MagicOnion.Server" Version="4.4.0" />
|
||||
<PackageReference Include="Polly" Version="7.2.3" />
|
||||
<PackageReference Include="protobuf-net.BuildTools" Version="3.0.115">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="protobuf-net.Grpc" Version="1.0.152" />
|
||||
<PackageReference Include="Hellang.Middleware.ProblemDetails" Version="6.3.0" />
|
||||
<PackageReference Include="Humanizer.Core" Version="2.14.1" />
|
||||
<PackageReference Include="IdGen" Version="3.0.0" />
|
||||
<PackageReference Include="Mapster" Version="7.3.0" />
|
||||
<PackageReference Include="Mapster.DependencyInjection" Version="1.0.0" />
|
||||
<PackageReference Include="MediatR" Version="9.0.0" />
|
||||
<PackageReference Include="MediatR.Extensions.Microsoft.DependencyInjection" Version="9.0.0" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="6.0.0" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Http" Version="2.2.2" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Mvc.Versioning.ApiExplorer" Version="5.0.0" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="6.0.1" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.InMemory" Version="6.0.1" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="6.0.1" />
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="6.0.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration.Binder" Version="6.0.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="6.0.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="6.0.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Http.Polly" Version="6.0.1" />
|
||||
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="6.0.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.PlatformAbstractions" Version="1.1.0" />
|
||||
<PackageReference Include="MongoDB.Driver" Version="2.14.1" />
|
||||
<PackageReference Include="Moq" Version="4.16.1" />
|
||||
<PackageReference Include="Newtonsoft.Json" Version="13.0.1" />
|
||||
<PackageReference Include="OpenTelemetry.Contrib.Instrumentation.MassTransit" Version="1.0.0-beta2" />
|
||||
<PackageReference Include="protobuf-net.Grpc.AspNetCore" Version="1.0.152" />
|
||||
<PackageReference Include="Scrutor" Version="3.3.0" />
|
||||
<PackageReference Include="Scrutor.AspNetCore" Version="3.3.0" />
|
||||
<PackageReference Include="Serilog" Version="2.10.0" />
|
||||
<PackageReference Include="Serilog.AspNetCore" Version="4.1.0" />
|
||||
<PackageReference Include="Serilog.Enrichers.Span" Version="2.2.0" />
|
||||
<PackageReference Include="Serilog.Formatting.Elasticsearch" Version="8.4.1" />
|
||||
<PackageReference Include="Serilog.Sinks.Console" Version="4.0.1" />
|
||||
<PackageReference Include="Serilog.Sinks.Elasticsearch" Version="8.4.1" />
|
||||
<PackageReference Include="Serilog.Sinks.Seq" Version="5.1.0" />
|
||||
<PackageReference Include="Serilog.Sinks.SpectreConsole" Version="0.1.1" />
|
||||
<PackageReference Include="Swashbuckle.AspNetCore.Annotations" Version="6.2.3" />
|
||||
<PackageReference Include="Swashbuckle.AspNetCore.SwaggerGen" Version="6.2.3" />
|
||||
<PackageReference Include="Swashbuckle.AspNetCore.SwaggerUI" Version="6.2.3" />
|
||||
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.2.3" />
|
||||
<PackageReference Include="AspNetCore.HealthChecks.UI" Version="5.0.1" />
|
||||
<PackageReference Include="AspNetCore.HealthChecks.UI.Client" Version="5.0.1" />
|
||||
<PackageReference Include="AspNetCore.HealthChecks.UI.InMemory.Storage" Version="5.0.1" />
|
||||
<PackageReference Include="Microsoft.Extensions.Diagnostics.HealthChecks" Version="6.0.1" />
|
||||
<PackageReference Include="AspNetCore.HealthChecks.MongoDb" Version="6.0.1" />
|
||||
<PackageReference Include="System.Interactive.Async" Version="5.1.0" />
|
||||
<PackageReference Include="MassTransit" Version="8.0.0" />
|
||||
<PackageReference Include="MassTransit.RabbitMQ" Version="8.0.0" />
|
||||
<PackageReference Include="DotNetCore.CAP" Version="6.0.0" />
|
||||
<PackageReference Include="DotNetCore.CAP.Dashboard" Version="6.0.0" />
|
||||
<PackageReference Include="DotNetCore.CAP.MongoDB" Version="6.0.0" />
|
||||
<PackageReference Include="DotNetCore.CAP.OpenTelemetry" Version="6.0.0" />
|
||||
<PackageReference Include="DotNetCore.CAP.RabbitMQ" Version="6.0.0" />
|
||||
<PackageReference Include="DotNetCore.CAP.SqlServer" Version="6.0.0" />
|
||||
<PackageReference Include="Duende.IdentityServer" Version="6.0.0" />
|
||||
<PackageReference Include="Duende.IdentityServer.AspNetIdentity" Version="6.0.0" />
|
||||
<PackageReference Include="Duende.IdentityServer.EntityFramework" Version="6.0.0" />
|
||||
<PackageReference Include="Duende.IdentityServer.EntityFramework.Storage" Version="6.0.3" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="6.0.1" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="6.0.0" />
|
||||
|
||||
<PackageReference Include="Jaeger" Version="0.3.7" />
|
||||
<PackageReference Include="OpenTracing" Version="0.12.1" />
|
||||
<PackageReference Include="prometheus-net" Version="6.0.0" />
|
||||
<PackageReference Include="prometheus-net.AspNetCore" Version="6.0.0" />
|
||||
|
||||
<PackageReference Include="OpenTelemetry" Version="1.2.0-rc3" />
|
||||
<PackageReference Include="OpenTelemetry.Exporter.Jaeger" Version="1.2.0-rc3" />
|
||||
<PackageReference Include="OpenTelemetry.Extensions.Hosting" Version="1.0.0-rc9" />
|
||||
<PackageReference Include="OpenTelemetry.Instrumentation.AspNetCore" Version="1.0.0-rc9" />
|
||||
<PackageReference Include="OpenTelemetry.Instrumentation.Http" Version="1.0.0-rc9" />
|
||||
|
||||
<PackageReference Include="Microsoft.VisualStudio.Threading.Analyzers" Version="17.0.64">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
|
||||
<PackageReference Include="EventStore.Client.Grpc.Streams" Version="22.0.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Hosting" Version="6.0.1" />
|
||||
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="6.0.3" />
|
||||
|
||||
</ItemGroup>
|
||||
|
||||
|
||||
<ItemGroup>
|
||||
<Folder Include="Contracts" />
|
||||
<Folder Include="EventStoreDB\BackgroundWorkers" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
54
src/BuildingBlocks/CAP/Extensions.cs
Normal file
54
src/BuildingBlocks/CAP/Extensions.cs
Normal file
@ -0,0 +1,54 @@
|
||||
using System.Text.Encodings.Web;
|
||||
using System.Text.Unicode;
|
||||
using BuildingBlocks.Utils;
|
||||
using DotNetCore.CAP;
|
||||
using DotNetCore.CAP.Messages;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using OpenTelemetry.Trace;
|
||||
|
||||
namespace BuildingBlocks.CAP;
|
||||
|
||||
public static class Extensions
|
||||
{
|
||||
public static IServiceCollection AddCustomCap<TDbContext>(this IServiceCollection services) where TDbContext : DbContext
|
||||
{
|
||||
var rabbitMqOptions = services.GetOptions<RabbitMQOptions>("RabbitMq");
|
||||
|
||||
services.AddCap(x =>
|
||||
{
|
||||
x.UseEntityFramework<TDbContext>();
|
||||
x.UseRabbitMQ(o =>
|
||||
{
|
||||
o.HostName = rabbitMqOptions.HostName;
|
||||
o.UserName = rabbitMqOptions.UserName;
|
||||
o.Password = rabbitMqOptions.Password;
|
||||
});
|
||||
x.UseDashboard();
|
||||
x.FailedRetryCount = 5;
|
||||
x.FailedThresholdCallback = failed =>
|
||||
{
|
||||
var logger = failed.ServiceProvider.GetService<ILogger>();
|
||||
logger?.LogError(
|
||||
$@"A message of type {failed.MessageType} failed after executing {x.FailedRetryCount} several times,
|
||||
requiring manual troubleshooting. Message name: {failed.Message.GetName()}");
|
||||
};
|
||||
x.JsonSerializerOptions.Encoder = JavaScriptEncoder.Create(UnicodeRanges.All);
|
||||
});
|
||||
|
||||
// services.AddOpenTelemetryTracing((builder) => builder
|
||||
// .AddAspNetCoreInstrumentation()
|
||||
// .AddCapInstrumentation()
|
||||
// .AddZipkinExporter()
|
||||
// );
|
||||
|
||||
services.Scan(s =>
|
||||
s.FromAssemblies(AppDomain.CurrentDomain.GetAssemblies())
|
||||
.AddClasses(c => c.AssignableTo(typeof(ICapSubscribe)))
|
||||
.AsImplementedInterfaces()
|
||||
.WithScopedLifetime());
|
||||
|
||||
return services;
|
||||
}
|
||||
}
|
||||
63
src/BuildingBlocks/Caching/CachingBehavior.cs
Normal file
63
src/BuildingBlocks/Caching/CachingBehavior.cs
Normal file
@ -0,0 +1,63 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using EasyCaching.Core;
|
||||
using MediatR;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace BuildingBlocks.Caching
|
||||
{
|
||||
public class CachingBehavior<TRequest, TResponse> : IPipelineBehavior<TRequest, TResponse>
|
||||
where TRequest : notnull, IRequest<TResponse>
|
||||
where TResponse : notnull
|
||||
{
|
||||
private readonly ILogger<CachingBehavior<TRequest, TResponse>> _logger;
|
||||
private readonly IEasyCachingProvider _cachingProvider;
|
||||
private readonly ICacheRequest _cacheRequest;
|
||||
private readonly int defaultCacheExpirationInHours = 1;
|
||||
|
||||
public CachingBehavior(IEasyCachingProviderFactory cachingFactory,
|
||||
ILogger<CachingBehavior<TRequest, TResponse>> logger,
|
||||
ICacheRequest cacheRequest)
|
||||
{
|
||||
_logger = logger;
|
||||
_cachingProvider = cachingFactory.GetCachingProvider("mem");
|
||||
_cacheRequest = cacheRequest;
|
||||
}
|
||||
|
||||
|
||||
public async Task<TResponse> Handle(TRequest request, CancellationToken cancellationToken,
|
||||
RequestHandlerDelegate<TResponse> next)
|
||||
{
|
||||
|
||||
if (request is not ICacheRequest || _cacheRequest == null)
|
||||
{
|
||||
// No cache request found, so just continue through the pipeline
|
||||
return await next();
|
||||
}
|
||||
|
||||
var cacheKey = _cacheRequest.CacheKey;
|
||||
var cachedResponse = await _cachingProvider.GetAsync<TResponse>(cacheKey);
|
||||
if (cachedResponse.Value != null)
|
||||
{
|
||||
_logger.LogDebug("Response retrieved {TRequest} from cache. CacheKey: {CacheKey}",
|
||||
typeof(TRequest).FullName, cacheKey);
|
||||
return cachedResponse.Value;
|
||||
}
|
||||
|
||||
var response = await next();
|
||||
|
||||
var expirationTime = _cacheRequest.AbsoluteExpirationRelativeToNow ??
|
||||
DateTime.Now.AddHours(defaultCacheExpirationInHours);
|
||||
|
||||
await _cachingProvider.SetAsync(cacheKey, response, expirationTime.TimeOfDay);
|
||||
|
||||
_logger.LogDebug("Caching response for {TRequest} with cache key: {CacheKey}", typeof(TRequest).FullName,
|
||||
cacheKey);
|
||||
|
||||
return response;
|
||||
}
|
||||
}
|
||||
}
|
||||
29
src/BuildingBlocks/Caching/Extensions.cs
Normal file
29
src/BuildingBlocks/Caching/Extensions.cs
Normal file
@ -0,0 +1,29 @@
|
||||
using System.Reflection;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
namespace BuildingBlocks.Caching;
|
||||
|
||||
public static class Extensions
|
||||
{
|
||||
public static IServiceCollection AddCachingRequest(this IServiceCollection services,
|
||||
IList<Assembly> assembliesToScan, ServiceLifetime lifetime = ServiceLifetime.Transient)
|
||||
{
|
||||
// ICacheRequest discovery and registration
|
||||
services.Scan(scan => scan
|
||||
.FromAssemblies(assembliesToScan ?? AppDomain.CurrentDomain.GetAssemblies())
|
||||
.AddClasses(classes => classes.AssignableTo(typeof(ICacheRequest)),
|
||||
false)
|
||||
.AsImplementedInterfaces()
|
||||
.WithLifetime(lifetime));
|
||||
|
||||
// IInvalidateCacheRequest discovery and registration
|
||||
services.Scan(scan => scan
|
||||
.FromAssemblies(assembliesToScan ?? AppDomain.CurrentDomain.GetAssemblies())
|
||||
.AddClasses(classes => classes.AssignableTo(typeof(IInvalidateCacheRequest)),
|
||||
false)
|
||||
.AsImplementedInterfaces()
|
||||
.WithLifetime(lifetime));
|
||||
|
||||
return services;
|
||||
}
|
||||
}
|
||||
9
src/BuildingBlocks/Caching/ICacheRequest.cs
Normal file
9
src/BuildingBlocks/Caching/ICacheRequest.cs
Normal file
@ -0,0 +1,9 @@
|
||||
using MediatR;
|
||||
|
||||
namespace BuildingBlocks.Caching;
|
||||
|
||||
public interface ICacheRequest
|
||||
{
|
||||
string CacheKey { get; }
|
||||
DateTime? AbsoluteExpirationRelativeToNow { get; }
|
||||
}
|
||||
11
src/BuildingBlocks/Caching/IInvalidateCacheRequest.cs
Normal file
11
src/BuildingBlocks/Caching/IInvalidateCacheRequest.cs
Normal file
@ -0,0 +1,11 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
using MediatR;
|
||||
|
||||
namespace BuildingBlocks.Caching
|
||||
{
|
||||
public interface IInvalidateCacheRequest
|
||||
{
|
||||
string CacheKey { get; }
|
||||
}
|
||||
}
|
||||
48
src/BuildingBlocks/Caching/InvalidateCachingBehavior.cs
Normal file
48
src/BuildingBlocks/Caching/InvalidateCachingBehavior.cs
Normal file
@ -0,0 +1,48 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using EasyCaching.Core;
|
||||
using MediatR;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace BuildingBlocks.Caching
|
||||
{
|
||||
public class InvalidateCachingBehavior<TRequest, TResponse> : IPipelineBehavior<TRequest, TResponse>
|
||||
where TRequest : notnull, IRequest<TResponse>
|
||||
where TResponse : notnull
|
||||
{
|
||||
private readonly ILogger<InvalidateCachingBehavior<TRequest, TResponse>> _logger;
|
||||
private readonly IEasyCachingProvider _cachingProvider;
|
||||
private readonly IInvalidateCacheRequest _invalidateCacheRequest;
|
||||
|
||||
|
||||
public InvalidateCachingBehavior(IEasyCachingProviderFactory cachingFactory,
|
||||
ILogger<InvalidateCachingBehavior<TRequest, TResponse>> logger,
|
||||
IInvalidateCacheRequest invalidateCacheRequest)
|
||||
{
|
||||
_logger = logger;
|
||||
_cachingProvider = cachingFactory.GetCachingProvider("mem");
|
||||
_invalidateCacheRequest = invalidateCacheRequest;
|
||||
}
|
||||
|
||||
public async Task<TResponse> Handle(TRequest request, CancellationToken cancellationToken,
|
||||
RequestHandlerDelegate<TResponse> next)
|
||||
{
|
||||
if (request is not IInvalidateCacheRequest || _invalidateCacheRequest == null)
|
||||
{
|
||||
// No cache request found, so just continue through the pipeline
|
||||
return await next();
|
||||
}
|
||||
|
||||
var cacheKey = _invalidateCacheRequest.CacheKey;
|
||||
var response = await next();
|
||||
|
||||
await _cachingProvider.RemoveAsync(cacheKey);
|
||||
|
||||
_logger.LogDebug("Cache data with cache key: {CacheKey} removed.", cacheKey);
|
||||
|
||||
return response;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,8 @@
|
||||
using BuildingBlocks.Domain.Event;
|
||||
|
||||
namespace BuildingBlocks.Contracts.EventBus.Messages;
|
||||
|
||||
public record FlightCreated(string FlightNumber) : IIntegrationEvent;
|
||||
public record FlightUpdated(string FlightNumber) : IIntegrationEvent;
|
||||
public record AircraftCreated(long Id) : IIntegrationEvent;
|
||||
public record AirportCreated(long Id) : IIntegrationEvent;
|
||||
@ -0,0 +1,5 @@
|
||||
using BuildingBlocks.Domain.Event;
|
||||
|
||||
namespace BuildingBlocks.Contracts.EventBus.Messages;
|
||||
|
||||
public record UserCreated(long Id, string Name, string PassportNumber) : IIntegrationEvent;
|
||||
@ -0,0 +1,6 @@
|
||||
namespace BuildingBlocks.Contracts.EventBus.Messages;
|
||||
|
||||
public class PassengerContracts
|
||||
{
|
||||
|
||||
}
|
||||
@ -0,0 +1,5 @@
|
||||
using BuildingBlocks.Domain.Event;
|
||||
|
||||
namespace BuildingBlocks.Contracts.EventBus.Messages;
|
||||
|
||||
public record BookingCreated(long Id) : IIntegrationEvent;
|
||||
87
src/BuildingBlocks/Contracts/Grpc/FlightGrpcContracts.cs
Normal file
87
src/BuildingBlocks/Contracts/Grpc/FlightGrpcContracts.cs
Normal file
@ -0,0 +1,87 @@
|
||||
using MagicOnion;
|
||||
using MessagePack;
|
||||
|
||||
namespace BuildingBlocks.Contracts.Grpc;
|
||||
|
||||
|
||||
public interface IFlightGrpcService : IService<IFlightGrpcService>
|
||||
{
|
||||
UnaryResult<FlightResponseDto> GetById(long id);
|
||||
UnaryResult<IEnumerable<SeatResponseDto>> GetAvailableSeats(long flightId);
|
||||
UnaryResult<FlightResponseDto> ReserveSeat(ReserveSeatRequestDto request);
|
||||
}
|
||||
|
||||
|
||||
[MessagePackObject]
|
||||
public class ReserveSeatRequestDto
|
||||
{
|
||||
[Key(0)]
|
||||
public long FlightId { get; set; }
|
||||
[Key(1)]
|
||||
public string SeatNumber { get; set; }
|
||||
}
|
||||
|
||||
[MessagePackObject]
|
||||
public record SeatResponseDto
|
||||
{
|
||||
[Key(0)]
|
||||
public long Id { get; set; }
|
||||
[Key(1)]
|
||||
public string SeatNumber { get; init; }
|
||||
[Key(2)]
|
||||
public SeatType Type { get; init; }
|
||||
[Key(3)]
|
||||
public SeatClass Class { get; init; }
|
||||
[Key(4)]
|
||||
public long FlightId { get; init; }
|
||||
}
|
||||
|
||||
[MessagePackObject]
|
||||
public record FlightResponseDto
|
||||
{
|
||||
[Key(0)]
|
||||
public long Id { get; init; }
|
||||
[Key(1)]
|
||||
public string FlightNumber { get; init; }
|
||||
[Key(2)]
|
||||
public long AircraftId { get; init; }
|
||||
[Key(3)]
|
||||
public long DepartureAirportId { get; init; }
|
||||
[Key(4)]
|
||||
public DateTime DepartureDate { get; init; }
|
||||
[Key(5)]
|
||||
public DateTime ArriveDate { get; init; }
|
||||
[Key(6)]
|
||||
public long ArriveAirportId { get; init; }
|
||||
[Key(7)]
|
||||
public decimal DurationMinutes { get; init; }
|
||||
[Key(8)]
|
||||
public DateTime FlightDate { get; init; }
|
||||
[Key(9)]
|
||||
public FlightStatus Status { get; init; }
|
||||
[Key(10)]
|
||||
public decimal Price { get; init; }
|
||||
}
|
||||
|
||||
public enum FlightStatus
|
||||
{
|
||||
Flying = 1,
|
||||
Delay = 2,
|
||||
Canceled = 3,
|
||||
Completed = 4
|
||||
}
|
||||
|
||||
public enum SeatType
|
||||
{
|
||||
Window,
|
||||
Middle,
|
||||
Aisle
|
||||
}
|
||||
|
||||
public enum SeatClass
|
||||
{
|
||||
FirstClass,
|
||||
Business,
|
||||
Economy
|
||||
}
|
||||
|
||||
35
src/BuildingBlocks/Contracts/Grpc/PassengerGrpcContracts.cs
Normal file
35
src/BuildingBlocks/Contracts/Grpc/PassengerGrpcContracts.cs
Normal file
@ -0,0 +1,35 @@
|
||||
using MagicOnion;
|
||||
using MessagePack;
|
||||
|
||||
namespace BuildingBlocks.Contracts.Grpc;
|
||||
|
||||
public interface IPassengerGrpcService : IService<IPassengerGrpcService>
|
||||
{
|
||||
UnaryResult<PassengerResponseDto> GetById(long id);
|
||||
}
|
||||
|
||||
|
||||
[MessagePackObject]
|
||||
public class PassengerResponseDto
|
||||
{
|
||||
[Key(0)]
|
||||
public long Id { get; init; }
|
||||
[Key(1)]
|
||||
public string Name { get; init; }
|
||||
[Key(2)]
|
||||
public string PassportNumber { get; init; }
|
||||
[Key(3)]
|
||||
public PassengerType PassengerType { get; init; }
|
||||
[Key(4)]
|
||||
public int Age { get; init; }
|
||||
[Key(5)]
|
||||
public string Email { get; init; }
|
||||
}
|
||||
|
||||
public enum PassengerType
|
||||
{
|
||||
Male,
|
||||
Female,
|
||||
Baby,
|
||||
Unknown
|
||||
}
|
||||
128
src/BuildingBlocks/Domain/BusPublisher.cs
Normal file
128
src/BuildingBlocks/Domain/BusPublisher.cs
Normal file
@ -0,0 +1,128 @@
|
||||
using System.Security.Claims;
|
||||
using BuildingBlocks.Domain.Event;
|
||||
using BuildingBlocks.Web;
|
||||
using MassTransit;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace BuildingBlocks.Domain;
|
||||
|
||||
public sealed class BusPublisher : IBusPublisher
|
||||
{
|
||||
private readonly IEventMapper _eventMapper;
|
||||
private readonly ILogger<BusPublisher> _logger;
|
||||
private readonly IPublishEndpoint _publishEndpoint;
|
||||
private readonly IHttpContextAccessor _httpContextAccessor;
|
||||
private readonly IServiceScopeFactory _serviceScopeFactory;
|
||||
|
||||
public BusPublisher(IServiceScopeFactory serviceScopeFactory,
|
||||
IEventMapper eventMapper,
|
||||
ILogger<BusPublisher> logger,
|
||||
IPublishEndpoint publishEndpoint,
|
||||
IHttpContextAccessor httpContextAccessor)
|
||||
{
|
||||
_serviceScopeFactory = serviceScopeFactory;
|
||||
_eventMapper = eventMapper;
|
||||
_logger = logger;
|
||||
_publishEndpoint = publishEndpoint;
|
||||
_httpContextAccessor = httpContextAccessor;
|
||||
}
|
||||
|
||||
public async Task SendAsync(IDomainEvent domainEvent,
|
||||
CancellationToken cancellationToken = default) => await SendAsync(new[] { domainEvent }, cancellationToken);
|
||||
|
||||
public async Task SendAsync(IReadOnlyList<IDomainEvent> domainEvents, CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (domainEvents is null) return;
|
||||
|
||||
_logger.LogTrace("Processing integration events start...");
|
||||
|
||||
var integrationEvents = await MapDomainEventToIntegrationEventAsync(domainEvents).ConfigureAwait(false);
|
||||
|
||||
if (!integrationEvents.Any()) return;
|
||||
|
||||
foreach (var integrationEvent in integrationEvents)
|
||||
{
|
||||
await _publishEndpoint.Publish((object)integrationEvent, context =>
|
||||
{
|
||||
context.CorrelationId = new Guid(_httpContextAccessor.HttpContext.GetCorrelationId());
|
||||
context.Headers.Set("UserId",
|
||||
_httpContextAccessor?.HttpContext?.User?.FindFirstValue(ClaimTypes.NameIdentifier));
|
||||
context.Headers.Set("UserName",
|
||||
_httpContextAccessor?.HttpContext?.User?.FindFirstValue(ClaimTypes.Name));
|
||||
}, cancellationToken);
|
||||
|
||||
_logger.LogTrace("Publish a message with ID: {Id}", integrationEvent?.EventId);
|
||||
}
|
||||
|
||||
_logger.LogTrace("Processing integration events done...");
|
||||
}
|
||||
|
||||
|
||||
|
||||
public async Task SendAsync(IIntegrationEvent integrationEvent,
|
||||
CancellationToken cancellationToken = default) => await SendAsync(new[] { integrationEvent }, cancellationToken);
|
||||
|
||||
public async Task SendAsync(IReadOnlyList<IIntegrationEvent> integrationEvents, CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (integrationEvents is null) return;
|
||||
|
||||
_logger.LogTrace("Processing integration events start...");
|
||||
|
||||
foreach (var integrationEvent in integrationEvents)
|
||||
{
|
||||
await _publishEndpoint.Publish((object)integrationEvent, context =>
|
||||
{
|
||||
context.CorrelationId = new Guid(_httpContextAccessor.HttpContext.GetCorrelationId());
|
||||
context.Headers.Set("UserId",
|
||||
_httpContextAccessor?.HttpContext?.User?.FindFirstValue(ClaimTypes.NameIdentifier));
|
||||
context.Headers.Set("UserName",
|
||||
_httpContextAccessor?.HttpContext?.User?.FindFirstValue(ClaimTypes.Name));
|
||||
}, cancellationToken);
|
||||
|
||||
_logger.LogTrace("Publish a message with ID: {Id}", integrationEvent?.EventId);
|
||||
}
|
||||
|
||||
_logger.LogTrace("Processing integration events done...");
|
||||
}
|
||||
|
||||
private Task<IReadOnlyList<IIntegrationEvent>> MapDomainEventToIntegrationEventAsync(
|
||||
IReadOnlyList<IDomainEvent> events)
|
||||
{
|
||||
var wrappedIntegrationEvents = GetWrappedIntegrationEvents(events.ToList())?.ToList();
|
||||
if (wrappedIntegrationEvents?.Count > 0)
|
||||
return Task.FromResult<IReadOnlyList<IIntegrationEvent>>(wrappedIntegrationEvents);
|
||||
|
||||
var integrationEvents = new List<IIntegrationEvent>();
|
||||
using var scope = _serviceScopeFactory.CreateScope();
|
||||
foreach (var @event in events)
|
||||
{
|
||||
var eventType = @event.GetType();
|
||||
_logger.LogTrace($"Handling domain event: {eventType.Name}");
|
||||
|
||||
var integrationEvent = _eventMapper.Map(@event);
|
||||
|
||||
if (integrationEvent is null) continue;
|
||||
|
||||
integrationEvents.Add(integrationEvent);
|
||||
}
|
||||
|
||||
return Task.FromResult<IReadOnlyList<IIntegrationEvent>>(integrationEvents);
|
||||
}
|
||||
|
||||
private IEnumerable<IIntegrationEvent> GetWrappedIntegrationEvents(IReadOnlyList<IDomainEvent> domainEvents)
|
||||
{
|
||||
foreach (var domainEvent in domainEvents.Where(x =>
|
||||
x is IHaveIntegrationEvent))
|
||||
{
|
||||
var genericType = typeof(IntegrationEventWrapper<>)
|
||||
.MakeGenericType(domainEvent.GetType());
|
||||
|
||||
var domainNotificationEvent = (IIntegrationEvent)Activator
|
||||
.CreateInstance(genericType, domainEvent);
|
||||
|
||||
yield return domainNotificationEvent;
|
||||
}
|
||||
}
|
||||
}
|
||||
8
src/BuildingBlocks/Domain/Event/EventType.cs
Normal file
8
src/BuildingBlocks/Domain/Event/EventType.cs
Normal file
@ -0,0 +1,8 @@
|
||||
namespace BuildingBlocks.Domain.Event;
|
||||
|
||||
[Flags]
|
||||
public enum EventType
|
||||
{
|
||||
IntegrationEvent = 1,
|
||||
DomainEvent = 2,
|
||||
}
|
||||
8
src/BuildingBlocks/Domain/Event/IDomainEvent.cs
Normal file
8
src/BuildingBlocks/Domain/Event/IDomainEvent.cs
Normal file
@ -0,0 +1,8 @@
|
||||
using MediatR;
|
||||
|
||||
namespace BuildingBlocks.Domain.Event;
|
||||
|
||||
public interface IDomainEvent : IEvent
|
||||
{
|
||||
|
||||
}
|
||||
11
src/BuildingBlocks/Domain/Event/IEvent.cs
Normal file
11
src/BuildingBlocks/Domain/Event/IEvent.cs
Normal file
@ -0,0 +1,11 @@
|
||||
using MassTransit;
|
||||
using MediatR;
|
||||
|
||||
namespace BuildingBlocks.Domain.Event;
|
||||
|
||||
public interface IEvent : INotification
|
||||
{
|
||||
Guid EventId => Guid.NewGuid();
|
||||
public DateTime OccurredOn => DateTime.Now;
|
||||
public string EventType => GetType().AssemblyQualifiedName;
|
||||
}
|
||||
5
src/BuildingBlocks/Domain/Event/IHaveIntegrationEvent.cs
Normal file
5
src/BuildingBlocks/Domain/Event/IHaveIntegrationEvent.cs
Normal file
@ -0,0 +1,5 @@
|
||||
namespace BuildingBlocks.Domain.Event;
|
||||
|
||||
public interface IHaveIntegrationEvent
|
||||
{
|
||||
}
|
||||
9
src/BuildingBlocks/Domain/Event/IIntegrationEvent.cs
Normal file
9
src/BuildingBlocks/Domain/Event/IIntegrationEvent.cs
Normal file
@ -0,0 +1,9 @@
|
||||
using MassTransit;
|
||||
using MassTransit.Topology;
|
||||
|
||||
namespace BuildingBlocks.Domain.Event;
|
||||
|
||||
[ExcludeFromTopology]
|
||||
public interface IIntegrationEvent : IEvent
|
||||
{
|
||||
}
|
||||
12
src/BuildingBlocks/Domain/IBusPublisher.cs
Normal file
12
src/BuildingBlocks/Domain/IBusPublisher.cs
Normal file
@ -0,0 +1,12 @@
|
||||
using BuildingBlocks.Domain.Event;
|
||||
|
||||
namespace BuildingBlocks.Domain;
|
||||
|
||||
public interface IBusPublisher
|
||||
{
|
||||
public Task SendAsync(IReadOnlyList<IDomainEvent> domainEvents, CancellationToken cancellationToken = default);
|
||||
public Task SendAsync(IDomainEvent domainEvent, CancellationToken cancellationToken = default);
|
||||
|
||||
public Task SendAsync(IIntegrationEvent integrationEvent, CancellationToken cancellationToken = default);
|
||||
public Task SendAsync(IReadOnlyList<IIntegrationEvent> integrationEvents, CancellationToken cancellationToken = default);
|
||||
}
|
||||
9
src/BuildingBlocks/Domain/IEventMapper.cs
Normal file
9
src/BuildingBlocks/Domain/IEventMapper.cs
Normal file
@ -0,0 +1,9 @@
|
||||
using BuildingBlocks.Domain.Event;
|
||||
|
||||
namespace BuildingBlocks.Domain;
|
||||
|
||||
public interface IEventMapper
|
||||
{
|
||||
IIntegrationEvent Map(IDomainEvent @event);
|
||||
IEnumerable<IIntegrationEvent> MapAll(IEnumerable<IDomainEvent> events);
|
||||
}
|
||||
6
src/BuildingBlocks/Domain/IntegrationEventWrapper.cs
Normal file
6
src/BuildingBlocks/Domain/IntegrationEventWrapper.cs
Normal file
@ -0,0 +1,6 @@
|
||||
using BuildingBlocks.Domain.Event;
|
||||
|
||||
namespace BuildingBlocks.Domain;
|
||||
|
||||
public record IntegrationEventWrapper<TDomainEventType>(TDomainEventType DomainEvent) : IIntegrationEvent
|
||||
where TDomainEventType : IDomainEvent;
|
||||
34
src/BuildingBlocks/Domain/Model/Aggregate.cs
Normal file
34
src/BuildingBlocks/Domain/Model/Aggregate.cs
Normal file
@ -0,0 +1,34 @@
|
||||
using BuildingBlocks.Domain.Event;
|
||||
|
||||
namespace BuildingBlocks.Domain.Model
|
||||
{
|
||||
public abstract class Aggregate : Aggregate<long>
|
||||
{
|
||||
}
|
||||
|
||||
public abstract class Aggregate<TId> : Auditable, IAggregate<TId>
|
||||
{
|
||||
private readonly List<IDomainEvent> _domainEvents = new();
|
||||
public IReadOnlyList<IDomainEvent> DomainEvents => _domainEvents.AsReadOnly();
|
||||
|
||||
public void AddDomainEvent(IDomainEvent domainEvent)
|
||||
{
|
||||
_domainEvents.Add(domainEvent);
|
||||
}
|
||||
|
||||
public IEvent[] ClearDomainEvents()
|
||||
{
|
||||
IEvent[] dequeuedEvents = _domainEvents.ToArray();
|
||||
|
||||
_domainEvents.Clear();
|
||||
|
||||
return dequeuedEvents;
|
||||
}
|
||||
|
||||
public virtual void When(object @event) { }
|
||||
|
||||
public TId Id { get; protected set; }
|
||||
public long Version { get; protected set; } = -1;
|
||||
public bool IsDeleted { get; protected set; }
|
||||
}
|
||||
}
|
||||
9
src/BuildingBlocks/Domain/Model/Entity.cs
Normal file
9
src/BuildingBlocks/Domain/Model/Entity.cs
Normal file
@ -0,0 +1,9 @@
|
||||
namespace BuildingBlocks.Domain.Model;
|
||||
|
||||
public abstract class Auditable : IAuditable
|
||||
{
|
||||
public DateTime? CreatedAt { get; set; }
|
||||
public long? CreatedBy { get; set; }
|
||||
public DateTime? LastModified { get; set; }
|
||||
public long? LastModifiedBy { get; set; }
|
||||
}
|
||||
20
src/BuildingBlocks/Domain/Model/IAggregate.cs
Normal file
20
src/BuildingBlocks/Domain/Model/IAggregate.cs
Normal file
@ -0,0 +1,20 @@
|
||||
using BuildingBlocks.Domain.Event;
|
||||
using BuildingBlocks.EventStoreDB.Events;
|
||||
|
||||
namespace BuildingBlocks.Domain.Model
|
||||
{
|
||||
public interface IAggregate : IProjection, IAuditable
|
||||
{
|
||||
IReadOnlyList<IDomainEvent> DomainEvents { get; }
|
||||
IEvent[] ClearDomainEvents();
|
||||
long Version { get; }
|
||||
public bool IsDeleted { get; }
|
||||
}
|
||||
|
||||
public interface IAggregate<out T> : IAggregate
|
||||
{
|
||||
T Id { get; }
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
11
src/BuildingBlocks/Domain/Model/IAuditable.cs
Normal file
11
src/BuildingBlocks/Domain/Model/IAuditable.cs
Normal file
@ -0,0 +1,11 @@
|
||||
namespace BuildingBlocks.Domain.Model;
|
||||
|
||||
public interface IAuditable
|
||||
{
|
||||
public DateTime? CreatedAt { get; set; }
|
||||
public long? CreatedBy { get; set; }
|
||||
public DateTime? LastModified { get; set; }
|
||||
public long? LastModifiedBy { get; set; }
|
||||
}
|
||||
|
||||
|
||||
7
src/BuildingBlocks/Domain/Model/IEntity.cs
Normal file
7
src/BuildingBlocks/Domain/Model/IEntity.cs
Normal file
@ -0,0 +1,7 @@
|
||||
namespace BuildingBlocks.Domain.Model;
|
||||
|
||||
public interface IEntity<out TId>
|
||||
{
|
||||
TId Id { get; }
|
||||
public bool IsDeleted { get; }
|
||||
}
|
||||
120
src/BuildingBlocks/EFCore/AppDbContextBase.cs
Normal file
120
src/BuildingBlocks/EFCore/AppDbContextBase.cs
Normal file
@ -0,0 +1,120 @@
|
||||
using System.Collections.Immutable;
|
||||
using System.Data;
|
||||
using System.Reflection;
|
||||
using System.Security.Claims;
|
||||
using BuildingBlocks.Domain.Event;
|
||||
using BuildingBlocks.Domain.Model;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Storage;
|
||||
|
||||
namespace BuildingBlocks.EFCore;
|
||||
|
||||
public abstract class AppDbContextBase : DbContext, IDbContext
|
||||
{
|
||||
private readonly IHttpContextAccessor _httpContextAccessor;
|
||||
|
||||
private IDbContextTransaction _currentTransaction;
|
||||
|
||||
protected AppDbContextBase(DbContextOptions options, IHttpContextAccessor httpContextAccessor) : base(options)
|
||||
{
|
||||
_httpContextAccessor = httpContextAccessor;
|
||||
}
|
||||
|
||||
protected override void OnModelCreating(ModelBuilder builder)
|
||||
{
|
||||
builder.ApplyConfigurationsFromAssembly(Assembly.GetExecutingAssembly());
|
||||
base.OnModelCreating(builder);
|
||||
}
|
||||
|
||||
public async Task BeginTransactionAsync(IsolationLevel isolationLevel,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
_currentTransaction ??= await Database.BeginTransactionAsync(isolationLevel, cancellationToken);
|
||||
}
|
||||
|
||||
public async Task CommitTransactionAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
try
|
||||
{
|
||||
await SaveChangesAsync(cancellationToken);
|
||||
await _currentTransaction?.CommitAsync(cancellationToken)!;
|
||||
}
|
||||
catch(System.Exception ex)
|
||||
{
|
||||
await RollbackTransactionAsync(cancellationToken);
|
||||
throw;
|
||||
}
|
||||
finally
|
||||
{
|
||||
_currentTransaction?.Dispose();
|
||||
_currentTransaction = null;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task RollbackTransactionAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
try
|
||||
{
|
||||
await _currentTransaction?.RollbackAsync(cancellationToken)!;
|
||||
}
|
||||
finally
|
||||
{
|
||||
_currentTransaction?.Dispose();
|
||||
_currentTransaction = null;
|
||||
}
|
||||
}
|
||||
|
||||
public override Task<int> SaveChangesAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
OnBeforeSaving();
|
||||
return base.SaveChangesAsync(cancellationToken);
|
||||
}
|
||||
|
||||
public IReadOnlyList<IDomainEvent> GetDomainEvents()
|
||||
{
|
||||
var domainEntities = ChangeTracker
|
||||
.Entries<IAggregate>()
|
||||
.Where(x => x.Entity.DomainEvents.Any())
|
||||
.Select(x => x.Entity)
|
||||
.ToList();
|
||||
|
||||
var domainEvents = domainEntities
|
||||
.SelectMany(x => x.DomainEvents)
|
||||
.ToImmutableList();
|
||||
|
||||
domainEntities.ForEach(entity => entity.ClearDomainEvents());
|
||||
|
||||
return domainEvents.ToImmutableList();
|
||||
}
|
||||
|
||||
// https://www.meziantou.net/entity-framework-core-generate-tracking-columns.htm
|
||||
// https://www.meziantou.net/entity-framework-core-soft-delete-using-query-filters.htm
|
||||
private void OnBeforeSaving()
|
||||
{
|
||||
var nameIdentifier = _httpContextAccessor?.HttpContext?.User?.FindFirstValue(ClaimTypes.NameIdentifier);
|
||||
|
||||
long.TryParse(nameIdentifier, out var userId);
|
||||
|
||||
foreach (var entry in ChangeTracker.Entries<IAggregate>())
|
||||
{
|
||||
bool isAuditable = entry.Entity.GetType().IsAssignableTo(typeof(IAggregate));
|
||||
|
||||
if (isAuditable)
|
||||
{
|
||||
switch (entry.State)
|
||||
{
|
||||
case EntityState.Added:
|
||||
entry.Entity.CreatedBy = userId;
|
||||
entry.Entity.CreatedAt = DateTime.Now;
|
||||
break;
|
||||
|
||||
case EntityState.Modified:
|
||||
entry.Entity.LastModifiedBy = userId;
|
||||
entry.Entity.LastModified = DateTime.Now;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
6
src/BuildingBlocks/EFCore/DatabaseOptions.cs
Normal file
6
src/BuildingBlocks/EFCore/DatabaseOptions.cs
Normal file
@ -0,0 +1,6 @@
|
||||
namespace BuildingBlocks.EFCore;
|
||||
|
||||
public class DatabaseOptions
|
||||
{
|
||||
public string ConnectionString { get; set; }
|
||||
}
|
||||
62
src/BuildingBlocks/EFCore/DesignTimeDbContextFactoryBase.cs
Normal file
62
src/BuildingBlocks/EFCore/DesignTimeDbContextFactoryBase.cs
Normal file
@ -0,0 +1,62 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Design;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
|
||||
namespace BuildingBlocks.EFCore
|
||||
{
|
||||
public abstract class DesignTimeDbContextFactoryBase<TContext> : IDesignTimeDbContextFactory<TContext> where TContext : DbContext
|
||||
{
|
||||
public TContext CreateDbContext(string[] args)
|
||||
{
|
||||
return Create(Directory.GetCurrentDirectory(), Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT"));
|
||||
}
|
||||
|
||||
protected abstract TContext CreateNewInstance(DbContextOptions<TContext> options);
|
||||
|
||||
public TContext Create()
|
||||
{
|
||||
var environmentName = Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT");
|
||||
var basePath = AppContext.BaseDirectory;
|
||||
return Create(basePath, environmentName);
|
||||
}
|
||||
|
||||
private TContext Create(string basePath, string environmentName)
|
||||
{
|
||||
var builder = new ConfigurationBuilder()
|
||||
.SetBasePath(basePath)
|
||||
.AddJsonFile("appsettings.json")
|
||||
.AddJsonFile($"appsettings.{environmentName}.json", true)
|
||||
.AddEnvironmentVariables();
|
||||
|
||||
var config = builder.Build();
|
||||
|
||||
var connstr = config.GetConnectionString("DefaultConnection");
|
||||
|
||||
if (string.IsNullOrWhiteSpace(connstr))
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
"Could not find a connection string named 'Default'.");
|
||||
}
|
||||
return Create(connstr);
|
||||
}
|
||||
|
||||
private TContext Create(string connectionString)
|
||||
{
|
||||
if (string.IsNullOrEmpty(connectionString))
|
||||
throw new ArgumentException(
|
||||
$"{nameof(connectionString)} is null or empty.",
|
||||
nameof(connectionString));
|
||||
|
||||
var optionsBuilder = new DbContextOptionsBuilder<TContext>();
|
||||
|
||||
Console.WriteLine("DesignTimeDbContextFactory.Create(string): Connection string: {0}", connectionString);
|
||||
|
||||
optionsBuilder.UseSqlServer(connectionString);
|
||||
|
||||
var options = optionsBuilder.Options;
|
||||
return CreateNewInstance(options);
|
||||
}
|
||||
}
|
||||
}
|
||||
65
src/BuildingBlocks/EFCore/EfIdentityTxBehavior.cs
Normal file
65
src/BuildingBlocks/EFCore/EfIdentityTxBehavior.cs
Normal file
@ -0,0 +1,65 @@
|
||||
using System.Data;
|
||||
using System.Text.Json;
|
||||
using MediatR;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace BuildingBlocks.EFCore;
|
||||
|
||||
public class EfIdentityTxBehavior<TRequest, TResponse> : IPipelineBehavior<TRequest, TResponse>
|
||||
where TRequest : notnull, IRequest<TResponse>
|
||||
where TResponse : notnull
|
||||
{
|
||||
private readonly ILogger<EfTxBehavior<TRequest, TResponse>> _logger;
|
||||
private readonly IDbContext _dbContextBase;
|
||||
|
||||
public EfIdentityTxBehavior(
|
||||
ILogger<EfTxBehavior<TRequest, TResponse>> logger,
|
||||
IDbContext dbContextBase)
|
||||
{
|
||||
_logger = logger;
|
||||
_dbContextBase = dbContextBase;
|
||||
}
|
||||
|
||||
public async Task<TResponse> Handle(
|
||||
TRequest request,
|
||||
CancellationToken cancellationToken,
|
||||
RequestHandlerDelegate<TResponse> next)
|
||||
{
|
||||
_logger.LogInformation(
|
||||
"{Prefix} Handled command {MediatrRequest}",
|
||||
nameof(EfTxBehavior<TRequest, TResponse>),
|
||||
typeof(TRequest).FullName);
|
||||
|
||||
_logger.LogDebug(
|
||||
"{Prefix} Handled command {MediatrRequest} with content {RequestContent}",
|
||||
nameof(EfTxBehavior<TRequest, TResponse>),
|
||||
typeof(TRequest).FullName,
|
||||
JsonSerializer.Serialize(request));
|
||||
|
||||
_logger.LogInformation(
|
||||
"{Prefix} Open the transaction for {MediatrRequest}",
|
||||
nameof(EfTxBehavior<TRequest, TResponse>),
|
||||
typeof(TRequest).FullName);
|
||||
|
||||
await _dbContextBase.BeginTransactionAsync(IsolationLevel.ReadCommitted, cancellationToken);
|
||||
|
||||
try
|
||||
{
|
||||
var response = await next();
|
||||
|
||||
_logger.LogInformation(
|
||||
"{Prefix} Executed the {MediatrRequest} request",
|
||||
nameof(EfTxBehavior<TRequest, TResponse>),
|
||||
typeof(TRequest).FullName);
|
||||
|
||||
await _dbContextBase.CommitTransactionAsync(cancellationToken);
|
||||
|
||||
return response;
|
||||
}
|
||||
catch
|
||||
{
|
||||
await _dbContextBase.RollbackTransactionAsync(cancellationToken);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
}
|
||||
73
src/BuildingBlocks/EFCore/EfTxBehavior.cs
Normal file
73
src/BuildingBlocks/EFCore/EfTxBehavior.cs
Normal file
@ -0,0 +1,73 @@
|
||||
using System.Data;
|
||||
using System.Text.Json;
|
||||
using BuildingBlocks.Domain;
|
||||
using MediatR;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace BuildingBlocks.EFCore;
|
||||
|
||||
public class EfTxBehavior<TRequest, TResponse> : IPipelineBehavior<TRequest, TResponse>
|
||||
where TRequest : notnull, IRequest<TResponse>
|
||||
where TResponse : notnull
|
||||
{
|
||||
private readonly ILogger<EfTxBehavior<TRequest, TResponse>> _logger;
|
||||
private readonly IDbContext _dbContextBase;
|
||||
private readonly IBusPublisher _busPublisher;
|
||||
|
||||
public EfTxBehavior(
|
||||
ILogger<EfTxBehavior<TRequest, TResponse>> logger,
|
||||
IDbContext dbContextBase,
|
||||
IBusPublisher busPublisher)
|
||||
{
|
||||
_logger = logger;
|
||||
_dbContextBase = dbContextBase;
|
||||
_busPublisher = busPublisher;
|
||||
}
|
||||
|
||||
public async Task<TResponse> Handle(
|
||||
TRequest request,
|
||||
CancellationToken cancellationToken,
|
||||
RequestHandlerDelegate<TResponse> next)
|
||||
{
|
||||
_logger.LogInformation(
|
||||
"{Prefix} Handled command {MediatrRequest}",
|
||||
nameof(EfTxBehavior<TRequest, TResponse>),
|
||||
typeof(TRequest).FullName);
|
||||
|
||||
_logger.LogDebug(
|
||||
"{Prefix} Handled command {MediatrRequest} with content {RequestContent}",
|
||||
nameof(EfTxBehavior<TRequest, TResponse>),
|
||||
typeof(TRequest).FullName,
|
||||
JsonSerializer.Serialize(request));
|
||||
|
||||
_logger.LogInformation(
|
||||
"{Prefix} Open the transaction for {MediatrRequest}",
|
||||
nameof(EfTxBehavior<TRequest, TResponse>),
|
||||
typeof(TRequest).FullName);
|
||||
|
||||
await _dbContextBase.BeginTransactionAsync(IsolationLevel.ReadCommitted, cancellationToken);
|
||||
|
||||
try
|
||||
{
|
||||
var response = await next();
|
||||
|
||||
_logger.LogInformation(
|
||||
"{Prefix} Executed the {MediatrRequest} request",
|
||||
nameof(EfTxBehavior<TRequest, TResponse>),
|
||||
typeof(TRequest).FullName);
|
||||
|
||||
var domainEvents = _dbContextBase.GetDomainEvents();
|
||||
|
||||
await _busPublisher.SendAsync(domainEvents.ToArray(), cancellationToken);
|
||||
|
||||
await _dbContextBase.CommitTransactionAsync(cancellationToken);
|
||||
|
||||
return response;
|
||||
}
|
||||
catch
|
||||
{
|
||||
await _dbContextBase.RollbackTransactionAsync(cancellationToken);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
}
|
||||
25
src/BuildingBlocks/EFCore/Extensions.cs
Normal file
25
src/BuildingBlocks/EFCore/Extensions.cs
Normal file
@ -0,0 +1,25 @@
|
||||
using System.Reflection;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
namespace BuildingBlocks.EFCore;
|
||||
|
||||
public static class Extensions
|
||||
{
|
||||
public static IServiceCollection AddCustomDbContext<TContext>(
|
||||
this IServiceCollection services,
|
||||
IConfiguration configuration,
|
||||
Assembly migrationAssembly)
|
||||
where TContext : AppDbContextBase
|
||||
{
|
||||
services.AddScoped<IDbContext>(provider => provider.GetService<TContext>());
|
||||
|
||||
services.AddDbContext<TContext>(options =>
|
||||
options.UseSqlServer(
|
||||
configuration.GetConnectionString("DefaultConnection"),
|
||||
x => x.MigrationsAssembly(migrationAssembly.GetName().Name)));
|
||||
|
||||
return services;
|
||||
}
|
||||
}
|
||||
7
src/BuildingBlocks/EFCore/IDataSeeder.cs
Normal file
7
src/BuildingBlocks/EFCore/IDataSeeder.cs
Normal file
@ -0,0 +1,7 @@
|
||||
namespace BuildingBlocks.EFCore
|
||||
{
|
||||
public interface IDataSeeder
|
||||
{
|
||||
Task SeedAllAsync();
|
||||
}
|
||||
}
|
||||
16
src/BuildingBlocks/EFCore/IDbContext.cs
Normal file
16
src/BuildingBlocks/EFCore/IDbContext.cs
Normal file
@ -0,0 +1,16 @@
|
||||
using System.Data;
|
||||
using BuildingBlocks.Domain.Event;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace BuildingBlocks.EFCore;
|
||||
|
||||
public interface IDbContext
|
||||
{
|
||||
DbSet<TEntity> Set<TEntity>()
|
||||
where TEntity : class;
|
||||
IReadOnlyList<IDomainEvent> GetDomainEvents();
|
||||
Task BeginTransactionAsync(IsolationLevel isolationLevel, CancellationToken cancellationToken = default);
|
||||
Task CommitTransactionAsync(CancellationToken cancellationToken = default);
|
||||
Task RollbackTransactionAsync(CancellationToken cancellationToken = default);
|
||||
Task<int> SaveChangesAsync(CancellationToken cancellationToken = default);
|
||||
}
|
||||
@ -0,0 +1,28 @@
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace BuildingBlocks.EventStoreDB.BackgroundWorkers;
|
||||
|
||||
public class BackgroundWorker : BackgroundService
|
||||
{
|
||||
private readonly ILogger<BackgroundWorker> logger;
|
||||
private readonly Func<CancellationToken, Task> perform;
|
||||
|
||||
public BackgroundWorker(
|
||||
ILogger<BackgroundWorker> logger,
|
||||
Func<CancellationToken, Task> perform
|
||||
)
|
||||
{
|
||||
this.logger = logger;
|
||||
this.perform = perform;
|
||||
}
|
||||
|
||||
protected override Task ExecuteAsync(CancellationToken stoppingToken) =>
|
||||
Task.Run(async () =>
|
||||
{
|
||||
await Task.Yield();
|
||||
logger.LogInformation("Background worker stopped");
|
||||
await perform(stoppingToken);
|
||||
logger.LogInformation("Background worker stopped");
|
||||
}, stoppingToken);
|
||||
}
|
||||
88
src/BuildingBlocks/EventStoreDB/Config.cs
Normal file
88
src/BuildingBlocks/EventStoreDB/Config.cs
Normal file
@ -0,0 +1,88 @@
|
||||
using System.Reflection;
|
||||
using BuildingBlocks.EventStoreDB.BackgroundWorkers;
|
||||
using BuildingBlocks.EventStoreDB.Events;
|
||||
using BuildingBlocks.EventStoreDB.Projections;
|
||||
using BuildingBlocks.EventStoreDB.Repository;
|
||||
using BuildingBlocks.EventStoreDB.Subscriptions;
|
||||
using EventStore.Client;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace BuildingBlocks.EventStoreDB;
|
||||
|
||||
public class EventStoreDBConfig
|
||||
{
|
||||
public string ConnectionString { get; set; } = default!;
|
||||
}
|
||||
|
||||
public record EventStoreDBOptions(
|
||||
bool UseInternalCheckpointing = true
|
||||
);
|
||||
|
||||
public static class EventStoreDBConfigExtensions
|
||||
{
|
||||
private const string DefaultConfigKey = "EventStore";
|
||||
|
||||
public static IServiceCollection AddEventStoreDB(this IServiceCollection services, IConfiguration config,
|
||||
EventStoreDBOptions? options = null)
|
||||
{
|
||||
var eventStoreDBConfig = config.GetSection(DefaultConfigKey).Get<EventStoreDBConfig>();
|
||||
|
||||
services
|
||||
.AddSingleton(new EventStoreClient(EventStoreClientSettings.Create(eventStoreDBConfig.ConnectionString)))
|
||||
.AddScoped(typeof(IEventStoreDBRepository<>), typeof(EventStoreDBRepository<>))
|
||||
.AddTransient<EventStoreDBSubscriptionToAll, EventStoreDBSubscriptionToAll>();
|
||||
|
||||
if (options?.UseInternalCheckpointing != false)
|
||||
services.AddTransient<ISubscriptionCheckpointRepository, EventStoreDBSubscriptionCheckpointRepository>();
|
||||
|
||||
return services;
|
||||
}
|
||||
|
||||
public static IServiceCollection AddEventStoreDBSubscriptionToAll(
|
||||
this IServiceCollection services,
|
||||
EventStoreDBSubscriptionToAllOptions? subscriptionOptions = null,
|
||||
bool checkpointToEventStoreDB = true)
|
||||
{
|
||||
if (checkpointToEventStoreDB)
|
||||
services.AddTransient<ISubscriptionCheckpointRepository, EventStoreDBSubscriptionCheckpointRepository>();
|
||||
|
||||
return services.AddHostedService(serviceProvider =>
|
||||
{
|
||||
var logger =
|
||||
serviceProvider.GetRequiredService<ILogger<BackgroundWorker>>();
|
||||
|
||||
var eventStoreDBSubscriptionToAll =
|
||||
serviceProvider.GetRequiredService<EventStoreDBSubscriptionToAll>();
|
||||
|
||||
return new BackgroundWorker(
|
||||
logger,
|
||||
ct =>
|
||||
eventStoreDBSubscriptionToAll.SubscribeToAll(
|
||||
subscriptionOptions ?? new EventStoreDBSubscriptionToAllOptions(),
|
||||
ct
|
||||
)
|
||||
);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
public static IServiceCollection AddProjections(this IServiceCollection services, params Assembly[] assembliesToScan)
|
||||
{
|
||||
services.AddSingleton<IProjectionPublisher, ProjectionPublisher>();
|
||||
|
||||
RegisterProjections(services, assembliesToScan!);
|
||||
|
||||
return services;
|
||||
}
|
||||
|
||||
private static void RegisterProjections(IServiceCollection services, Assembly[] assembliesToScan)
|
||||
{
|
||||
services.Scan(scan => scan
|
||||
.FromAssemblies(assembliesToScan)
|
||||
.AddClasses(classes => classes.AssignableTo<IProjection>()) // Filter classes
|
||||
.AsImplementedInterfaces()
|
||||
.WithTransientLifetime());
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,38 @@
|
||||
using BuildingBlocks.Domain.Model;
|
||||
using BuildingBlocks.EventStoreDB.Serialization;
|
||||
using EventStore.Client;
|
||||
|
||||
namespace BuildingBlocks.EventStoreDB.Events;
|
||||
|
||||
public static class AggregateStreamExtensions
|
||||
{
|
||||
public static async Task<T?> AggregateStream<T>(
|
||||
this EventStoreClient eventStore,
|
||||
long id,
|
||||
CancellationToken cancellationToken,
|
||||
ulong? fromVersion = null
|
||||
) where T : class, IProjection
|
||||
{
|
||||
var readResult = eventStore.ReadStreamAsync(
|
||||
Direction.Forwards,
|
||||
StreamNameMapper.ToStreamId<T>(id),
|
||||
fromVersion ?? StreamPosition.Start,
|
||||
cancellationToken: cancellationToken
|
||||
);
|
||||
|
||||
// TODO: consider adding extension method for the aggregation and deserialisation
|
||||
var aggregate = (T)Activator.CreateInstance(typeof(T), true)!;
|
||||
|
||||
if (await readResult.ReadState == ReadState.StreamNotFound)
|
||||
return null;
|
||||
|
||||
await foreach (var @event in readResult)
|
||||
{
|
||||
var eventData = @event.Deserialize();
|
||||
|
||||
aggregate.When(eventData!);
|
||||
}
|
||||
|
||||
return aggregate;
|
||||
}
|
||||
}
|
||||
43
src/BuildingBlocks/EventStoreDB/Events/EventTypeMapper.cs
Normal file
43
src/BuildingBlocks/EventStoreDB/Events/EventTypeMapper.cs
Normal file
@ -0,0 +1,43 @@
|
||||
using System.Collections.Concurrent;
|
||||
using BuildingBlocks.Utils;
|
||||
|
||||
namespace BuildingBlocks.EventStoreDB.Events;
|
||||
|
||||
public class EventTypeMapper
|
||||
{
|
||||
private static readonly EventTypeMapper Instance = new();
|
||||
|
||||
private readonly ConcurrentDictionary<string, Type?> typeMap = new();
|
||||
private readonly ConcurrentDictionary<Type, string> typeNameMap = new();
|
||||
|
||||
public static void AddCustomMap<T>(string mappedEventTypeName) => AddCustomMap(typeof(T), mappedEventTypeName);
|
||||
|
||||
public static void AddCustomMap(Type eventType, string mappedEventTypeName)
|
||||
{
|
||||
Instance.typeNameMap.AddOrUpdate(eventType, mappedEventTypeName, (_, _) => mappedEventTypeName);
|
||||
Instance.typeMap.AddOrUpdate(mappedEventTypeName, eventType, (_, _) => eventType);
|
||||
}
|
||||
|
||||
public static string ToName<TEventType>() => ToName(typeof(TEventType));
|
||||
|
||||
public static string ToName(Type eventType) => Instance.typeNameMap.GetOrAdd(eventType, _ =>
|
||||
{
|
||||
var eventTypeName = eventType.FullName!.Replace(".", "_");
|
||||
|
||||
Instance.typeMap.AddOrUpdate(eventTypeName, eventType, (_, _) => eventType);
|
||||
|
||||
return eventTypeName;
|
||||
});
|
||||
|
||||
public static Type? ToType(string eventTypeName) => Instance.typeMap.GetOrAdd(eventTypeName, _ =>
|
||||
{
|
||||
var type = TypeProvider.GetFirstMatchingTypeFromCurrentDomainAssembly(eventTypeName.Replace("_", "."));
|
||||
|
||||
if (type == null)
|
||||
return null;
|
||||
|
||||
Instance.typeNameMap.AddOrUpdate(type, eventTypeName, (_, _) => eventTypeName);
|
||||
|
||||
return type;
|
||||
});
|
||||
}
|
||||
9
src/BuildingBlocks/EventStoreDB/Events/IEventHandler.cs
Normal file
9
src/BuildingBlocks/EventStoreDB/Events/IEventHandler.cs
Normal file
@ -0,0 +1,9 @@
|
||||
using BuildingBlocks.Domain.Event;
|
||||
using MediatR;
|
||||
|
||||
namespace BuildingBlocks.EventStoreDB.Events;
|
||||
|
||||
public interface IEventHandler<in TEvent>: INotificationHandler<TEvent>
|
||||
where TEvent : IEvent
|
||||
{
|
||||
}
|
||||
7
src/BuildingBlocks/EventStoreDB/Events/IExternalEvent.cs
Normal file
7
src/BuildingBlocks/EventStoreDB/Events/IExternalEvent.cs
Normal file
@ -0,0 +1,7 @@
|
||||
using BuildingBlocks.Domain.Event;
|
||||
|
||||
namespace BuildingBlocks.EventStoreDB.Events;
|
||||
|
||||
public interface IExternalEvent: IEvent
|
||||
{
|
||||
}
|
||||
6
src/BuildingBlocks/EventStoreDB/Events/IProjection.cs
Normal file
6
src/BuildingBlocks/EventStoreDB/Events/IProjection.cs
Normal file
@ -0,0 +1,6 @@
|
||||
namespace BuildingBlocks.EventStoreDB.Events;
|
||||
|
||||
public interface IProjection
|
||||
{
|
||||
void When(object @event);
|
||||
}
|
||||
30
src/BuildingBlocks/EventStoreDB/Events/StreamEvent.cs
Normal file
30
src/BuildingBlocks/EventStoreDB/Events/StreamEvent.cs
Normal file
@ -0,0 +1,30 @@
|
||||
using BuildingBlocks.Domain.Event;
|
||||
|
||||
namespace BuildingBlocks.EventStoreDB.Events;
|
||||
|
||||
public record EventMetadata(
|
||||
ulong StreamRevision,
|
||||
ulong LogPosition
|
||||
);
|
||||
|
||||
public class StreamEvent: IEvent
|
||||
{
|
||||
public object Data { get; }
|
||||
public EventMetadata Metadata { get; }
|
||||
|
||||
public StreamEvent(object data, EventMetadata metadata)
|
||||
{
|
||||
Data = data;
|
||||
Metadata = metadata;
|
||||
}
|
||||
}
|
||||
|
||||
public class StreamEvent<T>: StreamEvent where T: notnull
|
||||
{
|
||||
public new T Data => (T)base.Data;
|
||||
|
||||
public StreamEvent(T data, EventMetadata metadata) : base(data, metadata)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
@ -0,0 +1,19 @@
|
||||
using System.Diagnostics.Eventing.Reader;
|
||||
using BuildingBlocks.EventStoreDB.Serialization;
|
||||
using EventStore.Client;
|
||||
|
||||
namespace BuildingBlocks.EventStoreDB.Events;
|
||||
|
||||
public static class StreamEventExtensions
|
||||
{
|
||||
public static StreamEvent? ToStreamEvent(this ResolvedEvent resolvedEvent)
|
||||
{
|
||||
var eventData = resolvedEvent.Deserialize();
|
||||
if (eventData == null)
|
||||
return null;
|
||||
|
||||
var metaData = new EventMetadata(resolvedEvent.Event.EventNumber.ToUInt64(), resolvedEvent.Event.Position.CommitPosition);
|
||||
var type = typeof(StreamEvent<>).MakeGenericType(eventData.GetType());
|
||||
return (StreamEvent)Activator.CreateInstance(type, eventData, metaData)!;
|
||||
}
|
||||
}
|
||||
28
src/BuildingBlocks/EventStoreDB/Events/StreamNameMapper.cs
Normal file
28
src/BuildingBlocks/EventStoreDB/Events/StreamNameMapper.cs
Normal file
@ -0,0 +1,28 @@
|
||||
using System.Collections.Concurrent;
|
||||
|
||||
namespace BuildingBlocks.EventStoreDB.Events;
|
||||
|
||||
public class StreamNameMapper
|
||||
{
|
||||
private static readonly StreamNameMapper Instance = new();
|
||||
|
||||
private readonly ConcurrentDictionary<Type, string> TypeNameMap = new();
|
||||
|
||||
public static void AddCustomMap<TStream>(string mappedStreamName) =>
|
||||
AddCustomMap(typeof(TStream), mappedStreamName);
|
||||
|
||||
public static void AddCustomMap(Type streamType, string mappedStreamName)
|
||||
{
|
||||
Instance.TypeNameMap.AddOrUpdate(streamType, mappedStreamName, (_, _) => mappedStreamName);
|
||||
}
|
||||
public static string ToStreamId<TStream>(object aggregateId, object? tenantId = null) =>
|
||||
ToStreamId(typeof(TStream), aggregateId);
|
||||
|
||||
public static string ToStreamId(Type streamType, object aggregateId, object? tenantId = null)
|
||||
{
|
||||
var tenantPrefix = tenantId != null ? $"{tenantId}_" : "";
|
||||
|
||||
return $"{tenantPrefix}{streamType.Name}-{aggregateId}";
|
||||
}
|
||||
|
||||
}
|
||||
21
src/BuildingBlocks/EventStoreDB/Extensions.cs
Normal file
21
src/BuildingBlocks/EventStoreDB/Extensions.cs
Normal file
@ -0,0 +1,21 @@
|
||||
using System.Reflection;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
namespace BuildingBlocks.EventStoreDB;
|
||||
|
||||
public static class Extensions
|
||||
{
|
||||
public static IServiceCollection AddEventStore(
|
||||
this IServiceCollection services,
|
||||
IConfiguration configuration,
|
||||
params Assembly[] assemblies
|
||||
)
|
||||
{
|
||||
var assembliesToScan = assemblies.Length > 0 ? assemblies : new[] { Assembly.GetEntryAssembly()! };
|
||||
|
||||
return services
|
||||
.AddEventStoreDB(configuration)
|
||||
.AddProjections(assembliesToScan);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,10 @@
|
||||
using BuildingBlocks.EventStoreDB.Events;
|
||||
using MediatR;
|
||||
|
||||
namespace BuildingBlocks.EventStoreDB.Projections;
|
||||
|
||||
public interface IProjectionProcessor
|
||||
{
|
||||
Task ProcessEventAsync<T>(StreamEvent<T> streamEvent, CancellationToken cancellationToken = default)
|
||||
where T : INotification;
|
||||
}
|
||||
@ -0,0 +1,12 @@
|
||||
using BuildingBlocks.EventStoreDB.Events;
|
||||
using MediatR;
|
||||
|
||||
namespace BuildingBlocks.EventStoreDB.Projections;
|
||||
|
||||
public interface IProjectionPublisher
|
||||
{
|
||||
Task PublishAsync<T>(StreamEvent<T> streamEvent, CancellationToken cancellationToken = default)
|
||||
where T : INotification;
|
||||
|
||||
Task PublishAsync(StreamEvent streamEvent, CancellationToken cancellationToken = default);
|
||||
}
|
||||
@ -0,0 +1,39 @@
|
||||
using BuildingBlocks.EventStoreDB.Events;
|
||||
using MediatR;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
namespace BuildingBlocks.EventStoreDB.Projections;
|
||||
|
||||
public class ProjectionPublisher : IProjectionPublisher
|
||||
{
|
||||
private readonly IServiceProvider _serviceProvider;
|
||||
|
||||
public ProjectionPublisher(IServiceProvider serviceProvider)
|
||||
{
|
||||
_serviceProvider = serviceProvider;
|
||||
}
|
||||
|
||||
public async Task PublishAsync<T>(StreamEvent<T> streamEvent, CancellationToken cancellationToken = default)
|
||||
where T : INotification
|
||||
{
|
||||
using var scope = _serviceProvider.CreateScope();
|
||||
var projectionsProcessors = scope.ServiceProvider.GetRequiredService<IEnumerable<IProjectionProcessor>>();
|
||||
foreach (var projectionProcessor in projectionsProcessors)
|
||||
{
|
||||
await projectionProcessor.ProcessEventAsync(streamEvent, cancellationToken);
|
||||
}
|
||||
}
|
||||
|
||||
public Task PublishAsync(StreamEvent streamEvent, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var streamData = streamEvent.Data.GetType();
|
||||
|
||||
var method = typeof(IProjectionPublisher)
|
||||
.GetMethods()
|
||||
.Single(m => m.Name == nameof(PublishAsync) && m.GetGenericArguments().Any())
|
||||
.MakeGenericMethod(streamData);
|
||||
|
||||
return (Task)method
|
||||
.Invoke(this, new object[] { streamEvent, cancellationToken })!;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,65 @@
|
||||
using BuildingBlocks.Domain.Model;
|
||||
using BuildingBlocks.EventStoreDB.Events;
|
||||
using BuildingBlocks.EventStoreDB.Serialization;
|
||||
using EventStore.Client;
|
||||
|
||||
namespace BuildingBlocks.EventStoreDB.Repository;
|
||||
|
||||
public interface IEventStoreDBRepository<T> where T : class, IAggregate<long>
|
||||
{
|
||||
Task<T?> Find(long id, CancellationToken cancellationToken);
|
||||
Task<ulong> Add(T aggregate, CancellationToken cancellationToken);
|
||||
Task<ulong> Update(T aggregate, long? expectedRevision = null, CancellationToken cancellationToken = default);
|
||||
Task<ulong> Delete(T aggregate, long? expectedRevision = null, CancellationToken cancellationToken = default);
|
||||
}
|
||||
|
||||
public class EventStoreDBRepository<T>: IEventStoreDBRepository<T> where T : class, IAggregate<long>
|
||||
{
|
||||
private readonly EventStoreClient eventStore;
|
||||
|
||||
public EventStoreDBRepository(EventStoreClient eventStore)
|
||||
{
|
||||
this.eventStore = eventStore ?? throw new ArgumentNullException(nameof(eventStore));
|
||||
}
|
||||
|
||||
public Task<T?> Find(long id, CancellationToken cancellationToken) =>
|
||||
eventStore.AggregateStream<T>(
|
||||
id,
|
||||
cancellationToken
|
||||
);
|
||||
|
||||
public async Task<ulong> Add(T aggregate, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var result = await eventStore.AppendToStreamAsync(
|
||||
StreamNameMapper.ToStreamId<T>(aggregate.Id),
|
||||
StreamState.NoStream,
|
||||
GetEventsToStore(aggregate),
|
||||
cancellationToken: cancellationToken
|
||||
);
|
||||
return result.NextExpectedStreamRevision;
|
||||
}
|
||||
|
||||
public async Task<ulong> Update(T aggregate, long? expectedRevision = null, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var nextVersion = expectedRevision ?? aggregate.Version;
|
||||
|
||||
var result = await eventStore.AppendToStreamAsync(
|
||||
StreamNameMapper.ToStreamId<T>(aggregate.Id),
|
||||
(ulong)nextVersion,
|
||||
GetEventsToStore(aggregate),
|
||||
cancellationToken: cancellationToken
|
||||
);
|
||||
return result.NextExpectedStreamRevision;
|
||||
}
|
||||
|
||||
public Task<ulong> Delete(T aggregate, long? expectedRevision = null, CancellationToken cancellationToken = default) =>
|
||||
Update(aggregate, expectedRevision, cancellationToken);
|
||||
|
||||
private static IEnumerable<EventData> GetEventsToStore(T aggregate)
|
||||
{
|
||||
var events = aggregate.ClearDomainEvents();
|
||||
|
||||
return events
|
||||
.Select(EventStoreDBSerializer.ToJsonEventData);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,33 @@
|
||||
using BuildingBlocks.Domain.Model;
|
||||
using BuildingBlocks.Exception;
|
||||
|
||||
namespace BuildingBlocks.EventStoreDB.Repository;
|
||||
|
||||
public static class RepositoryExtensions
|
||||
{
|
||||
public static async Task<T> Get<T>(
|
||||
this IEventStoreDBRepository<T> repository,
|
||||
long id,
|
||||
CancellationToken cancellationToken
|
||||
) where T : class, IAggregate<long>
|
||||
{
|
||||
var entity = await repository.Find(id, cancellationToken);
|
||||
|
||||
return entity ?? throw AggregateNotFoundException.For<T>(id);
|
||||
}
|
||||
|
||||
public static async Task<ulong> GetAndUpdate<T>(
|
||||
this IEventStoreDBRepository<T> repository,
|
||||
long id,
|
||||
Action<T> action,
|
||||
long? expectedVersion = null,
|
||||
CancellationToken cancellationToken = default
|
||||
) where T : class, IAggregate<long>
|
||||
{
|
||||
var entity = await repository.Get(id, cancellationToken);
|
||||
|
||||
action(entity);
|
||||
|
||||
return await repository.Update(entity, expectedVersion, cancellationToken);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,39 @@
|
||||
using System.Text;
|
||||
using BuildingBlocks.EventStoreDB.Events;
|
||||
using EventStore.Client;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace BuildingBlocks.EventStoreDB.Serialization;
|
||||
|
||||
public static class EventStoreDBSerializer
|
||||
{
|
||||
private static readonly JsonSerializerSettings SerializerSettings =
|
||||
new JsonSerializerSettings().WithNonDefaultConstructorContractResolver();
|
||||
|
||||
public static T? Deserialize<T>(this ResolvedEvent resolvedEvent) where T : class =>
|
||||
Deserialize(resolvedEvent) as T;
|
||||
|
||||
public static object? Deserialize(this ResolvedEvent resolvedEvent)
|
||||
{
|
||||
// get type
|
||||
var eventType = EventTypeMapper.ToType(resolvedEvent.Event.EventType);
|
||||
|
||||
if (eventType == null)
|
||||
return null;
|
||||
|
||||
// deserialize event
|
||||
return JsonConvert.DeserializeObject(
|
||||
Encoding.UTF8.GetString(resolvedEvent.Event.Data.Span),
|
||||
eventType,
|
||||
SerializerSettings
|
||||
)!;
|
||||
}
|
||||
|
||||
public static EventData ToJsonEventData(this object @event) =>
|
||||
new(
|
||||
Uuid.NewUuid(),
|
||||
EventTypeMapper.ToName(@event.GetType()),
|
||||
Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(@event)),
|
||||
Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(new { }))
|
||||
);
|
||||
}
|
||||
@ -0,0 +1,80 @@
|
||||
using System.Collections.Concurrent;
|
||||
using System.Reflection;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Serialization;
|
||||
|
||||
namespace BuildingBlocks.EventStoreDB.Serialization;
|
||||
|
||||
public static class JsonObjectContractProvider
|
||||
{
|
||||
private static readonly Type ConstructorAttributeType = typeof(JsonConstructorAttribute);
|
||||
private static readonly ConcurrentDictionary<string, JsonObjectContract> Constructors = new();
|
||||
|
||||
public static JsonObjectContract UsingNonDefaultConstructor(
|
||||
JsonObjectContract contract,
|
||||
Type objectType,
|
||||
Func<ConstructorInfo, JsonPropertyCollection, IList<JsonProperty>> createConstructorParameters) =>
|
||||
Constructors.GetOrAdd(objectType.AssemblyQualifiedName!, _ =>
|
||||
{
|
||||
var nonDefaultConstructor = GetNonDefaultConstructor(objectType);
|
||||
|
||||
if (nonDefaultConstructor == null) return contract;
|
||||
|
||||
contract.OverrideCreator = GetObjectConstructor(nonDefaultConstructor);
|
||||
contract.CreatorParameters.Clear();
|
||||
foreach (var constructorParameter in
|
||||
createConstructorParameters(nonDefaultConstructor, contract.Properties))
|
||||
{
|
||||
contract.CreatorParameters.Add(constructorParameter);
|
||||
}
|
||||
|
||||
return contract;
|
||||
});
|
||||
|
||||
private static ObjectConstructor<object> GetObjectConstructor(MethodBase method)
|
||||
{
|
||||
var c = method as ConstructorInfo;
|
||||
|
||||
if (c == null)
|
||||
return a => method.Invoke(null, a)!;
|
||||
|
||||
if (!c.GetParameters().Any())
|
||||
return _ => c.Invoke(Array.Empty<object?>());
|
||||
|
||||
return a => c.Invoke(a);
|
||||
}
|
||||
|
||||
private static ConstructorInfo? GetNonDefaultConstructor(Type objectType)
|
||||
{
|
||||
// Use default contract for non-object types.
|
||||
if (objectType.IsPrimitive || objectType.IsEnum)
|
||||
return null;
|
||||
|
||||
return GetAttributeConstructor(objectType)
|
||||
?? GetTheMostSpecificConstructor(objectType);
|
||||
}
|
||||
|
||||
private static ConstructorInfo? GetAttributeConstructor(Type objectType)
|
||||
{
|
||||
// Use default contract for non-object types.
|
||||
if (objectType.IsPrimitive || objectType.IsEnum)
|
||||
return null;
|
||||
|
||||
var constructors = objectType
|
||||
.GetConstructors(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic)
|
||||
.Where(c => c.GetCustomAttributes().Any(a => a.GetType() == ConstructorAttributeType)).ToList();
|
||||
|
||||
return constructors.Count switch
|
||||
{
|
||||
1 => constructors[0],
|
||||
> 1 => throw new JsonException($"Multiple constructors with a {ConstructorAttributeType.Name}."),
|
||||
_ => null
|
||||
};
|
||||
}
|
||||
|
||||
private static ConstructorInfo? GetTheMostSpecificConstructor(Type objectType) =>
|
||||
objectType
|
||||
.GetConstructors(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic)
|
||||
.OrderByDescending(e => e.GetParameters().Length)
|
||||
.FirstOrDefault();
|
||||
}
|
||||
@ -0,0 +1,15 @@
|
||||
using Newtonsoft.Json.Serialization;
|
||||
|
||||
namespace BuildingBlocks.EventStoreDB.Serialization;
|
||||
|
||||
public class NonDefaultConstructorContractResolver: DefaultContractResolver
|
||||
{
|
||||
protected override JsonObjectContract CreateObjectContract(Type objectType)
|
||||
{
|
||||
return JsonObjectContractProvider.UsingNonDefaultConstructor(
|
||||
base.CreateObjectContract(objectType),
|
||||
objectType,
|
||||
base.CreateConstructorParameters
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,68 @@
|
||||
using System.Text;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Converters;
|
||||
|
||||
namespace BuildingBlocks.EventStoreDB.Serialization;
|
||||
|
||||
public static class SerializationExtensions
|
||||
{
|
||||
public static JsonSerializerSettings WithDefaults(this JsonSerializerSettings settings)
|
||||
{
|
||||
settings.WithNonDefaultConstructorContractResolver()
|
||||
.Converters.Add(new StringEnumConverter());
|
||||
|
||||
return settings;
|
||||
}
|
||||
|
||||
public static JsonSerializerSettings WithNonDefaultConstructorContractResolver(this JsonSerializerSettings settings)
|
||||
{
|
||||
settings.ContractResolver = new NonDefaultConstructorContractResolver();
|
||||
return settings;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Deserialize object from json with JsonNet
|
||||
/// </summary>
|
||||
/// <typeparam name="T">Type of the deserialized object</typeparam>
|
||||
/// <param name="json">json string</param>
|
||||
/// <returns>deserialized object</returns>
|
||||
public static T FromJson<T>(this string json)
|
||||
{
|
||||
return JsonConvert.DeserializeObject<T>(json,
|
||||
new JsonSerializerSettings().WithNonDefaultConstructorContractResolver())!;
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Deserialize object from json with JsonNet
|
||||
/// </summary>
|
||||
/// <typeparam name="T">Type of the deserialized object</typeparam>
|
||||
/// <param name="json">json string</param>
|
||||
/// <param name="type">object type</param>
|
||||
/// <returns>deserialized object</returns>
|
||||
public static object FromJson(this string json, Type type)
|
||||
{
|
||||
return JsonConvert.DeserializeObject(json, type,
|
||||
new JsonSerializerSettings().WithNonDefaultConstructorContractResolver())!;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Serialize object to json with JsonNet
|
||||
/// </summary>
|
||||
/// <param name="obj">object to serialize</param>
|
||||
/// <returns>json string</returns>
|
||||
public static string ToJson(this object obj)
|
||||
{
|
||||
return JsonConvert.SerializeObject(obj);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Serialize object to json with JsonNet
|
||||
/// </summary>
|
||||
/// <param name="obj">object to serialize</param>
|
||||
/// <returns>json string</returns>
|
||||
public static StringContent ToJsonStringContent(this object obj)
|
||||
{
|
||||
return new StringContent(obj.ToJson(), Encoding.UTF8, "application/json");
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,76 @@
|
||||
using BuildingBlocks.Domain.Event;
|
||||
using BuildingBlocks.EventStoreDB.Events;
|
||||
using BuildingBlocks.EventStoreDB.Serialization;
|
||||
using EventStore.Client;
|
||||
|
||||
namespace BuildingBlocks.EventStoreDB.Subscriptions;
|
||||
|
||||
public record CheckpointStored(string SubscriptionId, ulong? Position, DateTime CheckpointedAt): IEvent;
|
||||
|
||||
public class EventStoreDBSubscriptionCheckpointRepository: ISubscriptionCheckpointRepository
|
||||
{
|
||||
private readonly EventStoreClient eventStoreClient;
|
||||
|
||||
public EventStoreDBSubscriptionCheckpointRepository(
|
||||
EventStoreClient eventStoreClient)
|
||||
{
|
||||
this.eventStoreClient = eventStoreClient ?? throw new ArgumentNullException(nameof(eventStoreClient));
|
||||
}
|
||||
|
||||
public async ValueTask<ulong?> Load(string subscriptionId, CancellationToken ct)
|
||||
{
|
||||
var streamName = GetCheckpointStreamName(subscriptionId);
|
||||
|
||||
var result = eventStoreClient.ReadStreamAsync(Direction.Backwards, streamName, StreamPosition.End, 1,
|
||||
cancellationToken: ct);
|
||||
|
||||
if (await result.ReadState == ReadState.StreamNotFound)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
ResolvedEvent? @event = await result.FirstOrDefaultAsync(ct);
|
||||
|
||||
return @event?.Deserialize<CheckpointStored>()?.Position;
|
||||
}
|
||||
|
||||
public async ValueTask Store(string subscriptionId, ulong position, CancellationToken ct)
|
||||
{
|
||||
var @event = new CheckpointStored(subscriptionId, position, DateTime.UtcNow);
|
||||
var eventToAppend = new[] {@event.ToJsonEventData()};
|
||||
var streamName = GetCheckpointStreamName(subscriptionId);
|
||||
|
||||
try
|
||||
{
|
||||
// store new checkpoint expecting stream to exist
|
||||
await eventStoreClient.AppendToStreamAsync(
|
||||
streamName,
|
||||
StreamState.StreamExists,
|
||||
eventToAppend,
|
||||
cancellationToken: ct
|
||||
);
|
||||
}
|
||||
catch (WrongExpectedVersionException)
|
||||
{
|
||||
// WrongExpectedVersionException means that stream did not exist
|
||||
// Set the checkpoint stream to have at most 1 event
|
||||
// using stream metadata $maxCount property
|
||||
await eventStoreClient.SetStreamMetadataAsync(
|
||||
streamName,
|
||||
StreamState.NoStream,
|
||||
new StreamMetadata(1),
|
||||
cancellationToken: ct
|
||||
);
|
||||
|
||||
// append event again expecting stream to not exist
|
||||
await eventStoreClient.AppendToStreamAsync(
|
||||
streamName,
|
||||
StreamState.NoStream,
|
||||
eventToAppend,
|
||||
cancellationToken: ct
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private static string GetCheckpointStreamName(string subscriptionId) => $"checkpoint_{subscriptionId}";
|
||||
}
|
||||
@ -0,0 +1,188 @@
|
||||
using BuildingBlocks.EventStoreDB.Events;
|
||||
using BuildingBlocks.EventStoreDB.Projections;
|
||||
using BuildingBlocks.Utils;
|
||||
using EventStore.Client;
|
||||
using Grpc.Core;
|
||||
using MediatR;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace BuildingBlocks.EventStoreDB.Subscriptions;
|
||||
|
||||
public class EventStoreDBSubscriptionToAllOptions
|
||||
{
|
||||
public string SubscriptionId { get; set; } = "default";
|
||||
|
||||
public SubscriptionFilterOptions FilterOptions { get; set; } =
|
||||
new(EventTypeFilter.ExcludeSystemEvents());
|
||||
|
||||
public Action<EventStoreClientOperationOptions>? ConfigureOperation { get; set; }
|
||||
public UserCredentials? Credentials { get; set; }
|
||||
public bool ResolveLinkTos { get; set; }
|
||||
public bool IgnoreDeserializationErrors { get; set; } = true;
|
||||
}
|
||||
|
||||
public class EventStoreDBSubscriptionToAll
|
||||
{
|
||||
private readonly IProjectionPublisher projectionPublisher;
|
||||
private readonly EventStoreClient eventStoreClient;
|
||||
private readonly IMediator _mediator;
|
||||
private readonly ISubscriptionCheckpointRepository checkpointRepository;
|
||||
private readonly ILogger<EventStoreDBSubscriptionToAll> logger;
|
||||
private EventStoreDBSubscriptionToAllOptions subscriptionOptions = default!;
|
||||
private string SubscriptionId => subscriptionOptions.SubscriptionId;
|
||||
private readonly object resubscribeLock = new();
|
||||
private CancellationToken cancellationToken;
|
||||
|
||||
public EventStoreDBSubscriptionToAll(
|
||||
EventStoreClient eventStoreClient,
|
||||
IMediator mediator,
|
||||
IProjectionPublisher projectionPublisher,
|
||||
ISubscriptionCheckpointRepository checkpointRepository,
|
||||
ILogger<EventStoreDBSubscriptionToAll> logger
|
||||
)
|
||||
{
|
||||
this.projectionPublisher = projectionPublisher;
|
||||
this.eventStoreClient = eventStoreClient ?? throw new ArgumentNullException(nameof(eventStoreClient));
|
||||
_mediator = mediator;
|
||||
this.checkpointRepository =
|
||||
checkpointRepository ?? throw new ArgumentNullException(nameof(checkpointRepository));
|
||||
this.logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
||||
}
|
||||
|
||||
public async Task SubscribeToAll(EventStoreDBSubscriptionToAllOptions subscriptionOptions, CancellationToken ct)
|
||||
{
|
||||
// see: https://github.com/dotnet/runtime/issues/36063
|
||||
await Task.Yield();
|
||||
|
||||
this.subscriptionOptions = subscriptionOptions;
|
||||
cancellationToken = ct;
|
||||
|
||||
logger.LogInformation("Subscription to all '{SubscriptionId}'", subscriptionOptions.SubscriptionId);
|
||||
|
||||
var checkpoint = await checkpointRepository.Load(SubscriptionId, ct);
|
||||
|
||||
await eventStoreClient.SubscribeToAllAsync(
|
||||
checkpoint == null ? FromAll.Start : FromAll.After(new Position(checkpoint.Value, checkpoint.Value)),
|
||||
HandleEvent,
|
||||
subscriptionOptions.ResolveLinkTos,
|
||||
HandleDrop,
|
||||
subscriptionOptions.FilterOptions,
|
||||
subscriptionOptions.Credentials,
|
||||
ct
|
||||
);
|
||||
|
||||
logger.LogInformation("Subscription to all '{SubscriptionId}' started", SubscriptionId);
|
||||
}
|
||||
|
||||
private async Task HandleEvent(StreamSubscription subscription, ResolvedEvent resolvedEvent,
|
||||
CancellationToken ct)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (IsEventWithEmptyData(resolvedEvent) || IsCheckpointEvent(resolvedEvent)) return;
|
||||
|
||||
var streamEvent = resolvedEvent.ToStreamEvent();
|
||||
|
||||
if (streamEvent == null)
|
||||
{
|
||||
// That can happen if we're sharing database between modules.
|
||||
// If we're subscribing to all and not filtering out events from other modules,
|
||||
// then we might get events that are from other module and we might not be able to deserialize them.
|
||||
// In that case it's safe to ignore deserialization error.
|
||||
// You may add more sophisticated logic checking if it should be ignored or not.
|
||||
logger.LogWarning("Couldn't deserialize event with id: {EventId}", resolvedEvent.Event.EventId);
|
||||
|
||||
if (!subscriptionOptions.IgnoreDeserializationErrors)
|
||||
throw new InvalidOperationException($"Unable to deserialize event {resolvedEvent.Event.EventType} with id: {resolvedEvent.Event.EventId}");
|
||||
return;
|
||||
}
|
||||
|
||||
// publish event to internal event bus
|
||||
await _mediator.Publish(streamEvent, ct);
|
||||
|
||||
await projectionPublisher.PublishAsync(streamEvent, ct);
|
||||
|
||||
await checkpointRepository.Store(SubscriptionId, resolvedEvent.Event.Position.CommitPosition, ct);
|
||||
}
|
||||
catch (System.Exception e)
|
||||
{
|
||||
logger.LogError("Error consuming message: {ExceptionMessage}{ExceptionStackTrace}", e.Message,
|
||||
e.StackTrace);
|
||||
// if you're fine with dropping some events instead of stopping subscription
|
||||
// then you can add some logic if error should be ignored
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
private void HandleDrop(StreamSubscription _, SubscriptionDroppedReason reason, System.Exception? exception)
|
||||
{
|
||||
logger.LogError(
|
||||
exception,
|
||||
"Subscription to all '{SubscriptionId}' dropped with '{Reason}'",
|
||||
SubscriptionId,
|
||||
reason
|
||||
);
|
||||
|
||||
if (exception is RpcException {StatusCode: StatusCode.Cancelled})
|
||||
return;
|
||||
|
||||
Resubscribe();
|
||||
}
|
||||
|
||||
private void Resubscribe()
|
||||
{
|
||||
// You may consider adding a max resubscribe count if you want to fail process
|
||||
// instead of retrying until database is up
|
||||
while (true)
|
||||
{
|
||||
var resubscribed = false;
|
||||
try
|
||||
{
|
||||
Monitor.Enter(resubscribeLock);
|
||||
|
||||
// No synchronization context is needed to disable synchronization context.
|
||||
// That enables running asynchronous method not causing deadlocks.
|
||||
// As this is a background process then we don't need to have async context here.
|
||||
using (NoSynchronizationContextScope.Enter())
|
||||
{
|
||||
SubscribeToAll(subscriptionOptions, cancellationToken).Wait(cancellationToken);
|
||||
}
|
||||
|
||||
resubscribed = true;
|
||||
}
|
||||
catch (System.Exception exception)
|
||||
{
|
||||
logger.LogWarning(exception,
|
||||
"Failed to resubscribe to all '{SubscriptionId}' dropped with '{ExceptionMessage}{ExceptionStackTrace}'",
|
||||
SubscriptionId, exception.Message, exception.StackTrace);
|
||||
}
|
||||
finally
|
||||
{
|
||||
Monitor.Exit(resubscribeLock);
|
||||
}
|
||||
|
||||
if (resubscribed)
|
||||
break;
|
||||
|
||||
// Sleep between reconnections to not flood the database or not kill the CPU with infinite loop
|
||||
// Randomness added to reduce the chance of multiple subscriptions trying to reconnect at the same time
|
||||
Thread.Sleep(1000 + new Random((int)DateTime.UtcNow.Ticks).Next(1000));
|
||||
}
|
||||
}
|
||||
|
||||
private bool IsEventWithEmptyData(ResolvedEvent resolvedEvent)
|
||||
{
|
||||
if (resolvedEvent.Event.Data.Length != 0) return false;
|
||||
|
||||
logger.LogInformation("Event without data received");
|
||||
return true;
|
||||
}
|
||||
|
||||
private bool IsCheckpointEvent(ResolvedEvent resolvedEvent)
|
||||
{
|
||||
if (resolvedEvent.Event.EventType != EventTypeMapper.ToName<CheckpointStored>()) return false;
|
||||
|
||||
logger.LogInformation("Checkpoint event - ignoring");
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,8 @@
|
||||
namespace BuildingBlocks.EventStoreDB.Subscriptions;
|
||||
|
||||
public interface ISubscriptionCheckpointRepository
|
||||
{
|
||||
ValueTask<ulong?> Load(string subscriptionId, CancellationToken ct);
|
||||
|
||||
ValueTask Store(string subscriptionId, ulong position, CancellationToken ct);
|
||||
}
|
||||
@ -0,0 +1,20 @@
|
||||
using System.Collections.Concurrent;
|
||||
|
||||
namespace BuildingBlocks.EventStoreDB.Subscriptions;
|
||||
|
||||
public class InMemorySubscriptionCheckpointRepository : ISubscriptionCheckpointRepository
|
||||
{
|
||||
private readonly ConcurrentDictionary<string, ulong> checkpoints = new();
|
||||
|
||||
public ValueTask<ulong?> Load(string subscriptionId, CancellationToken ct)
|
||||
{
|
||||
return new(checkpoints.TryGetValue(subscriptionId, out var checkpoint) ? checkpoint : null);
|
||||
}
|
||||
|
||||
public ValueTask Store(string subscriptionId, ulong position, CancellationToken ct)
|
||||
{
|
||||
checkpoints.AddOrUpdate(subscriptionId, position, (_, _) => position);
|
||||
|
||||
return ValueTask.CompletedTask;
|
||||
}
|
||||
}
|
||||
14
src/BuildingBlocks/Exception/AggregateNotFoundException.cs
Normal file
14
src/BuildingBlocks/Exception/AggregateNotFoundException.cs
Normal file
@ -0,0 +1,14 @@
|
||||
namespace BuildingBlocks.Exception;
|
||||
|
||||
public class AggregateNotFoundException : System.Exception
|
||||
{
|
||||
public AggregateNotFoundException(string typeName, long id): base($"{typeName} with id '{id}' was not found")
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
public static AggregateNotFoundException For<T>(long id)
|
||||
{
|
||||
return new AggregateNotFoundException(typeof(T).Name, id);
|
||||
}
|
||||
}
|
||||
30
src/BuildingBlocks/Exception/AppException.cs
Normal file
30
src/BuildingBlocks/Exception/AppException.cs
Normal file
@ -0,0 +1,30 @@
|
||||
using System.Net;
|
||||
using OpenTelemetry.Trace;
|
||||
|
||||
namespace BuildingBlocks.Exception;
|
||||
|
||||
public class AppException : CustomException
|
||||
{
|
||||
public AppException(string message, string code = default!) : base(message)
|
||||
{
|
||||
Code = code;
|
||||
}
|
||||
|
||||
public AppException() : base()
|
||||
{
|
||||
}
|
||||
|
||||
public AppException(string message) : base(message)
|
||||
{
|
||||
}
|
||||
|
||||
public AppException(string message, HttpStatusCode statusCode) : base(message, statusCode)
|
||||
{
|
||||
}
|
||||
|
||||
public AppException(string message, System.Exception innerException) : base(message, innerException)
|
||||
{
|
||||
}
|
||||
|
||||
public string Code { get; }
|
||||
}
|
||||
12
src/BuildingBlocks/Exception/BadRequestException.cs
Normal file
12
src/BuildingBlocks/Exception/BadRequestException.cs
Normal file
@ -0,0 +1,12 @@
|
||||
using System;
|
||||
|
||||
namespace BuildingBlocks.Exception
|
||||
{
|
||||
public class BadRequestException : CustomException
|
||||
{
|
||||
public BadRequestException(string message) : base(message)
|
||||
{
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
11
src/BuildingBlocks/Exception/ConflictException.cs
Normal file
11
src/BuildingBlocks/Exception/ConflictException.cs
Normal file
@ -0,0 +1,11 @@
|
||||
namespace BuildingBlocks.Exception
|
||||
{
|
||||
public class ConflictException : CustomException
|
||||
{
|
||||
public virtual string Code { get; }
|
||||
public ConflictException(string message, string code = default!) : base(message)
|
||||
{
|
||||
Code = code;
|
||||
}
|
||||
}
|
||||
}
|
||||
29
src/BuildingBlocks/Exception/CustomException.cs
Normal file
29
src/BuildingBlocks/Exception/CustomException.cs
Normal file
@ -0,0 +1,29 @@
|
||||
using System.Net;
|
||||
|
||||
namespace BuildingBlocks.Exception;
|
||||
|
||||
public class CustomException: System.Exception
|
||||
{
|
||||
public CustomException(
|
||||
string message,
|
||||
HttpStatusCode statusCode = HttpStatusCode.BadRequest) : base(message)
|
||||
{
|
||||
StatusCode = statusCode;
|
||||
}
|
||||
|
||||
public CustomException(
|
||||
string message,
|
||||
System.Exception innerException,
|
||||
HttpStatusCode statusCode = HttpStatusCode.BadRequest) : base(message, innerException)
|
||||
{
|
||||
StatusCode = statusCode;
|
||||
}
|
||||
|
||||
public CustomException(
|
||||
HttpStatusCode statusCode = HttpStatusCode.BadRequest) : base()
|
||||
{
|
||||
StatusCode = statusCode;
|
||||
}
|
||||
|
||||
public HttpStatusCode StatusCode { get; }
|
||||
}
|
||||
30
src/BuildingBlocks/Exception/GrpcExceptionInterceptor.cs
Normal file
30
src/BuildingBlocks/Exception/GrpcExceptionInterceptor.cs
Normal file
@ -0,0 +1,30 @@
|
||||
using Grpc.Core;
|
||||
using Grpc.Core.Interceptors;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace BuildingBlocks.Exception;
|
||||
|
||||
public class GrpcExceptionInterceptor : Interceptor
|
||||
{
|
||||
private readonly ILogger<GrpcExceptionInterceptor> _logger;
|
||||
|
||||
public GrpcExceptionInterceptor(ILogger<GrpcExceptionInterceptor> logger)
|
||||
{
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public override async Task<TResponse> UnaryServerHandler<TRequest, TResponse>(
|
||||
TRequest request,
|
||||
ServerCallContext context,
|
||||
UnaryServerMethod<TRequest, TResponse> continuation)
|
||||
{
|
||||
try
|
||||
{
|
||||
return await continuation(request, context);
|
||||
}
|
||||
catch (System.Exception exception)
|
||||
{
|
||||
throw new RpcException(new Status(StatusCode.Cancelled, exception.Message));
|
||||
}
|
||||
}
|
||||
}
|
||||
11
src/BuildingBlocks/Exception/IdentityException.cs
Normal file
11
src/BuildingBlocks/Exception/IdentityException.cs
Normal file
@ -0,0 +1,11 @@
|
||||
using System.Net;
|
||||
|
||||
namespace BuildingBlocks.Exception;
|
||||
|
||||
public class IdentityException: CustomException
|
||||
{
|
||||
public IdentityException(string message = default, HttpStatusCode statusCode = default)
|
||||
: base(message, statusCode)
|
||||
{
|
||||
}
|
||||
}
|
||||
17
src/BuildingBlocks/Exception/InternalServerException.cs
Normal file
17
src/BuildingBlocks/Exception/InternalServerException.cs
Normal file
@ -0,0 +1,17 @@
|
||||
using System;
|
||||
using System.Globalization;
|
||||
|
||||
namespace BuildingBlocks.Exception
|
||||
{
|
||||
public class InternalServerException : CustomException
|
||||
{
|
||||
public InternalServerException() : base() { }
|
||||
|
||||
public InternalServerException(string message) : base(message) { }
|
||||
|
||||
public InternalServerException(string message, params object[] args)
|
||||
: base(String.Format(CultureInfo.CurrentCulture, message, args))
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
9
src/BuildingBlocks/Exception/NotFoundException.cs
Normal file
9
src/BuildingBlocks/Exception/NotFoundException.cs
Normal file
@ -0,0 +1,9 @@
|
||||
namespace BuildingBlocks.Exception
|
||||
{
|
||||
public class NotFoundException : CustomException
|
||||
{
|
||||
public NotFoundException(string message) : base(message)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
13
src/BuildingBlocks/Exception/ValidationException.cs
Normal file
13
src/BuildingBlocks/Exception/ValidationException.cs
Normal file
@ -0,0 +1,13 @@
|
||||
using BuildingBlocks.Validation;
|
||||
|
||||
namespace BuildingBlocks.Exception
|
||||
{
|
||||
public class ValidationException : CustomException
|
||||
{
|
||||
public ValidationException(ValidationResultModel validationResultModel)
|
||||
{
|
||||
ValidationResultModel = validationResultModel;
|
||||
}
|
||||
public ValidationResultModel ValidationResultModel { get; }
|
||||
}
|
||||
}
|
||||
33
src/BuildingBlocks/IdsGenerator/SnowFlakIdGenerator.cs
Normal file
33
src/BuildingBlocks/IdsGenerator/SnowFlakIdGenerator.cs
Normal file
@ -0,0 +1,33 @@
|
||||
using Ardalis.GuardClauses;
|
||||
using IdGen;
|
||||
|
||||
namespace BuildingBlocks.IdsGenerator;
|
||||
|
||||
// Ref: https://github.com/RobThree/IdGen
|
||||
// https://github.com/RobThree/IdGen/issues/34
|
||||
// https://www.callicoder.com/distributed-unique-id-sequence-number-generator/
|
||||
public static class SnowFlakIdGenerator
|
||||
{
|
||||
private static IdGenerator _generator;
|
||||
|
||||
public static void Configure(int generatorId)
|
||||
{
|
||||
Guard.Against.NegativeOrZero(generatorId, nameof(generatorId));
|
||||
|
||||
// Let's say we take jan 17st 2022 as our epoch
|
||||
var epoch = new DateTime(2022, 1, 1, 0, 0, 0, DateTimeKind.Local);
|
||||
|
||||
// Create an ID with 45 bits for timestamp, 2 for generator-id
|
||||
// and 16 for sequence
|
||||
var structure = new IdStructure(45, 2, 16);
|
||||
|
||||
// Prepare options
|
||||
var options = new IdGeneratorOptions(structure, new DefaultTimeSource(epoch));
|
||||
|
||||
// Create an IdGenerator with it's generator-id set to 0, our custom epoch
|
||||
// and id-structure
|
||||
_generator = new IdGenerator(0, options);
|
||||
}
|
||||
|
||||
public static long NewId() => _generator.CreateId();
|
||||
}
|
||||
24
src/BuildingBlocks/Jwt/AuthHeaderHandler.cs
Normal file
24
src/BuildingBlocks/Jwt/AuthHeaderHandler.cs
Normal file
@ -0,0 +1,24 @@
|
||||
using System.Net.Http.Headers;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
|
||||
namespace BuildingBlocks.Jwt;
|
||||
|
||||
public class AuthHeaderHandler : DelegatingHandler
|
||||
{
|
||||
private readonly IHttpContextAccessor _httpContext;
|
||||
|
||||
public AuthHeaderHandler(IHttpContextAccessor httpContext)
|
||||
{
|
||||
_httpContext = httpContext;
|
||||
}
|
||||
|
||||
protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var token = (_httpContext?.HttpContext?.Request.Headers["Authorization"])?.ToString();
|
||||
|
||||
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", token?.Replace("Bearer ", "", StringComparison.Ordinal));
|
||||
|
||||
return base.SendAsync(request, cancellationToken);
|
||||
}
|
||||
}
|
||||
34
src/BuildingBlocks/Jwt/JwtExtensions.cs
Normal file
34
src/BuildingBlocks/Jwt/JwtExtensions.cs
Normal file
@ -0,0 +1,34 @@
|
||||
using BuildingBlocks.Utils;
|
||||
using Duende.IdentityServer.Models;
|
||||
using Microsoft.AspNetCore.Authentication.JwtBearer;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
namespace BuildingBlocks.Jwt;
|
||||
|
||||
public static class JwtExtensions
|
||||
{
|
||||
public static IServiceCollection AddJwt(this IServiceCollection services)
|
||||
{
|
||||
var jwtOptions = services.GetOptions<JwtBearerOptions>("Jwt");
|
||||
|
||||
services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
|
||||
.AddJwtBearer(JwtBearerDefaults.AuthenticationScheme, options =>
|
||||
{
|
||||
options.Authority = jwtOptions.Authority;
|
||||
options.TokenValidationParameters.ValidateAudience = false;
|
||||
});
|
||||
|
||||
if (!string.IsNullOrEmpty(jwtOptions.Audience))
|
||||
{
|
||||
services.AddAuthorization(options =>
|
||||
options.AddPolicy(nameof(ApiScope), policy =>
|
||||
{
|
||||
policy.RequireAuthenticatedUser();
|
||||
policy.RequireClaim("scope", jwtOptions.Audience);
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
return services;
|
||||
}
|
||||
}
|
||||
27
src/BuildingBlocks/Logging/Extensions.cs
Normal file
27
src/BuildingBlocks/Logging/Extensions.cs
Normal file
@ -0,0 +1,27 @@
|
||||
using Microsoft.AspNetCore.Builder;
|
||||
using Serilog;
|
||||
using Serilog.Enrichers.Span;
|
||||
using Serilog.Events;
|
||||
using Serilog.Sinks.SpectreConsole;
|
||||
|
||||
namespace BuildingBlocks.Logging;
|
||||
|
||||
public static class Extensions
|
||||
{
|
||||
public static WebApplicationBuilder AddCustomSerilog(this WebApplicationBuilder builder)
|
||||
{
|
||||
Log.Logger = new LoggerConfiguration()
|
||||
.WriteTo.Console()
|
||||
.CreateBootstrapLogger();
|
||||
|
||||
builder.Host.UseSerilog((ctx, lc) => lc
|
||||
.WriteTo.Console()
|
||||
.WriteTo.SpectreConsole("{Timestamp:HH:mm:ss} [{Level:u4}] {Message:lj}{NewLine}{Exception}", LogEventLevel.Error)
|
||||
.MinimumLevel.Override("Microsoft.EntityFrameworkCore", LogEventLevel.Error)
|
||||
.Enrich.WithSpan()
|
||||
.Enrich.FromLogContext()
|
||||
.ReadFrom.Configuration(ctx.Configuration));
|
||||
|
||||
return builder;
|
||||
}
|
||||
}
|
||||
42
src/BuildingBlocks/Logging/LoggingBehavior.cs
Normal file
42
src/BuildingBlocks/Logging/LoggingBehavior.cs
Normal file
@ -0,0 +1,42 @@
|
||||
using System.Diagnostics;
|
||||
using MediatR;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace BuildingBlocks.Logging;
|
||||
|
||||
public class LoggingBehavior<TRequest, TResponse> : IPipelineBehavior<TRequest, TResponse>
|
||||
where TRequest : notnull, IRequest<TResponse>
|
||||
where TResponse : notnull
|
||||
{
|
||||
private readonly ILogger<LoggingBehavior<TRequest, TResponse>> _logger;
|
||||
|
||||
public LoggingBehavior(ILogger<LoggingBehavior<TRequest, TResponse>> logger)
|
||||
{
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public async Task<TResponse> Handle(
|
||||
TRequest request,
|
||||
CancellationToken cancellationToken,
|
||||
RequestHandlerDelegate<TResponse> next)
|
||||
{
|
||||
const string prefix = nameof(LoggingBehavior<TRequest, TResponse>);
|
||||
|
||||
_logger.LogInformation("[{Prefix}] Handle request={X-RequestData} and response={X-ResponseData}",
|
||||
prefix, typeof(TRequest).Name, typeof(TResponse).Name);
|
||||
|
||||
var timer = new Stopwatch();
|
||||
timer.Start();
|
||||
|
||||
var response = await next();
|
||||
|
||||
timer.Stop();
|
||||
var timeTaken = timer.Elapsed;
|
||||
if (timeTaken.Seconds > 3) // if the request is greater than 3 seconds, then log the warnings
|
||||
_logger.LogWarning("[{Perf-Possible}] The request {X-RequestData} took {TimeTaken} seconds.",
|
||||
prefix, typeof(TRequest).Name, timeTaken.Seconds);
|
||||
|
||||
_logger.LogInformation("[{Prefix}] Handled {X-RequestData}", prefix, typeof(TRequest).Name);
|
||||
return response;
|
||||
}
|
||||
}
|
||||
19
src/BuildingBlocks/Mapster/Extensions.cs
Normal file
19
src/BuildingBlocks/Mapster/Extensions.cs
Normal file
@ -0,0 +1,19 @@
|
||||
using System.Reflection;
|
||||
using Mapster;
|
||||
using MapsterMapper;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
namespace BuildingBlocks.Mapster;
|
||||
|
||||
public static class Extensions
|
||||
{
|
||||
public static IServiceCollection AddCustomMapster(this IServiceCollection services, Assembly assembly)
|
||||
{
|
||||
var typeAdapterConfig = TypeAdapterConfig.GlobalSettings;
|
||||
typeAdapterConfig.Scan(assembly);
|
||||
var mapperConfig = new Mapper(typeAdapterConfig);
|
||||
services.AddSingleton<IMapper>(mapperConfig);
|
||||
|
||||
return services;
|
||||
}
|
||||
}
|
||||
71
src/BuildingBlocks/MassTransit/Extensions.cs
Normal file
71
src/BuildingBlocks/MassTransit/Extensions.cs
Normal file
@ -0,0 +1,71 @@
|
||||
using System.Reflection;
|
||||
using BuildingBlocks.Domain.Event;
|
||||
using BuildingBlocks.Utils;
|
||||
using Humanizer;
|
||||
using MassTransit;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
namespace BuildingBlocks.MassTransit;
|
||||
|
||||
public static class Extensions
|
||||
{
|
||||
private static bool? _isRunningInContainer;
|
||||
|
||||
private static bool IsRunningInContainer => _isRunningInContainer ??=
|
||||
bool.TryParse(Environment.GetEnvironmentVariable("DOTNET_RUNNING_IN_CONTAINER"), out var inContainer) &&
|
||||
inContainer;
|
||||
|
||||
public static IServiceCollection AddCustomMassTransit(this IServiceCollection services, Assembly assembly)
|
||||
{
|
||||
services.AddMassTransit(configure =>
|
||||
{
|
||||
configure.AddConsumers(assembly);
|
||||
|
||||
configure.UsingRabbitMq((context, configurator) =>
|
||||
{
|
||||
var rabbitMqOptions = services.GetOptions<RabbitMqOptions>("RabbitMq");
|
||||
var host = IsRunningInContainer ? "rabbitmq" : rabbitMqOptions.HostName;
|
||||
|
||||
configurator.Host(host, h =>
|
||||
{
|
||||
h.Username(rabbitMqOptions.UserName);
|
||||
h.Password(rabbitMqOptions.Password);
|
||||
});
|
||||
|
||||
var types = AppDomain.CurrentDomain.GetAssemblies().SelectMany(x => x.GetTypes())
|
||||
.Where(x => x.IsAssignableTo(typeof(IIntegrationEvent))
|
||||
&& !x.IsInterface
|
||||
&& !x.IsAbstract
|
||||
&& !x.IsGenericType);
|
||||
|
||||
foreach (var type in types)
|
||||
{
|
||||
var consumers = AppDomain.CurrentDomain.GetAssemblies().SelectMany(x => x.GetTypes())
|
||||
.Where(x => x.IsAssignableTo(typeof(IConsumer<>).MakeGenericType(type))).ToList();
|
||||
|
||||
if (consumers.Any())
|
||||
configurator.ReceiveEndpoint(
|
||||
string.IsNullOrEmpty(rabbitMqOptions.ExchangeName)
|
||||
? type.Name.Underscore()
|
||||
: $"{rabbitMqOptions.ExchangeName}_{type.Name.Underscore()}", e =>
|
||||
{
|
||||
foreach (var consumer in consumers)
|
||||
{
|
||||
configurator.ConfigureEndpoints(context, x => x.Exclude(consumer));
|
||||
var methodInfo = typeof(DependencyInjectionReceiveEndpointExtensions)
|
||||
.GetMethods()
|
||||
.Where(x => x.GetParameters()
|
||||
.Any(p => p.ParameterType == typeof(IServiceProvider)))
|
||||
.FirstOrDefault(x => x.Name == "Consumer" && x.IsGenericMethod);
|
||||
|
||||
var generic = methodInfo?.MakeGenericMethod(consumer);
|
||||
generic?.Invoke(e, new object[] {e, context, null});
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
return services;
|
||||
}
|
||||
}
|
||||
9
src/BuildingBlocks/MassTransit/RabbitMqOptions.cs
Normal file
9
src/BuildingBlocks/MassTransit/RabbitMqOptions.cs
Normal file
@ -0,0 +1,9 @@
|
||||
namespace BuildingBlocks.MassTransit;
|
||||
|
||||
public class RabbitMqOptions
|
||||
{
|
||||
public string HostName { get; set; }
|
||||
public string ExchangeName { get; set; }
|
||||
public string UserName { get; set; }
|
||||
public string Password { get; set; }
|
||||
}
|
||||
44
src/BuildingBlocks/Mongo/Extensions.cs
Normal file
44
src/BuildingBlocks/Mongo/Extensions.cs
Normal file
@ -0,0 +1,44 @@
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
namespace BuildingBlocks.Mongo
|
||||
{
|
||||
public static class Extensions
|
||||
{
|
||||
public static IServiceCollection AddMongoDbContext<TContext>(
|
||||
this IServiceCollection services, IConfiguration configuration, Action<MongoOptions>? configurator = null)
|
||||
where TContext : MongoDbContext
|
||||
{
|
||||
return services.AddMongoDbContext<TContext, TContext>(configuration, configurator);
|
||||
}
|
||||
|
||||
public static IServiceCollection AddMongoDbContext<TContextService, TContextImplementation>(
|
||||
this IServiceCollection services, IConfiguration configuration, Action<MongoOptions>? configurator = null)
|
||||
where TContextService : IMongoDbContext
|
||||
where TContextImplementation : MongoDbContext, TContextService
|
||||
{
|
||||
var mongoOptions = configuration.GetSection(nameof(MongoOptions)).Get<MongoOptions>() ?? new MongoOptions();
|
||||
|
||||
services.Configure<MongoOptions>(configuration.GetSection(nameof(MongoOptions)));
|
||||
if (configurator is { })
|
||||
{
|
||||
services.Configure(nameof(MongoOptions), configurator);
|
||||
}
|
||||
else
|
||||
{
|
||||
services.AddOptions<MongoOptions>().Bind(configuration.GetSection(nameof(MongoOptions)))
|
||||
.ValidateDataAnnotations();
|
||||
}
|
||||
|
||||
services.AddScoped(typeof(TContextService), typeof(TContextImplementation));
|
||||
services.AddScoped(typeof(TContextImplementation));
|
||||
|
||||
services.AddScoped<IMongoDbContext>(sp => sp.GetRequiredService<TContextService>());
|
||||
|
||||
services.AddTransient(typeof(IMongoRepository<,>), typeof(MongoRepository<,>));
|
||||
services.AddTransient(typeof(IMongoUnitOfWork<>), typeof(MongoUnitOfWork<>));
|
||||
|
||||
return services;
|
||||
}
|
||||
}
|
||||
}
|
||||
13
src/BuildingBlocks/Mongo/IMongoDbContext.cs
Normal file
13
src/BuildingBlocks/Mongo/IMongoDbContext.cs
Normal file
@ -0,0 +1,13 @@
|
||||
using MongoDB.Driver;
|
||||
|
||||
namespace BuildingBlocks.Mongo;
|
||||
|
||||
public interface IMongoDbContext : IDisposable
|
||||
{
|
||||
IMongoCollection<T> GetCollection<T>(string? name = null);
|
||||
Task<int> SaveChangesAsync(CancellationToken cancellationToken = default);
|
||||
Task BeginTransactionAsync(CancellationToken cancellationToken = default);
|
||||
Task CommitTransactionAsync(CancellationToken cancellationToken = default);
|
||||
Task RollbackTransaction(CancellationToken cancellationToken = default);
|
||||
void AddCommand(Func<Task> func);
|
||||
}
|
||||
8
src/BuildingBlocks/Mongo/IMongoRepository.cs
Normal file
8
src/BuildingBlocks/Mongo/IMongoRepository.cs
Normal file
@ -0,0 +1,8 @@
|
||||
using BuildingBlocks.Domain.Model;
|
||||
|
||||
namespace BuildingBlocks.Mongo;
|
||||
|
||||
public interface IMongoRepository<TEntity, in TId> : IRepository<TEntity, TId>
|
||||
where TEntity : class, IEntity<TId>
|
||||
{
|
||||
}
|
||||
5
src/BuildingBlocks/Mongo/IMongoUnitOfWork.cs
Normal file
5
src/BuildingBlocks/Mongo/IMongoUnitOfWork.cs
Normal file
@ -0,0 +1,5 @@
|
||||
namespace BuildingBlocks.Mongo;
|
||||
|
||||
public interface IMongoUnitOfWork<out TContext> : IUnitOfWork<TContext> where TContext : class, IMongoDbContext
|
||||
{
|
||||
}
|
||||
49
src/BuildingBlocks/Mongo/IRepository.cs
Normal file
49
src/BuildingBlocks/Mongo/IRepository.cs
Normal file
@ -0,0 +1,49 @@
|
||||
using System.Linq.Expressions;
|
||||
using BuildingBlocks.Domain.Model;
|
||||
|
||||
namespace BuildingBlocks.Mongo;
|
||||
|
||||
public interface IReadRepository<TEntity, in TId>
|
||||
where TEntity : class, IEntity<TId>
|
||||
{
|
||||
Task<TEntity?> FindByIdAsync(TId id, CancellationToken cancellationToken = default);
|
||||
|
||||
Task<TEntity?> FindOneAsync(
|
||||
Expression<Func<TEntity, bool>> predicate,
|
||||
CancellationToken cancellationToken = default);
|
||||
|
||||
Task<IReadOnlyList<TEntity>> FindAsync(
|
||||
Expression<Func<TEntity, bool>> predicate,
|
||||
CancellationToken cancellationToken = default);
|
||||
|
||||
Task<IReadOnlyList<TEntity>> GetAllAsync(CancellationToken cancellationToken = default);
|
||||
|
||||
public Task<IReadOnlyList<TEntity>> RawQuery(
|
||||
string query,
|
||||
CancellationToken cancellationToken = default,
|
||||
params object[] queryParams);
|
||||
}
|
||||
|
||||
public interface IWriteRepository<TEntity, in TId>
|
||||
where TEntity : class, IEntity<TId>
|
||||
{
|
||||
Task<TEntity> AddAsync(TEntity entity, CancellationToken cancellationToken = default);
|
||||
Task<TEntity> UpdateAsync(TEntity entity, CancellationToken cancellationToken = default);
|
||||
Task DeleteRangeAsync(IReadOnlyList<TEntity> entities, CancellationToken cancellationToken = default);
|
||||
Task DeleteAsync(Expression<Func<TEntity, bool>> predicate, CancellationToken cancellationToken = default);
|
||||
Task DeleteAsync(TEntity entity, CancellationToken cancellationToken = default);
|
||||
Task DeleteByIdAsync(TId id, CancellationToken cancellationToken = default);
|
||||
}
|
||||
|
||||
public interface IRepository<TEntity, in TId> :
|
||||
IReadRepository<TEntity, TId>,
|
||||
IWriteRepository<TEntity, TId>,
|
||||
IDisposable
|
||||
where TEntity : class, IEntity<TId>
|
||||
{
|
||||
}
|
||||
|
||||
public interface IRepository<TEntity> : IRepository<TEntity, long>
|
||||
where TEntity : class, IEntity<long>
|
||||
{
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user