This commit is contained in:
meysamhadeli 2022-05-07 19:33:06 +04:30
parent 23c459a89b
commit 31dc05f580
361 changed files with 17045 additions and 0 deletions

26
.dockerignore Normal file
View 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
View 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
View File

@ -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
View 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
View File

@ -0,0 +1,162 @@
[![License: MIT](https://img.shields.io/badge/License-MIT-brightgreen.svg?style=flat-square)](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.
![](./assets/Vertical-Slice-Architecture.jpg)
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)

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

View 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
View 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"
}
###

View 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

View 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
View 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
View 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"]

View 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>

View 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();

View 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"
}
}
}
}

View File

@ -0,0 +1,8 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
}
}

View 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"
}
}
}
}
}
}

View 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": "*"
}

View 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>

View 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;
}
}

View 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;
}
}
}

View 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;
}
}

View File

@ -0,0 +1,9 @@
using MediatR;
namespace BuildingBlocks.Caching;
public interface ICacheRequest
{
string CacheKey { get; }
DateTime? AbsoluteExpirationRelativeToNow { get; }
}

View File

@ -0,0 +1,11 @@
using System;
using System.Linq;
using MediatR;
namespace BuildingBlocks.Caching
{
public interface IInvalidateCacheRequest
{
string CacheKey { get; }
}
}

View 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;
}
}
}

View File

@ -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;

View File

@ -0,0 +1,5 @@
using BuildingBlocks.Domain.Event;
namespace BuildingBlocks.Contracts.EventBus.Messages;
public record UserCreated(long Id, string Name, string PassportNumber) : IIntegrationEvent;

View File

@ -0,0 +1,6 @@
namespace BuildingBlocks.Contracts.EventBus.Messages;
public class PassengerContracts
{
}

View File

@ -0,0 +1,5 @@
using BuildingBlocks.Domain.Event;
namespace BuildingBlocks.Contracts.EventBus.Messages;
public record BookingCreated(long Id) : IIntegrationEvent;

View 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
}

View 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
}

View 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;
}
}
}

View File

@ -0,0 +1,8 @@
namespace BuildingBlocks.Domain.Event;
[Flags]
public enum EventType
{
IntegrationEvent = 1,
DomainEvent = 2,
}

View File

@ -0,0 +1,8 @@
using MediatR;
namespace BuildingBlocks.Domain.Event;
public interface IDomainEvent : IEvent
{
}

View 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;
}

View File

@ -0,0 +1,5 @@
namespace BuildingBlocks.Domain.Event;
public interface IHaveIntegrationEvent
{
}

View File

@ -0,0 +1,9 @@
using MassTransit;
using MassTransit.Topology;
namespace BuildingBlocks.Domain.Event;
[ExcludeFromTopology]
public interface IIntegrationEvent : IEvent
{
}

View 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);
}

View 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);
}

View File

@ -0,0 +1,6 @@
using BuildingBlocks.Domain.Event;
namespace BuildingBlocks.Domain;
public record IntegrationEventWrapper<TDomainEventType>(TDomainEventType DomainEvent) : IIntegrationEvent
where TDomainEventType : IDomainEvent;

View 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; }
}
}

View 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; }
}

View 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; }
}
}

View 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; }
}

View File

@ -0,0 +1,7 @@
namespace BuildingBlocks.Domain.Model;
public interface IEntity<out TId>
{
TId Id { get; }
public bool IsDeleted { get; }
}

View 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;
}
}
}
}
}

View File

@ -0,0 +1,6 @@
namespace BuildingBlocks.EFCore;
public class DatabaseOptions
{
public string ConnectionString { get; set; }
}

View 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);
}
}
}

View 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;
}
}
}

View 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;
}
}
}

View 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;
}
}

View File

@ -0,0 +1,7 @@
namespace BuildingBlocks.EFCore
{
public interface IDataSeeder
{
Task SeedAllAsync();
}
}

View 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);
}

View File

@ -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);
}

View 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());
}
}

View File

@ -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;
}
}

View 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;
});
}

View 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
{
}

View File

@ -0,0 +1,7 @@
using BuildingBlocks.Domain.Event;
namespace BuildingBlocks.EventStoreDB.Events;
public interface IExternalEvent: IEvent
{
}

View File

@ -0,0 +1,6 @@
namespace BuildingBlocks.EventStoreDB.Events;
public interface IProjection
{
void When(object @event);
}

View 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)
{
}
}

View File

@ -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)!;
}
}

View 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}";
}
}

View 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);
}
}

View File

@ -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;
}

View File

@ -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);
}

View File

@ -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 })!;
}
}

View File

@ -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);
}
}

View File

@ -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);
}
}

View File

@ -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 { }))
);
}

View File

@ -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();
}

View File

@ -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
);
}
}

View File

@ -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");
}
}

View File

@ -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}";
}

View File

@ -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;
}
}

View File

@ -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);
}

View File

@ -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;
}
}

View 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);
}
}

View 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; }
}

View File

@ -0,0 +1,12 @@
using System;
namespace BuildingBlocks.Exception
{
public class BadRequestException : CustomException
{
public BadRequestException(string message) : base(message)
{
}
}
}

View 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;
}
}
}

View 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; }
}

View 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));
}
}
}

View 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)
{
}
}

View 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))
{
}
}
}

View File

@ -0,0 +1,9 @@
namespace BuildingBlocks.Exception
{
public class NotFoundException : CustomException
{
public NotFoundException(string message) : base(message)
{
}
}
}

View 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; }
}
}

View 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();
}

View 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);
}
}

View 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;
}
}

View 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;
}
}

View 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;
}
}

View 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;
}
}

View 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;
}
}

View 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; }
}

View 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;
}
}
}

View 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);
}

View 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>
{
}

View File

@ -0,0 +1,5 @@
namespace BuildingBlocks.Mongo;
public interface IMongoUnitOfWork<out TContext> : IUnitOfWork<TContext> where TContext : class, IMongoDbContext
{
}

View 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