diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..d94cd38 --- /dev/null +++ b/.dockerignore @@ -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 \ No newline at end of file diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..e2d4cfd --- /dev/null +++ b/.editorconfig @@ -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 or IComparer 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 diff --git a/.gitignore b/.gitignore index dfcfd56..b010f82 100644 --- a/.gitignore +++ b/.gitignore @@ -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/ \ No newline at end of file diff --git a/Directory.Build.props b/Directory.Build.props new file mode 100644 index 0000000..456ef29 --- /dev/null +++ b/Directory.Build.props @@ -0,0 +1,64 @@ + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + diff --git a/README.md b/README.md new file mode 100644 index 0000000..4b006e8 --- /dev/null +++ b/README.md @@ -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) diff --git a/assets/Vertical-Slice-Architecture.jpg b/assets/Vertical-Slice-Architecture.jpg new file mode 100644 index 0000000..fff1aec Binary files /dev/null and b/assets/Vertical-Slice-Architecture.jpg differ diff --git a/booking-microservices-sample.sln b/booking-microservices-sample.sln new file mode 100644 index 0000000..2bbb2ce --- /dev/null +++ b/booking-microservices-sample.sln @@ -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 diff --git a/booking.rest b/booking.rest new file mode 100644 index 0000000..1eaa443 --- /dev/null +++ b/booking.rest @@ -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" +} +### diff --git a/deployments/docker-compose/docker-compose.yaml b/deployments/docker-compose/docker-compose.yaml new file mode 100644 index 0000000..5b86409 --- /dev/null +++ b/deployments/docker-compose/docker-compose.yaml @@ -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 + diff --git a/deployments/docker-compose/infrastructure.yaml b/deployments/docker-compose/infrastructure.yaml new file mode 100644 index 0000000..d8f0e93 --- /dev/null +++ b/deployments/docker-compose/infrastructure.yaml @@ -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 \ No newline at end of file diff --git a/deployments/tye/tye.yml b/deployments/tye/tye.yml new file mode 100644 index 0000000..5cc89b7 --- /dev/null +++ b/deployments/tye/tye.yml @@ -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 diff --git a/src/ApiGateway/Dockerfile b/src/ApiGateway/Dockerfile new file mode 100644 index 0000000..090d394 --- /dev/null +++ b/src/ApiGateway/Dockerfile @@ -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"] + diff --git a/src/ApiGateway/src/ApiGateway.csproj b/src/ApiGateway/src/ApiGateway.csproj new file mode 100644 index 0000000..f9270e5 --- /dev/null +++ b/src/ApiGateway/src/ApiGateway.csproj @@ -0,0 +1,18 @@ + + + + net6.0 + enable + + + + + + + + + + + + + diff --git a/src/ApiGateway/src/Program.cs b/src/ApiGateway/src/Program.cs new file mode 100644 index 0000000..9704e91 --- /dev/null +++ b/src/ApiGateway/src/Program.cs @@ -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"); +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(); diff --git a/src/ApiGateway/src/Properties/launchSettings.json b/src/ApiGateway/src/Properties/launchSettings.json new file mode 100644 index 0000000..ac95e35 --- /dev/null +++ b/src/ApiGateway/src/Properties/launchSettings.json @@ -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" + } + } + } +} diff --git a/src/ApiGateway/src/appsettings.Development.json b/src/ApiGateway/src/appsettings.Development.json new file mode 100644 index 0000000..0c208ae --- /dev/null +++ b/src/ApiGateway/src/appsettings.Development.json @@ -0,0 +1,8 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + } +} diff --git a/src/ApiGateway/src/appsettings.docker.json b/src/ApiGateway/src/appsettings.docker.json new file mode 100644 index 0000000..d1addbf --- /dev/null +++ b/src/ApiGateway/src/appsettings.docker.json @@ -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" + } + } + } + } + } +} diff --git a/src/ApiGateway/src/appsettings.json b/src/ApiGateway/src/appsettings.json new file mode 100644 index 0000000..0fd1b8d --- /dev/null +++ b/src/ApiGateway/src/appsettings.json @@ -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": "*" +} diff --git a/src/BuildingBlocks/BuildingBlocks.csproj b/src/BuildingBlocks/BuildingBlocks.csproj new file mode 100644 index 0000000..97d850b --- /dev/null +++ b/src/BuildingBlocks/BuildingBlocks.csproj @@ -0,0 +1,120 @@ + + + + net6.0 + enable + + + + + + + + + + + + + + + + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + + + + + + + diff --git a/src/BuildingBlocks/CAP/Extensions.cs b/src/BuildingBlocks/CAP/Extensions.cs new file mode 100644 index 0000000..85469fc --- /dev/null +++ b/src/BuildingBlocks/CAP/Extensions.cs @@ -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(this IServiceCollection services) where TDbContext : DbContext + { + var rabbitMqOptions = services.GetOptions("RabbitMq"); + + services.AddCap(x => + { + x.UseEntityFramework(); + 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(); + 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; + } +} diff --git a/src/BuildingBlocks/Caching/CachingBehavior.cs b/src/BuildingBlocks/Caching/CachingBehavior.cs new file mode 100644 index 0000000..3a93d0f --- /dev/null +++ b/src/BuildingBlocks/Caching/CachingBehavior.cs @@ -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 : IPipelineBehavior + where TRequest : notnull, IRequest + where TResponse : notnull + { + private readonly ILogger> _logger; + private readonly IEasyCachingProvider _cachingProvider; + private readonly ICacheRequest _cacheRequest; + private readonly int defaultCacheExpirationInHours = 1; + + public CachingBehavior(IEasyCachingProviderFactory cachingFactory, + ILogger> logger, + ICacheRequest cacheRequest) + { + _logger = logger; + _cachingProvider = cachingFactory.GetCachingProvider("mem"); + _cacheRequest = cacheRequest; + } + + + public async Task Handle(TRequest request, CancellationToken cancellationToken, + RequestHandlerDelegate 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(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; + } + } +} diff --git a/src/BuildingBlocks/Caching/Extensions.cs b/src/BuildingBlocks/Caching/Extensions.cs new file mode 100644 index 0000000..e255838 --- /dev/null +++ b/src/BuildingBlocks/Caching/Extensions.cs @@ -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 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; + } +} diff --git a/src/BuildingBlocks/Caching/ICacheRequest.cs b/src/BuildingBlocks/Caching/ICacheRequest.cs new file mode 100644 index 0000000..67fbb2f --- /dev/null +++ b/src/BuildingBlocks/Caching/ICacheRequest.cs @@ -0,0 +1,9 @@ +using MediatR; + +namespace BuildingBlocks.Caching; + +public interface ICacheRequest +{ + string CacheKey { get; } + DateTime? AbsoluteExpirationRelativeToNow { get; } +} diff --git a/src/BuildingBlocks/Caching/IInvalidateCacheRequest.cs b/src/BuildingBlocks/Caching/IInvalidateCacheRequest.cs new file mode 100644 index 0000000..9b43396 --- /dev/null +++ b/src/BuildingBlocks/Caching/IInvalidateCacheRequest.cs @@ -0,0 +1,11 @@ +using System; +using System.Linq; +using MediatR; + +namespace BuildingBlocks.Caching +{ + public interface IInvalidateCacheRequest + { + string CacheKey { get; } + } +} diff --git a/src/BuildingBlocks/Caching/InvalidateCachingBehavior.cs b/src/BuildingBlocks/Caching/InvalidateCachingBehavior.cs new file mode 100644 index 0000000..8ccf866 --- /dev/null +++ b/src/BuildingBlocks/Caching/InvalidateCachingBehavior.cs @@ -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 : IPipelineBehavior + where TRequest : notnull, IRequest + where TResponse : notnull + { + private readonly ILogger> _logger; + private readonly IEasyCachingProvider _cachingProvider; + private readonly IInvalidateCacheRequest _invalidateCacheRequest; + + + public InvalidateCachingBehavior(IEasyCachingProviderFactory cachingFactory, + ILogger> logger, + IInvalidateCacheRequest invalidateCacheRequest) + { + _logger = logger; + _cachingProvider = cachingFactory.GetCachingProvider("mem"); + _invalidateCacheRequest = invalidateCacheRequest; + } + + public async Task Handle(TRequest request, CancellationToken cancellationToken, + RequestHandlerDelegate 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; + } + } +} diff --git a/src/BuildingBlocks/Contracts/EventBus.Messages/FlighContracts.cs b/src/BuildingBlocks/Contracts/EventBus.Messages/FlighContracts.cs new file mode 100644 index 0000000..9fab45b --- /dev/null +++ b/src/BuildingBlocks/Contracts/EventBus.Messages/FlighContracts.cs @@ -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; diff --git a/src/BuildingBlocks/Contracts/EventBus.Messages/IdentityContracts.cs b/src/BuildingBlocks/Contracts/EventBus.Messages/IdentityContracts.cs new file mode 100644 index 0000000..20e45f8 --- /dev/null +++ b/src/BuildingBlocks/Contracts/EventBus.Messages/IdentityContracts.cs @@ -0,0 +1,5 @@ +using BuildingBlocks.Domain.Event; + +namespace BuildingBlocks.Contracts.EventBus.Messages; + +public record UserCreated(long Id, string Name, string PassportNumber) : IIntegrationEvent; diff --git a/src/BuildingBlocks/Contracts/EventBus.Messages/PassengerContracts.cs b/src/BuildingBlocks/Contracts/EventBus.Messages/PassengerContracts.cs new file mode 100644 index 0000000..4e4b9d8 --- /dev/null +++ b/src/BuildingBlocks/Contracts/EventBus.Messages/PassengerContracts.cs @@ -0,0 +1,6 @@ +namespace BuildingBlocks.Contracts.EventBus.Messages; + +public class PassengerContracts +{ + +} \ No newline at end of file diff --git a/src/BuildingBlocks/Contracts/EventBus.Messages/ReservationContracts.cs b/src/BuildingBlocks/Contracts/EventBus.Messages/ReservationContracts.cs new file mode 100644 index 0000000..81cd928 --- /dev/null +++ b/src/BuildingBlocks/Contracts/EventBus.Messages/ReservationContracts.cs @@ -0,0 +1,5 @@ +using BuildingBlocks.Domain.Event; + +namespace BuildingBlocks.Contracts.EventBus.Messages; + +public record BookingCreated(long Id) : IIntegrationEvent; diff --git a/src/BuildingBlocks/Contracts/Grpc/FlightGrpcContracts.cs b/src/BuildingBlocks/Contracts/Grpc/FlightGrpcContracts.cs new file mode 100644 index 0000000..24ddeda --- /dev/null +++ b/src/BuildingBlocks/Contracts/Grpc/FlightGrpcContracts.cs @@ -0,0 +1,87 @@ +using MagicOnion; +using MessagePack; + +namespace BuildingBlocks.Contracts.Grpc; + + + public interface IFlightGrpcService : IService + { + UnaryResult GetById(long id); + UnaryResult> GetAvailableSeats(long flightId); + UnaryResult 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 + } + diff --git a/src/BuildingBlocks/Contracts/Grpc/PassengerGrpcContracts.cs b/src/BuildingBlocks/Contracts/Grpc/PassengerGrpcContracts.cs new file mode 100644 index 0000000..6bf08ed --- /dev/null +++ b/src/BuildingBlocks/Contracts/Grpc/PassengerGrpcContracts.cs @@ -0,0 +1,35 @@ +using MagicOnion; +using MessagePack; + +namespace BuildingBlocks.Contracts.Grpc; + +public interface IPassengerGrpcService : IService +{ + UnaryResult 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 +} diff --git a/src/BuildingBlocks/Domain/BusPublisher.cs b/src/BuildingBlocks/Domain/BusPublisher.cs new file mode 100644 index 0000000..d764e71 --- /dev/null +++ b/src/BuildingBlocks/Domain/BusPublisher.cs @@ -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 _logger; + private readonly IPublishEndpoint _publishEndpoint; + private readonly IHttpContextAccessor _httpContextAccessor; + private readonly IServiceScopeFactory _serviceScopeFactory; + + public BusPublisher(IServiceScopeFactory serviceScopeFactory, + IEventMapper eventMapper, + ILogger 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 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 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> MapDomainEventToIntegrationEventAsync( + IReadOnlyList events) + { + var wrappedIntegrationEvents = GetWrappedIntegrationEvents(events.ToList())?.ToList(); + if (wrappedIntegrationEvents?.Count > 0) + return Task.FromResult>(wrappedIntegrationEvents); + + var integrationEvents = new List(); + 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>(integrationEvents); + } + + private IEnumerable GetWrappedIntegrationEvents(IReadOnlyList 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; + } + } +} diff --git a/src/BuildingBlocks/Domain/Event/EventType.cs b/src/BuildingBlocks/Domain/Event/EventType.cs new file mode 100644 index 0000000..c18ccd0 --- /dev/null +++ b/src/BuildingBlocks/Domain/Event/EventType.cs @@ -0,0 +1,8 @@ +namespace BuildingBlocks.Domain.Event; + +[Flags] +public enum EventType +{ + IntegrationEvent = 1, + DomainEvent = 2, +} diff --git a/src/BuildingBlocks/Domain/Event/IDomainEvent.cs b/src/BuildingBlocks/Domain/Event/IDomainEvent.cs new file mode 100644 index 0000000..42f0c01 --- /dev/null +++ b/src/BuildingBlocks/Domain/Event/IDomainEvent.cs @@ -0,0 +1,8 @@ +using MediatR; + +namespace BuildingBlocks.Domain.Event; + +public interface IDomainEvent : IEvent +{ + +} diff --git a/src/BuildingBlocks/Domain/Event/IEvent.cs b/src/BuildingBlocks/Domain/Event/IEvent.cs new file mode 100644 index 0000000..a7d67c3 --- /dev/null +++ b/src/BuildingBlocks/Domain/Event/IEvent.cs @@ -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; +} diff --git a/src/BuildingBlocks/Domain/Event/IHaveIntegrationEvent.cs b/src/BuildingBlocks/Domain/Event/IHaveIntegrationEvent.cs new file mode 100644 index 0000000..a1f1ff8 --- /dev/null +++ b/src/BuildingBlocks/Domain/Event/IHaveIntegrationEvent.cs @@ -0,0 +1,5 @@ +namespace BuildingBlocks.Domain.Event; + +public interface IHaveIntegrationEvent +{ +} \ No newline at end of file diff --git a/src/BuildingBlocks/Domain/Event/IIntegrationEvent.cs b/src/BuildingBlocks/Domain/Event/IIntegrationEvent.cs new file mode 100644 index 0000000..0ab1a76 --- /dev/null +++ b/src/BuildingBlocks/Domain/Event/IIntegrationEvent.cs @@ -0,0 +1,9 @@ +using MassTransit; +using MassTransit.Topology; + +namespace BuildingBlocks.Domain.Event; + +[ExcludeFromTopology] +public interface IIntegrationEvent : IEvent +{ +} diff --git a/src/BuildingBlocks/Domain/IBusPublisher.cs b/src/BuildingBlocks/Domain/IBusPublisher.cs new file mode 100644 index 0000000..8d57005 --- /dev/null +++ b/src/BuildingBlocks/Domain/IBusPublisher.cs @@ -0,0 +1,12 @@ +using BuildingBlocks.Domain.Event; + +namespace BuildingBlocks.Domain; + +public interface IBusPublisher +{ + public Task SendAsync(IReadOnlyList domainEvents, CancellationToken cancellationToken = default); + public Task SendAsync(IDomainEvent domainEvent, CancellationToken cancellationToken = default); + + public Task SendAsync(IIntegrationEvent integrationEvent, CancellationToken cancellationToken = default); + public Task SendAsync(IReadOnlyList integrationEvents, CancellationToken cancellationToken = default); +} diff --git a/src/BuildingBlocks/Domain/IEventMapper.cs b/src/BuildingBlocks/Domain/IEventMapper.cs new file mode 100644 index 0000000..970b088 --- /dev/null +++ b/src/BuildingBlocks/Domain/IEventMapper.cs @@ -0,0 +1,9 @@ +using BuildingBlocks.Domain.Event; + +namespace BuildingBlocks.Domain; + +public interface IEventMapper +{ + IIntegrationEvent Map(IDomainEvent @event); + IEnumerable MapAll(IEnumerable events); +} diff --git a/src/BuildingBlocks/Domain/IntegrationEventWrapper.cs b/src/BuildingBlocks/Domain/IntegrationEventWrapper.cs new file mode 100644 index 0000000..abcf1cc --- /dev/null +++ b/src/BuildingBlocks/Domain/IntegrationEventWrapper.cs @@ -0,0 +1,6 @@ +using BuildingBlocks.Domain.Event; + +namespace BuildingBlocks.Domain; + +public record IntegrationEventWrapper(TDomainEventType DomainEvent) : IIntegrationEvent + where TDomainEventType : IDomainEvent; diff --git a/src/BuildingBlocks/Domain/Model/Aggregate.cs b/src/BuildingBlocks/Domain/Model/Aggregate.cs new file mode 100644 index 0000000..36f10d7 --- /dev/null +++ b/src/BuildingBlocks/Domain/Model/Aggregate.cs @@ -0,0 +1,34 @@ +using BuildingBlocks.Domain.Event; + +namespace BuildingBlocks.Domain.Model +{ + public abstract class Aggregate : Aggregate + { + } + + public abstract class Aggregate : Auditable, IAggregate + { + private readonly List _domainEvents = new(); + public IReadOnlyList 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; } + } +} diff --git a/src/BuildingBlocks/Domain/Model/Entity.cs b/src/BuildingBlocks/Domain/Model/Entity.cs new file mode 100644 index 0000000..793ec29 --- /dev/null +++ b/src/BuildingBlocks/Domain/Model/Entity.cs @@ -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; } +} diff --git a/src/BuildingBlocks/Domain/Model/IAggregate.cs b/src/BuildingBlocks/Domain/Model/IAggregate.cs new file mode 100644 index 0000000..5c1f03a --- /dev/null +++ b/src/BuildingBlocks/Domain/Model/IAggregate.cs @@ -0,0 +1,20 @@ +using BuildingBlocks.Domain.Event; +using BuildingBlocks.EventStoreDB.Events; + +namespace BuildingBlocks.Domain.Model +{ + public interface IAggregate : IProjection, IAuditable + { + IReadOnlyList DomainEvents { get; } + IEvent[] ClearDomainEvents(); + long Version { get; } + public bool IsDeleted { get; } + } + + public interface IAggregate : IAggregate + { + T Id { get; } + } +} + + diff --git a/src/BuildingBlocks/Domain/Model/IAuditable.cs b/src/BuildingBlocks/Domain/Model/IAuditable.cs new file mode 100644 index 0000000..15dadf9 --- /dev/null +++ b/src/BuildingBlocks/Domain/Model/IAuditable.cs @@ -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; } +} + + diff --git a/src/BuildingBlocks/Domain/Model/IEntity.cs b/src/BuildingBlocks/Domain/Model/IEntity.cs new file mode 100644 index 0000000..be3e512 --- /dev/null +++ b/src/BuildingBlocks/Domain/Model/IEntity.cs @@ -0,0 +1,7 @@ +namespace BuildingBlocks.Domain.Model; + +public interface IEntity +{ + TId Id { get; } + public bool IsDeleted { get; } +} diff --git a/src/BuildingBlocks/EFCore/AppDbContextBase.cs b/src/BuildingBlocks/EFCore/AppDbContextBase.cs new file mode 100644 index 0000000..2458209 --- /dev/null +++ b/src/BuildingBlocks/EFCore/AppDbContextBase.cs @@ -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 SaveChangesAsync(CancellationToken cancellationToken = default) + { + OnBeforeSaving(); + return base.SaveChangesAsync(cancellationToken); + } + + public IReadOnlyList GetDomainEvents() + { + var domainEntities = ChangeTracker + .Entries() + .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()) + { + 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; + } + } + } + } +} diff --git a/src/BuildingBlocks/EFCore/DatabaseOptions.cs b/src/BuildingBlocks/EFCore/DatabaseOptions.cs new file mode 100644 index 0000000..9041047 --- /dev/null +++ b/src/BuildingBlocks/EFCore/DatabaseOptions.cs @@ -0,0 +1,6 @@ +namespace BuildingBlocks.EFCore; + +public class DatabaseOptions +{ + public string ConnectionString { get; set; } +} diff --git a/src/BuildingBlocks/EFCore/DesignTimeDbContextFactoryBase.cs b/src/BuildingBlocks/EFCore/DesignTimeDbContextFactoryBase.cs new file mode 100644 index 0000000..c3c3e2b --- /dev/null +++ b/src/BuildingBlocks/EFCore/DesignTimeDbContextFactoryBase.cs @@ -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 : IDesignTimeDbContextFactory where TContext : DbContext + { + public TContext CreateDbContext(string[] args) + { + return Create(Directory.GetCurrentDirectory(), Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT")); + } + + protected abstract TContext CreateNewInstance(DbContextOptions 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(); + + Console.WriteLine("DesignTimeDbContextFactory.Create(string): Connection string: {0}", connectionString); + + optionsBuilder.UseSqlServer(connectionString); + + var options = optionsBuilder.Options; + return CreateNewInstance(options); + } + } +} \ No newline at end of file diff --git a/src/BuildingBlocks/EFCore/EfIdentityTxBehavior.cs b/src/BuildingBlocks/EFCore/EfIdentityTxBehavior.cs new file mode 100644 index 0000000..fba037e --- /dev/null +++ b/src/BuildingBlocks/EFCore/EfIdentityTxBehavior.cs @@ -0,0 +1,65 @@ +using System.Data; +using System.Text.Json; +using MediatR; +using Microsoft.Extensions.Logging; + +namespace BuildingBlocks.EFCore; + +public class EfIdentityTxBehavior : IPipelineBehavior + where TRequest : notnull, IRequest + where TResponse : notnull +{ + private readonly ILogger> _logger; + private readonly IDbContext _dbContextBase; + + public EfIdentityTxBehavior( + ILogger> logger, + IDbContext dbContextBase) + { + _logger = logger; + _dbContextBase = dbContextBase; + } + + public async Task Handle( + TRequest request, + CancellationToken cancellationToken, + RequestHandlerDelegate next) + { + _logger.LogInformation( + "{Prefix} Handled command {MediatrRequest}", + nameof(EfTxBehavior), + typeof(TRequest).FullName); + + _logger.LogDebug( + "{Prefix} Handled command {MediatrRequest} with content {RequestContent}", + nameof(EfTxBehavior), + typeof(TRequest).FullName, + JsonSerializer.Serialize(request)); + + _logger.LogInformation( + "{Prefix} Open the transaction for {MediatrRequest}", + nameof(EfTxBehavior), + typeof(TRequest).FullName); + + await _dbContextBase.BeginTransactionAsync(IsolationLevel.ReadCommitted, cancellationToken); + + try + { + var response = await next(); + + _logger.LogInformation( + "{Prefix} Executed the {MediatrRequest} request", + nameof(EfTxBehavior), + typeof(TRequest).FullName); + + await _dbContextBase.CommitTransactionAsync(cancellationToken); + + return response; + } + catch + { + await _dbContextBase.RollbackTransactionAsync(cancellationToken); + throw; + } + } +} diff --git a/src/BuildingBlocks/EFCore/EfTxBehavior.cs b/src/BuildingBlocks/EFCore/EfTxBehavior.cs new file mode 100644 index 0000000..6373ee9 --- /dev/null +++ b/src/BuildingBlocks/EFCore/EfTxBehavior.cs @@ -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 : IPipelineBehavior + where TRequest : notnull, IRequest + where TResponse : notnull +{ + private readonly ILogger> _logger; + private readonly IDbContext _dbContextBase; + private readonly IBusPublisher _busPublisher; + + public EfTxBehavior( + ILogger> logger, + IDbContext dbContextBase, + IBusPublisher busPublisher) + { + _logger = logger; + _dbContextBase = dbContextBase; + _busPublisher = busPublisher; + } + + public async Task Handle( + TRequest request, + CancellationToken cancellationToken, + RequestHandlerDelegate next) + { + _logger.LogInformation( + "{Prefix} Handled command {MediatrRequest}", + nameof(EfTxBehavior), + typeof(TRequest).FullName); + + _logger.LogDebug( + "{Prefix} Handled command {MediatrRequest} with content {RequestContent}", + nameof(EfTxBehavior), + typeof(TRequest).FullName, + JsonSerializer.Serialize(request)); + + _logger.LogInformation( + "{Prefix} Open the transaction for {MediatrRequest}", + nameof(EfTxBehavior), + typeof(TRequest).FullName); + + await _dbContextBase.BeginTransactionAsync(IsolationLevel.ReadCommitted, cancellationToken); + + try + { + var response = await next(); + + _logger.LogInformation( + "{Prefix} Executed the {MediatrRequest} request", + nameof(EfTxBehavior), + 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; + } + } +} diff --git a/src/BuildingBlocks/EFCore/Extensions.cs b/src/BuildingBlocks/EFCore/Extensions.cs new file mode 100644 index 0000000..31daaba --- /dev/null +++ b/src/BuildingBlocks/EFCore/Extensions.cs @@ -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( + this IServiceCollection services, + IConfiguration configuration, + Assembly migrationAssembly) + where TContext : AppDbContextBase + { + services.AddScoped(provider => provider.GetService()); + + services.AddDbContext(options => + options.UseSqlServer( + configuration.GetConnectionString("DefaultConnection"), + x => x.MigrationsAssembly(migrationAssembly.GetName().Name))); + + return services; + } +} diff --git a/src/BuildingBlocks/EFCore/IDataSeeder.cs b/src/BuildingBlocks/EFCore/IDataSeeder.cs new file mode 100644 index 0000000..c7ba594 --- /dev/null +++ b/src/BuildingBlocks/EFCore/IDataSeeder.cs @@ -0,0 +1,7 @@ +namespace BuildingBlocks.EFCore +{ + public interface IDataSeeder + { + Task SeedAllAsync(); + } +} \ No newline at end of file diff --git a/src/BuildingBlocks/EFCore/IDbContext.cs b/src/BuildingBlocks/EFCore/IDbContext.cs new file mode 100644 index 0000000..03d0e50 --- /dev/null +++ b/src/BuildingBlocks/EFCore/IDbContext.cs @@ -0,0 +1,16 @@ +using System.Data; +using BuildingBlocks.Domain.Event; +using Microsoft.EntityFrameworkCore; + +namespace BuildingBlocks.EFCore; + +public interface IDbContext +{ + DbSet Set() + where TEntity : class; + IReadOnlyList GetDomainEvents(); + Task BeginTransactionAsync(IsolationLevel isolationLevel, CancellationToken cancellationToken = default); + Task CommitTransactionAsync(CancellationToken cancellationToken = default); + Task RollbackTransactionAsync(CancellationToken cancellationToken = default); + Task SaveChangesAsync(CancellationToken cancellationToken = default); +} diff --git a/src/BuildingBlocks/EventStoreDB/BackgroundWorkers/BackgroundWorker.cs b/src/BuildingBlocks/EventStoreDB/BackgroundWorkers/BackgroundWorker.cs new file mode 100644 index 0000000..d0b25d0 --- /dev/null +++ b/src/BuildingBlocks/EventStoreDB/BackgroundWorkers/BackgroundWorker.cs @@ -0,0 +1,28 @@ +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; + +namespace BuildingBlocks.EventStoreDB.BackgroundWorkers; + +public class BackgroundWorker : BackgroundService +{ + private readonly ILogger logger; + private readonly Func perform; + + public BackgroundWorker( + ILogger logger, + Func 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); +} diff --git a/src/BuildingBlocks/EventStoreDB/Config.cs b/src/BuildingBlocks/EventStoreDB/Config.cs new file mode 100644 index 0000000..2ab8901 --- /dev/null +++ b/src/BuildingBlocks/EventStoreDB/Config.cs @@ -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(); + + services + .AddSingleton(new EventStoreClient(EventStoreClientSettings.Create(eventStoreDBConfig.ConnectionString))) + .AddScoped(typeof(IEventStoreDBRepository<>), typeof(EventStoreDBRepository<>)) + .AddTransient(); + + if (options?.UseInternalCheckpointing != false) + services.AddTransient(); + + return services; + } + + public static IServiceCollection AddEventStoreDBSubscriptionToAll( + this IServiceCollection services, + EventStoreDBSubscriptionToAllOptions? subscriptionOptions = null, + bool checkpointToEventStoreDB = true) + { + if (checkpointToEventStoreDB) + services.AddTransient(); + + return services.AddHostedService(serviceProvider => + { + var logger = + serviceProvider.GetRequiredService>(); + + var eventStoreDBSubscriptionToAll = + serviceProvider.GetRequiredService(); + + return new BackgroundWorker( + logger, + ct => + eventStoreDBSubscriptionToAll.SubscribeToAll( + subscriptionOptions ?? new EventStoreDBSubscriptionToAllOptions(), + ct + ) + ); + } + ); + } + + public static IServiceCollection AddProjections(this IServiceCollection services, params Assembly[] assembliesToScan) + { + services.AddSingleton(); + + RegisterProjections(services, assembliesToScan!); + + return services; + } + + private static void RegisterProjections(IServiceCollection services, Assembly[] assembliesToScan) + { + services.Scan(scan => scan + .FromAssemblies(assembliesToScan) + .AddClasses(classes => classes.AssignableTo()) // Filter classes + .AsImplementedInterfaces() + .WithTransientLifetime()); + } +} diff --git a/src/BuildingBlocks/EventStoreDB/Events/AggregateStreamExtensions.cs b/src/BuildingBlocks/EventStoreDB/Events/AggregateStreamExtensions.cs new file mode 100644 index 0000000..f04385c --- /dev/null +++ b/src/BuildingBlocks/EventStoreDB/Events/AggregateStreamExtensions.cs @@ -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 AggregateStream( + this EventStoreClient eventStore, + long id, + CancellationToken cancellationToken, + ulong? fromVersion = null + ) where T : class, IProjection + { + var readResult = eventStore.ReadStreamAsync( + Direction.Forwards, + StreamNameMapper.ToStreamId(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; + } +} diff --git a/src/BuildingBlocks/EventStoreDB/Events/EventTypeMapper.cs b/src/BuildingBlocks/EventStoreDB/Events/EventTypeMapper.cs new file mode 100644 index 0000000..3891dff --- /dev/null +++ b/src/BuildingBlocks/EventStoreDB/Events/EventTypeMapper.cs @@ -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 typeMap = new(); + private readonly ConcurrentDictionary typeNameMap = new(); + + public static void AddCustomMap(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() => 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; + }); +} diff --git a/src/BuildingBlocks/EventStoreDB/Events/IEventHandler.cs b/src/BuildingBlocks/EventStoreDB/Events/IEventHandler.cs new file mode 100644 index 0000000..bdb650e --- /dev/null +++ b/src/BuildingBlocks/EventStoreDB/Events/IEventHandler.cs @@ -0,0 +1,9 @@ +using BuildingBlocks.Domain.Event; +using MediatR; + +namespace BuildingBlocks.EventStoreDB.Events; + +public interface IEventHandler: INotificationHandler + where TEvent : IEvent +{ +} diff --git a/src/BuildingBlocks/EventStoreDB/Events/IExternalEvent.cs b/src/BuildingBlocks/EventStoreDB/Events/IExternalEvent.cs new file mode 100644 index 0000000..23cacfd --- /dev/null +++ b/src/BuildingBlocks/EventStoreDB/Events/IExternalEvent.cs @@ -0,0 +1,7 @@ +using BuildingBlocks.Domain.Event; + +namespace BuildingBlocks.EventStoreDB.Events; + +public interface IExternalEvent: IEvent +{ +} diff --git a/src/BuildingBlocks/EventStoreDB/Events/IProjection.cs b/src/BuildingBlocks/EventStoreDB/Events/IProjection.cs new file mode 100644 index 0000000..272a213 --- /dev/null +++ b/src/BuildingBlocks/EventStoreDB/Events/IProjection.cs @@ -0,0 +1,6 @@ +namespace BuildingBlocks.EventStoreDB.Events; + +public interface IProjection +{ + void When(object @event); +} diff --git a/src/BuildingBlocks/EventStoreDB/Events/StreamEvent.cs b/src/BuildingBlocks/EventStoreDB/Events/StreamEvent.cs new file mode 100644 index 0000000..763258a --- /dev/null +++ b/src/BuildingBlocks/EventStoreDB/Events/StreamEvent.cs @@ -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: StreamEvent where T: notnull +{ + public new T Data => (T)base.Data; + + public StreamEvent(T data, EventMetadata metadata) : base(data, metadata) + { + } +} + diff --git a/src/BuildingBlocks/EventStoreDB/Events/StreamEventExtensions.cs b/src/BuildingBlocks/EventStoreDB/Events/StreamEventExtensions.cs new file mode 100644 index 0000000..ccfdcc3 --- /dev/null +++ b/src/BuildingBlocks/EventStoreDB/Events/StreamEventExtensions.cs @@ -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)!; + } +} diff --git a/src/BuildingBlocks/EventStoreDB/Events/StreamNameMapper.cs b/src/BuildingBlocks/EventStoreDB/Events/StreamNameMapper.cs new file mode 100644 index 0000000..1013026 --- /dev/null +++ b/src/BuildingBlocks/EventStoreDB/Events/StreamNameMapper.cs @@ -0,0 +1,28 @@ +using System.Collections.Concurrent; + +namespace BuildingBlocks.EventStoreDB.Events; + +public class StreamNameMapper +{ + private static readonly StreamNameMapper Instance = new(); + + private readonly ConcurrentDictionary TypeNameMap = new(); + + public static void AddCustomMap(string mappedStreamName) => + AddCustomMap(typeof(TStream), mappedStreamName); + + public static void AddCustomMap(Type streamType, string mappedStreamName) + { + Instance.TypeNameMap.AddOrUpdate(streamType, mappedStreamName, (_, _) => mappedStreamName); + } + public static string ToStreamId(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}"; + } + +} diff --git a/src/BuildingBlocks/EventStoreDB/Extensions.cs b/src/BuildingBlocks/EventStoreDB/Extensions.cs new file mode 100644 index 0000000..6253391 --- /dev/null +++ b/src/BuildingBlocks/EventStoreDB/Extensions.cs @@ -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); + } +} diff --git a/src/BuildingBlocks/EventStoreDB/Projections/IProjectionProcessor.cs b/src/BuildingBlocks/EventStoreDB/Projections/IProjectionProcessor.cs new file mode 100644 index 0000000..45e505f --- /dev/null +++ b/src/BuildingBlocks/EventStoreDB/Projections/IProjectionProcessor.cs @@ -0,0 +1,10 @@ +using BuildingBlocks.EventStoreDB.Events; +using MediatR; + +namespace BuildingBlocks.EventStoreDB.Projections; + +public interface IProjectionProcessor +{ + Task ProcessEventAsync(StreamEvent streamEvent, CancellationToken cancellationToken = default) + where T : INotification; +} diff --git a/src/BuildingBlocks/EventStoreDB/Projections/IProjectionPublisher.cs b/src/BuildingBlocks/EventStoreDB/Projections/IProjectionPublisher.cs new file mode 100644 index 0000000..9d45823 --- /dev/null +++ b/src/BuildingBlocks/EventStoreDB/Projections/IProjectionPublisher.cs @@ -0,0 +1,12 @@ +using BuildingBlocks.EventStoreDB.Events; +using MediatR; + +namespace BuildingBlocks.EventStoreDB.Projections; + +public interface IProjectionPublisher +{ + Task PublishAsync(StreamEvent streamEvent, CancellationToken cancellationToken = default) + where T : INotification; + + Task PublishAsync(StreamEvent streamEvent, CancellationToken cancellationToken = default); +} diff --git a/src/BuildingBlocks/EventStoreDB/Projections/ProjectionPublisher.cs b/src/BuildingBlocks/EventStoreDB/Projections/ProjectionPublisher.cs new file mode 100644 index 0000000..26ea017 --- /dev/null +++ b/src/BuildingBlocks/EventStoreDB/Projections/ProjectionPublisher.cs @@ -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(StreamEvent streamEvent, CancellationToken cancellationToken = default) + where T : INotification + { + using var scope = _serviceProvider.CreateScope(); + var projectionsProcessors = scope.ServiceProvider.GetRequiredService>(); + 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 })!; + } +} diff --git a/src/BuildingBlocks/EventStoreDB/Repository/EventStoreDBRepository.cs b/src/BuildingBlocks/EventStoreDB/Repository/EventStoreDBRepository.cs new file mode 100644 index 0000000..712b78d --- /dev/null +++ b/src/BuildingBlocks/EventStoreDB/Repository/EventStoreDBRepository.cs @@ -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 where T : class, IAggregate +{ + Task Find(long id, CancellationToken cancellationToken); + Task Add(T aggregate, CancellationToken cancellationToken); + Task Update(T aggregate, long? expectedRevision = null, CancellationToken cancellationToken = default); + Task Delete(T aggregate, long? expectedRevision = null, CancellationToken cancellationToken = default); +} + +public class EventStoreDBRepository: IEventStoreDBRepository where T : class, IAggregate +{ + private readonly EventStoreClient eventStore; + + public EventStoreDBRepository(EventStoreClient eventStore) + { + this.eventStore = eventStore ?? throw new ArgumentNullException(nameof(eventStore)); + } + + public Task Find(long id, CancellationToken cancellationToken) => + eventStore.AggregateStream( + id, + cancellationToken + ); + + public async Task Add(T aggregate, CancellationToken cancellationToken = default) + { + var result = await eventStore.AppendToStreamAsync( + StreamNameMapper.ToStreamId(aggregate.Id), + StreamState.NoStream, + GetEventsToStore(aggregate), + cancellationToken: cancellationToken + ); + return result.NextExpectedStreamRevision; + } + + public async Task Update(T aggregate, long? expectedRevision = null, CancellationToken cancellationToken = default) + { + var nextVersion = expectedRevision ?? aggregate.Version; + + var result = await eventStore.AppendToStreamAsync( + StreamNameMapper.ToStreamId(aggregate.Id), + (ulong)nextVersion, + GetEventsToStore(aggregate), + cancellationToken: cancellationToken + ); + return result.NextExpectedStreamRevision; + } + + public Task Delete(T aggregate, long? expectedRevision = null, CancellationToken cancellationToken = default) => + Update(aggregate, expectedRevision, cancellationToken); + + private static IEnumerable GetEventsToStore(T aggregate) + { + var events = aggregate.ClearDomainEvents(); + + return events + .Select(EventStoreDBSerializer.ToJsonEventData); + } +} diff --git a/src/BuildingBlocks/EventStoreDB/Repository/RepositoryExtensions.cs b/src/BuildingBlocks/EventStoreDB/Repository/RepositoryExtensions.cs new file mode 100644 index 0000000..c2c0343 --- /dev/null +++ b/src/BuildingBlocks/EventStoreDB/Repository/RepositoryExtensions.cs @@ -0,0 +1,33 @@ +using BuildingBlocks.Domain.Model; +using BuildingBlocks.Exception; + +namespace BuildingBlocks.EventStoreDB.Repository; + +public static class RepositoryExtensions +{ + public static async Task Get( + this IEventStoreDBRepository repository, + long id, + CancellationToken cancellationToken + ) where T : class, IAggregate + { + var entity = await repository.Find(id, cancellationToken); + + return entity ?? throw AggregateNotFoundException.For(id); + } + + public static async Task GetAndUpdate( + this IEventStoreDBRepository repository, + long id, + Action action, + long? expectedVersion = null, + CancellationToken cancellationToken = default + ) where T : class, IAggregate + { + var entity = await repository.Get(id, cancellationToken); + + action(entity); + + return await repository.Update(entity, expectedVersion, cancellationToken); + } +} diff --git a/src/BuildingBlocks/EventStoreDB/Serialization/EventStoreDBSerializer.cs b/src/BuildingBlocks/EventStoreDB/Serialization/EventStoreDBSerializer.cs new file mode 100644 index 0000000..fbdd8a0 --- /dev/null +++ b/src/BuildingBlocks/EventStoreDB/Serialization/EventStoreDBSerializer.cs @@ -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(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 { })) + ); +} diff --git a/src/BuildingBlocks/EventStoreDB/Serialization/JsonObjectContractProvider.cs b/src/BuildingBlocks/EventStoreDB/Serialization/JsonObjectContractProvider.cs new file mode 100644 index 0000000..b4fc37c --- /dev/null +++ b/src/BuildingBlocks/EventStoreDB/Serialization/JsonObjectContractProvider.cs @@ -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 Constructors = new(); + + public static JsonObjectContract UsingNonDefaultConstructor( + JsonObjectContract contract, + Type objectType, + Func> 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 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()); + + 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(); +} diff --git a/src/BuildingBlocks/EventStoreDB/Serialization/NonDefaultConstructorContractResolver.cs b/src/BuildingBlocks/EventStoreDB/Serialization/NonDefaultConstructorContractResolver.cs new file mode 100644 index 0000000..b651a47 --- /dev/null +++ b/src/BuildingBlocks/EventStoreDB/Serialization/NonDefaultConstructorContractResolver.cs @@ -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 + ); + } +} diff --git a/src/BuildingBlocks/EventStoreDB/Serialization/SerializationExtensions.cs b/src/BuildingBlocks/EventStoreDB/Serialization/SerializationExtensions.cs new file mode 100644 index 0000000..4519ee1 --- /dev/null +++ b/src/BuildingBlocks/EventStoreDB/Serialization/SerializationExtensions.cs @@ -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; + } + + /// + /// Deserialize object from json with JsonNet + /// + /// Type of the deserialized object + /// json string + /// deserialized object + public static T FromJson(this string json) + { + return JsonConvert.DeserializeObject(json, + new JsonSerializerSettings().WithNonDefaultConstructorContractResolver())!; + } + + + /// + /// Deserialize object from json with JsonNet + /// + /// Type of the deserialized object + /// json string + /// object type + /// deserialized object + public static object FromJson(this string json, Type type) + { + return JsonConvert.DeserializeObject(json, type, + new JsonSerializerSettings().WithNonDefaultConstructorContractResolver())!; + } + + /// + /// Serialize object to json with JsonNet + /// + /// object to serialize + /// json string + public static string ToJson(this object obj) + { + return JsonConvert.SerializeObject(obj); + } + + /// + /// Serialize object to json with JsonNet + /// + /// object to serialize + /// json string + public static StringContent ToJsonStringContent(this object obj) + { + return new StringContent(obj.ToJson(), Encoding.UTF8, "application/json"); + } +} diff --git a/src/BuildingBlocks/EventStoreDB/Subscriptions/EventStoreDBSubscriptionCheckpointRepository.cs b/src/BuildingBlocks/EventStoreDB/Subscriptions/EventStoreDBSubscriptionCheckpointRepository.cs new file mode 100644 index 0000000..0a0b727 --- /dev/null +++ b/src/BuildingBlocks/EventStoreDB/Subscriptions/EventStoreDBSubscriptionCheckpointRepository.cs @@ -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 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()?.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}"; +} diff --git a/src/BuildingBlocks/EventStoreDB/Subscriptions/EventStoreDBSubscriptionToAll.cs b/src/BuildingBlocks/EventStoreDB/Subscriptions/EventStoreDBSubscriptionToAll.cs new file mode 100644 index 0000000..d371806 --- /dev/null +++ b/src/BuildingBlocks/EventStoreDB/Subscriptions/EventStoreDBSubscriptionToAll.cs @@ -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? 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 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 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()) return false; + + logger.LogInformation("Checkpoint event - ignoring"); + return true; + } +} diff --git a/src/BuildingBlocks/EventStoreDB/Subscriptions/ISubscriptionCheckpointRepository.cs b/src/BuildingBlocks/EventStoreDB/Subscriptions/ISubscriptionCheckpointRepository.cs new file mode 100644 index 0000000..d65dd46 --- /dev/null +++ b/src/BuildingBlocks/EventStoreDB/Subscriptions/ISubscriptionCheckpointRepository.cs @@ -0,0 +1,8 @@ +namespace BuildingBlocks.EventStoreDB.Subscriptions; + +public interface ISubscriptionCheckpointRepository +{ + ValueTask Load(string subscriptionId, CancellationToken ct); + + ValueTask Store(string subscriptionId, ulong position, CancellationToken ct); +} diff --git a/src/BuildingBlocks/EventStoreDB/Subscriptions/InMemorySubscriptionCheckpointRepository.cs b/src/BuildingBlocks/EventStoreDB/Subscriptions/InMemorySubscriptionCheckpointRepository.cs new file mode 100644 index 0000000..9b90fa1 --- /dev/null +++ b/src/BuildingBlocks/EventStoreDB/Subscriptions/InMemorySubscriptionCheckpointRepository.cs @@ -0,0 +1,20 @@ +using System.Collections.Concurrent; + +namespace BuildingBlocks.EventStoreDB.Subscriptions; + +public class InMemorySubscriptionCheckpointRepository : ISubscriptionCheckpointRepository +{ + private readonly ConcurrentDictionary checkpoints = new(); + + public ValueTask 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; + } +} diff --git a/src/BuildingBlocks/Exception/AggregateNotFoundException.cs b/src/BuildingBlocks/Exception/AggregateNotFoundException.cs new file mode 100644 index 0000000..9ebadc9 --- /dev/null +++ b/src/BuildingBlocks/Exception/AggregateNotFoundException.cs @@ -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(long id) + { + return new AggregateNotFoundException(typeof(T).Name, id); + } +} diff --git a/src/BuildingBlocks/Exception/AppException.cs b/src/BuildingBlocks/Exception/AppException.cs new file mode 100644 index 0000000..397ce43 --- /dev/null +++ b/src/BuildingBlocks/Exception/AppException.cs @@ -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; } +} diff --git a/src/BuildingBlocks/Exception/BadRequestException.cs b/src/BuildingBlocks/Exception/BadRequestException.cs new file mode 100644 index 0000000..f1fe597 --- /dev/null +++ b/src/BuildingBlocks/Exception/BadRequestException.cs @@ -0,0 +1,12 @@ +using System; + +namespace BuildingBlocks.Exception +{ + public class BadRequestException : CustomException + { + public BadRequestException(string message) : base(message) + { + + } + } +} \ No newline at end of file diff --git a/src/BuildingBlocks/Exception/ConflictException.cs b/src/BuildingBlocks/Exception/ConflictException.cs new file mode 100644 index 0000000..a2399c1 --- /dev/null +++ b/src/BuildingBlocks/Exception/ConflictException.cs @@ -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; + } + } +} \ No newline at end of file diff --git a/src/BuildingBlocks/Exception/CustomException.cs b/src/BuildingBlocks/Exception/CustomException.cs new file mode 100644 index 0000000..fb891c4 --- /dev/null +++ b/src/BuildingBlocks/Exception/CustomException.cs @@ -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; } +} diff --git a/src/BuildingBlocks/Exception/GrpcExceptionInterceptor.cs b/src/BuildingBlocks/Exception/GrpcExceptionInterceptor.cs new file mode 100644 index 0000000..83e37ba --- /dev/null +++ b/src/BuildingBlocks/Exception/GrpcExceptionInterceptor.cs @@ -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 _logger; + + public GrpcExceptionInterceptor(ILogger logger) + { + _logger = logger; + } + + public override async Task UnaryServerHandler( + TRequest request, + ServerCallContext context, + UnaryServerMethod continuation) + { + try + { + return await continuation(request, context); + } + catch (System.Exception exception) + { + throw new RpcException(new Status(StatusCode.Cancelled, exception.Message)); + } + } +} diff --git a/src/BuildingBlocks/Exception/IdentityException.cs b/src/BuildingBlocks/Exception/IdentityException.cs new file mode 100644 index 0000000..987cd05 --- /dev/null +++ b/src/BuildingBlocks/Exception/IdentityException.cs @@ -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) + { + } +} \ No newline at end of file diff --git a/src/BuildingBlocks/Exception/InternalServerException.cs b/src/BuildingBlocks/Exception/InternalServerException.cs new file mode 100644 index 0000000..963cebc --- /dev/null +++ b/src/BuildingBlocks/Exception/InternalServerException.cs @@ -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)) + { + } + } +} \ No newline at end of file diff --git a/src/BuildingBlocks/Exception/NotFoundException.cs b/src/BuildingBlocks/Exception/NotFoundException.cs new file mode 100644 index 0000000..e9dc06b --- /dev/null +++ b/src/BuildingBlocks/Exception/NotFoundException.cs @@ -0,0 +1,9 @@ +namespace BuildingBlocks.Exception +{ + public class NotFoundException : CustomException + { + public NotFoundException(string message) : base(message) + { + } + } +} \ No newline at end of file diff --git a/src/BuildingBlocks/Exception/ValidationException.cs b/src/BuildingBlocks/Exception/ValidationException.cs new file mode 100644 index 0000000..baaeb48 --- /dev/null +++ b/src/BuildingBlocks/Exception/ValidationException.cs @@ -0,0 +1,13 @@ +using BuildingBlocks.Validation; + +namespace BuildingBlocks.Exception +{ + public class ValidationException : CustomException + { + public ValidationException(ValidationResultModel validationResultModel) + { + ValidationResultModel = validationResultModel; + } + public ValidationResultModel ValidationResultModel { get; } + } +} \ No newline at end of file diff --git a/src/BuildingBlocks/IdsGenerator/SnowFlakIdGenerator.cs b/src/BuildingBlocks/IdsGenerator/SnowFlakIdGenerator.cs new file mode 100644 index 0000000..1209c32 --- /dev/null +++ b/src/BuildingBlocks/IdsGenerator/SnowFlakIdGenerator.cs @@ -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(); +} diff --git a/src/BuildingBlocks/Jwt/AuthHeaderHandler.cs b/src/BuildingBlocks/Jwt/AuthHeaderHandler.cs new file mode 100644 index 0000000..e8f06da --- /dev/null +++ b/src/BuildingBlocks/Jwt/AuthHeaderHandler.cs @@ -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 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); + } +} diff --git a/src/BuildingBlocks/Jwt/JwtExtensions.cs b/src/BuildingBlocks/Jwt/JwtExtensions.cs new file mode 100644 index 0000000..f903cc7 --- /dev/null +++ b/src/BuildingBlocks/Jwt/JwtExtensions.cs @@ -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("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; + } +} diff --git a/src/BuildingBlocks/Logging/Extensions.cs b/src/BuildingBlocks/Logging/Extensions.cs new file mode 100644 index 0000000..97f85ec --- /dev/null +++ b/src/BuildingBlocks/Logging/Extensions.cs @@ -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; + } +} diff --git a/src/BuildingBlocks/Logging/LoggingBehavior.cs b/src/BuildingBlocks/Logging/LoggingBehavior.cs new file mode 100644 index 0000000..e56c2c1 --- /dev/null +++ b/src/BuildingBlocks/Logging/LoggingBehavior.cs @@ -0,0 +1,42 @@ +using System.Diagnostics; +using MediatR; +using Microsoft.Extensions.Logging; + +namespace BuildingBlocks.Logging; + +public class LoggingBehavior : IPipelineBehavior + where TRequest : notnull, IRequest + where TResponse : notnull +{ + private readonly ILogger> _logger; + + public LoggingBehavior(ILogger> logger) + { + _logger = logger; + } + + public async Task Handle( + TRequest request, + CancellationToken cancellationToken, + RequestHandlerDelegate next) + { + const string prefix = nameof(LoggingBehavior); + + _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; + } +} diff --git a/src/BuildingBlocks/Mapster/Extensions.cs b/src/BuildingBlocks/Mapster/Extensions.cs new file mode 100644 index 0000000..6d71074 --- /dev/null +++ b/src/BuildingBlocks/Mapster/Extensions.cs @@ -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(mapperConfig); + + return services; + } +} diff --git a/src/BuildingBlocks/MassTransit/Extensions.cs b/src/BuildingBlocks/MassTransit/Extensions.cs new file mode 100644 index 0000000..e13711e --- /dev/null +++ b/src/BuildingBlocks/MassTransit/Extensions.cs @@ -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("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; + } +} diff --git a/src/BuildingBlocks/MassTransit/RabbitMqOptions.cs b/src/BuildingBlocks/MassTransit/RabbitMqOptions.cs new file mode 100644 index 0000000..6156b6a --- /dev/null +++ b/src/BuildingBlocks/MassTransit/RabbitMqOptions.cs @@ -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; } +} \ No newline at end of file diff --git a/src/BuildingBlocks/Mongo/Extensions.cs b/src/BuildingBlocks/Mongo/Extensions.cs new file mode 100644 index 0000000..1de42c7 --- /dev/null +++ b/src/BuildingBlocks/Mongo/Extensions.cs @@ -0,0 +1,44 @@ +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; + +namespace BuildingBlocks.Mongo +{ + public static class Extensions + { + public static IServiceCollection AddMongoDbContext( + this IServiceCollection services, IConfiguration configuration, Action? configurator = null) + where TContext : MongoDbContext + { + return services.AddMongoDbContext(configuration, configurator); + } + + public static IServiceCollection AddMongoDbContext( + this IServiceCollection services, IConfiguration configuration, Action? configurator = null) + where TContextService : IMongoDbContext + where TContextImplementation : MongoDbContext, TContextService + { + var mongoOptions = configuration.GetSection(nameof(MongoOptions)).Get() ?? new MongoOptions(); + + services.Configure(configuration.GetSection(nameof(MongoOptions))); + if (configurator is { }) + { + services.Configure(nameof(MongoOptions), configurator); + } + else + { + services.AddOptions().Bind(configuration.GetSection(nameof(MongoOptions))) + .ValidateDataAnnotations(); + } + + services.AddScoped(typeof(TContextService), typeof(TContextImplementation)); + services.AddScoped(typeof(TContextImplementation)); + + services.AddScoped(sp => sp.GetRequiredService()); + + services.AddTransient(typeof(IMongoRepository<,>), typeof(MongoRepository<,>)); + services.AddTransient(typeof(IMongoUnitOfWork<>), typeof(MongoUnitOfWork<>)); + + return services; + } + } +} diff --git a/src/BuildingBlocks/Mongo/IMongoDbContext.cs b/src/BuildingBlocks/Mongo/IMongoDbContext.cs new file mode 100644 index 0000000..9449ad4 --- /dev/null +++ b/src/BuildingBlocks/Mongo/IMongoDbContext.cs @@ -0,0 +1,13 @@ +using MongoDB.Driver; + +namespace BuildingBlocks.Mongo; + +public interface IMongoDbContext : IDisposable +{ + IMongoCollection GetCollection(string? name = null); + Task SaveChangesAsync(CancellationToken cancellationToken = default); + Task BeginTransactionAsync(CancellationToken cancellationToken = default); + Task CommitTransactionAsync(CancellationToken cancellationToken = default); + Task RollbackTransaction(CancellationToken cancellationToken = default); + void AddCommand(Func func); +} diff --git a/src/BuildingBlocks/Mongo/IMongoRepository.cs b/src/BuildingBlocks/Mongo/IMongoRepository.cs new file mode 100644 index 0000000..4be4cfb --- /dev/null +++ b/src/BuildingBlocks/Mongo/IMongoRepository.cs @@ -0,0 +1,8 @@ +using BuildingBlocks.Domain.Model; + +namespace BuildingBlocks.Mongo; + +public interface IMongoRepository : IRepository + where TEntity : class, IEntity +{ +} diff --git a/src/BuildingBlocks/Mongo/IMongoUnitOfWork.cs b/src/BuildingBlocks/Mongo/IMongoUnitOfWork.cs new file mode 100644 index 0000000..e9a46bd --- /dev/null +++ b/src/BuildingBlocks/Mongo/IMongoUnitOfWork.cs @@ -0,0 +1,5 @@ +namespace BuildingBlocks.Mongo; + +public interface IMongoUnitOfWork : IUnitOfWork where TContext : class, IMongoDbContext +{ +} diff --git a/src/BuildingBlocks/Mongo/IRepository.cs b/src/BuildingBlocks/Mongo/IRepository.cs new file mode 100644 index 0000000..fc0982a --- /dev/null +++ b/src/BuildingBlocks/Mongo/IRepository.cs @@ -0,0 +1,49 @@ +using System.Linq.Expressions; +using BuildingBlocks.Domain.Model; + +namespace BuildingBlocks.Mongo; + +public interface IReadRepository + where TEntity : class, IEntity +{ + Task FindByIdAsync(TId id, CancellationToken cancellationToken = default); + + Task FindOneAsync( + Expression> predicate, + CancellationToken cancellationToken = default); + + Task> FindAsync( + Expression> predicate, + CancellationToken cancellationToken = default); + + Task> GetAllAsync(CancellationToken cancellationToken = default); + + public Task> RawQuery( + string query, + CancellationToken cancellationToken = default, + params object[] queryParams); +} + +public interface IWriteRepository + where TEntity : class, IEntity +{ + Task AddAsync(TEntity entity, CancellationToken cancellationToken = default); + Task UpdateAsync(TEntity entity, CancellationToken cancellationToken = default); + Task DeleteRangeAsync(IReadOnlyList entities, CancellationToken cancellationToken = default); + Task DeleteAsync(Expression> predicate, CancellationToken cancellationToken = default); + Task DeleteAsync(TEntity entity, CancellationToken cancellationToken = default); + Task DeleteByIdAsync(TId id, CancellationToken cancellationToken = default); +} + +public interface IRepository : + IReadRepository, + IWriteRepository, + IDisposable + where TEntity : class, IEntity +{ +} + +public interface IRepository : IRepository + where TEntity : class, IEntity +{ +} diff --git a/src/BuildingBlocks/Mongo/ITransactionAble.cs b/src/BuildingBlocks/Mongo/ITransactionAble.cs new file mode 100644 index 0000000..8cff62d --- /dev/null +++ b/src/BuildingBlocks/Mongo/ITransactionAble.cs @@ -0,0 +1,8 @@ +namespace BuildingBlocks.Mongo; + +public interface ITransactionAble +{ + Task BeginTransactionAsync(CancellationToken cancellationToken = default); + Task RollbackTransactionAsync(CancellationToken cancellationToken = default); + Task CommitTransactionAsync(CancellationToken cancellationToken = default); +} diff --git a/src/BuildingBlocks/Mongo/IUnitOfWork.cs b/src/BuildingBlocks/Mongo/IUnitOfWork.cs new file mode 100644 index 0000000..76752c8 --- /dev/null +++ b/src/BuildingBlocks/Mongo/IUnitOfWork.cs @@ -0,0 +1,13 @@ +namespace BuildingBlocks.Mongo; + +public interface IUnitOfWork : IDisposable +{ + Task BeginTransactionAsync(CancellationToken cancellationToken = default); + Task CommitAsync(CancellationToken cancellationToken = default); +} + +public interface IUnitOfWork : IUnitOfWork + where TContext : class +{ + TContext Context { get; } +} diff --git a/src/BuildingBlocks/Mongo/ImmutablePocoConvention.cs b/src/BuildingBlocks/Mongo/ImmutablePocoConvention.cs new file mode 100644 index 0000000..55d5222 --- /dev/null +++ b/src/BuildingBlocks/Mongo/ImmutablePocoConvention.cs @@ -0,0 +1,96 @@ +using System.Reflection; +using MongoDB.Bson.Serialization; +using MongoDB.Bson.Serialization.Conventions; + +namespace BuildingBlocks.Mongo +{ + /// + /// A convention that map all read only properties for which a matching constructor is found. + /// Also matching constructors are mapped. + /// + public class ImmutablePocoConvention : ConventionBase, IClassMapConvention + { + private readonly BindingFlags _bindingFlags; + + public ImmutablePocoConvention() + : this(BindingFlags.Instance | BindingFlags.Public) + { + } + + public ImmutablePocoConvention(BindingFlags bindingFlags) + { + _bindingFlags = bindingFlags | BindingFlags.DeclaredOnly; + } + + public void Apply(BsonClassMap classMap) + { + var readOnlyProperties = classMap.ClassType.GetTypeInfo() + .GetProperties(_bindingFlags) + .Where(p => IsReadOnlyProperty(classMap, p)) + .ToList(); + + foreach (var constructor in classMap.ClassType.GetConstructors()) + { + // If we found a matching constructor then we map it and all the readonly properties + var matchProperties = GetMatchingProperties(constructor, readOnlyProperties); + if (matchProperties.Any()) + { + // Map constructor + classMap.MapConstructor(constructor); + + // Map properties + foreach (var p in matchProperties) + classMap.MapMember(p); + } + } + } + + private static List GetMatchingProperties( + ConstructorInfo constructor, + List properties) + { + var matchProperties = new List(); + + var ctorParameters = constructor.GetParameters(); + foreach (var ctorParameter in ctorParameters) + { + var matchProperty = properties.FirstOrDefault(p => ParameterMatchProperty(ctorParameter, p)); + if (matchProperty == null) + return new List(); + + matchProperties.Add(matchProperty); + } + + return matchProperties; + } + + + private static bool ParameterMatchProperty(ParameterInfo parameter, PropertyInfo property) + { + return string.Equals(property.Name, parameter.Name, System.StringComparison.InvariantCultureIgnoreCase) + && parameter.ParameterType == property.PropertyType; + } + + private static bool IsReadOnlyProperty(BsonClassMap classMap, PropertyInfo propertyInfo) + { + // we can't read + if (!propertyInfo.CanRead) + return false; + + // we can write (already handled by the default convention...) + if (propertyInfo.CanWrite) + return false; + + // skip indexers + if (propertyInfo.GetIndexParameters().Length != 0) + return false; + + // skip overridden properties (they are already included by the base class) + var getMethodInfo = propertyInfo.GetMethod; + if (getMethodInfo.IsVirtual && getMethodInfo.GetBaseDefinition().DeclaringType != classMap.ClassType) + return false; + + return true; + } + } +} diff --git a/src/BuildingBlocks/Mongo/MicroBootstrap.Persistence.Mongo.csproj b/src/BuildingBlocks/Mongo/MicroBootstrap.Persistence.Mongo.csproj new file mode 100644 index 0000000..4005485 --- /dev/null +++ b/src/BuildingBlocks/Mongo/MicroBootstrap.Persistence.Mongo.csproj @@ -0,0 +1,14 @@ + + + + net6.0 + enable + enable + + + + + + + + diff --git a/src/BuildingBlocks/Mongo/MongoDbContext.cs b/src/BuildingBlocks/Mongo/MongoDbContext.cs new file mode 100644 index 0000000..1c59a41 --- /dev/null +++ b/src/BuildingBlocks/Mongo/MongoDbContext.cs @@ -0,0 +1,142 @@ +using MongoDB.Bson; +using MongoDB.Bson.Serialization.Conventions; +using MongoDB.Driver; + +namespace BuildingBlocks.Mongo; + +// https://www.thecodebuzz.com/mongodb-repository-implementation-unit-testing-net-core-example/ + +public class MongoDbContext : IMongoDbContext +{ + public IClientSessionHandle? Session { get; set; } + public IMongoDatabase Database { get; } + public IMongoClient MongoClient { get; } + protected readonly IList> _commands; + + public MongoDbContext(MongoOptions options) + { + RegisterConventions(); + + MongoClient = new MongoClient(options.ConnectionString); + var databaseName = options.DatabaseName; + Database = MongoClient.GetDatabase(databaseName); + + // Every command will be stored and it'll be processed at SaveChanges + _commands = new List>(); + } + + private static void RegisterConventions() + { + ConventionRegistry.Register( + "conventions", + new ConventionPack + { + new CamelCaseElementNameConvention(), + new IgnoreExtraElementsConvention(true), + new EnumRepresentationConvention(BsonType.String), + new IgnoreIfDefaultConvention(false), + new ImmutablePocoConvention() + }, _ => true); + } + + public IMongoCollection GetCollection(string? name = null) + { + return Database.GetCollection(name ?? typeof(T).Name.ToLower()); + } + + public void Dispose() + { + while (Session is { IsInTransaction: true }) + Thread.Sleep(TimeSpan.FromMilliseconds(100)); + + GC.SuppressFinalize(this); + } + + public async Task SaveChangesAsync(CancellationToken cancellationToken = default) + { + var result = _commands.Count; + + using (Session = await MongoClient.StartSessionAsync(cancellationToken: cancellationToken)) + { + Session.StartTransaction(); + + try + { + var commandTasks = _commands.Select(c => c()); + + await Task.WhenAll(commandTasks); + + await Session.CommitTransactionAsync(cancellationToken); + } + catch (System.Exception ex) + { + await Session.AbortTransactionAsync(cancellationToken); + _commands.Clear(); + throw; + } + } + + _commands.Clear(); + return result; + } + + public async Task BeginTransactionAsync(CancellationToken cancellationToken = default) + { + Session = await MongoClient.StartSessionAsync(cancellationToken: cancellationToken); + Session.StartTransaction(); + } + + public async Task CommitTransactionAsync(CancellationToken cancellationToken = default) + { + if (Session is { IsInTransaction: true }) + await Session.CommitTransactionAsync(cancellationToken); + + Session?.Dispose(); + } + + public async Task RollbackTransaction(CancellationToken cancellationToken = default) + { + await Session?.AbortTransactionAsync(cancellationToken)!; + } + + public void AddCommand(Func func) + { + _commands.Add(func); + } + + public async Task ExecuteTransactionalAsync(Func action, CancellationToken cancellationToken = default) + { + await BeginTransactionAsync(cancellationToken); + try + { + await action(); + + await CommitTransactionAsync(cancellationToken); + } + catch + { + await RollbackTransaction(cancellationToken); + throw; + } + } + + public async Task ExecuteTransactionalAsync( + Func> action, + CancellationToken cancellationToken = default) + { + await BeginTransactionAsync(cancellationToken); + try + { + var result = await action(); + + await CommitTransactionAsync(cancellationToken); + + return result; + } + catch + { + await RollbackTransaction(cancellationToken); + throw; + } + } +} diff --git a/src/BuildingBlocks/Mongo/MongoOptions.cs b/src/BuildingBlocks/Mongo/MongoOptions.cs new file mode 100644 index 0000000..c127b79 --- /dev/null +++ b/src/BuildingBlocks/Mongo/MongoOptions.cs @@ -0,0 +1,8 @@ +namespace BuildingBlocks.Mongo; + +public class MongoOptions +{ + public string ConnectionString { get; set; } = null!; + public string DatabaseName { get; set; } = null!; + public static Guid UniqueId { get; set; } = Guid.NewGuid(); +} diff --git a/src/BuildingBlocks/Mongo/MongoRepository.cs b/src/BuildingBlocks/Mongo/MongoRepository.cs new file mode 100644 index 0000000..773d7db --- /dev/null +++ b/src/BuildingBlocks/Mongo/MongoRepository.cs @@ -0,0 +1,89 @@ +using System.Linq.Expressions; +using BuildingBlocks.Domain.Model; +using MongoDB.Driver; + +namespace BuildingBlocks.Mongo; + +public class MongoRepository : IMongoRepository + where TEntity : class, IEntity +{ + private readonly IMongoDbContext _context; + protected readonly IMongoCollection DbSet; + + public MongoRepository(IMongoDbContext context) + { + _context = context; + DbSet = _context.GetCollection(); + } + + public void Dispose() + { + _context?.Dispose(); + } + + public Task FindByIdAsync(TId id, CancellationToken cancellationToken = default) + { + return FindOneAsync(e => e.Id.Equals(id), cancellationToken); + } + + public Task FindOneAsync( + Expression> predicate, + CancellationToken cancellationToken = default) + { + return DbSet.Find(predicate).SingleOrDefaultAsync(cancellationToken: cancellationToken)!; + } + + public async Task> FindAsync( + Expression> predicate, + CancellationToken cancellationToken = default) + { + return await DbSet.Find(predicate).ToListAsync(cancellationToken: cancellationToken)!; + } + + public async Task> GetAllAsync(CancellationToken cancellationToken = default) + { + return await DbSet.AsQueryable().ToListAsync(cancellationToken); + } + + public Task> RawQuery( + string query, + CancellationToken cancellationToken = default, + params object[] queryParams) + { + throw new NotImplementedException(); + } + + public async Task AddAsync(TEntity entity, CancellationToken cancellationToken = default) + { + await DbSet.InsertOneAsync(entity, new InsertOneOptions(), cancellationToken); + + return entity; + } + + public async Task UpdateAsync(TEntity entity, CancellationToken cancellationToken = default) + { + await DbSet.ReplaceOneAsync(e => e.Id.Equals(entity.Id), entity, new ReplaceOptions(), cancellationToken); + + return entity; + } + + public Task DeleteRangeAsync(IReadOnlyList entities, CancellationToken cancellationToken = default) + { + return DbSet.DeleteOneAsync(e => entities.Any(i => e.Id.Equals(i.Id)), cancellationToken); + } + + public Task DeleteAsync( + Expression> predicate, + CancellationToken cancellationToken = default) + => DbSet.DeleteOneAsync(predicate, cancellationToken); + + public Task DeleteAsync(TEntity entity, CancellationToken cancellationToken = default) + { + return DbSet.DeleteOneAsync(e => e.Id.Equals(entity.Id), cancellationToken); + } + + public Task DeleteByIdAsync(TId id, CancellationToken cancellationToken = default) + { + return DbSet.DeleteOneAsync(e => e.Id.Equals(id), cancellationToken); + } +} diff --git a/src/BuildingBlocks/Mongo/MongoUnitOfWork.cs b/src/BuildingBlocks/Mongo/MongoUnitOfWork.cs new file mode 100644 index 0000000..7d6d94c --- /dev/null +++ b/src/BuildingBlocks/Mongo/MongoUnitOfWork.cs @@ -0,0 +1,32 @@ +namespace BuildingBlocks.Mongo; + +public class MongoUnitOfWork : IMongoUnitOfWork, ITransactionAble + where TContext : MongoDbContext +{ + public MongoUnitOfWork(TContext context) => Context = context; + + public TContext Context { get; } + + public async Task CommitAsync(CancellationToken cancellationToken = default) + { + await Context.SaveChangesAsync(cancellationToken); + } + + public Task BeginTransactionAsync(CancellationToken cancellationToken = default) + { + return Context.BeginTransactionAsync(cancellationToken); + } + + public Task RollbackTransactionAsync(CancellationToken cancellationToken = default) + { + return Context.RollbackTransaction(cancellationToken); + } + + public Task CommitTransactionAsync(CancellationToken cancellationToken = default) + { + return Context.CommitTransactionAsync(cancellationToken); + } + + public void Dispose() => Context.Dispose(); +} + diff --git a/src/BuildingBlocks/OpenTelemetry/Extensions.cs b/src/BuildingBlocks/OpenTelemetry/Extensions.cs new file mode 100644 index 0000000..098f06b --- /dev/null +++ b/src/BuildingBlocks/OpenTelemetry/Extensions.cs @@ -0,0 +1,22 @@ +using BuildingBlocks.Utils; +using BuildingBlocks.Web; +using Microsoft.Extensions.DependencyInjection; +using OpenTelemetry.Resources; +using OpenTelemetry.Trace; + +namespace BuildingBlocks.OpenTelemetry; + +public static class Extensions +{ + public static IServiceCollection AddCustomOpenTelemetry(this IServiceCollection services) + { + services.AddOpenTelemetryTracing(builder => builder + .AddMassTransitInstrumentation() + .AddAspNetCoreInstrumentation() + .AddHttpClientInstrumentation() + .SetResourceBuilder(ResourceBuilder.CreateDefault().AddService(services.GetOptions("AppOptions").Name)) + .AddJaegerExporter()); + + return services; + } +} diff --git a/src/BuildingBlocks/Swagger/ConfigureSwaggerOptions.cs b/src/BuildingBlocks/Swagger/ConfigureSwaggerOptions.cs new file mode 100644 index 0000000..8480f20 --- /dev/null +++ b/src/BuildingBlocks/Swagger/ConfigureSwaggerOptions.cs @@ -0,0 +1,48 @@ +using System; +using Microsoft.AspNetCore.Mvc.ApiExplorer; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; +using Microsoft.OpenApi.Models; +using Swashbuckle.AspNetCore.SwaggerGen; + +namespace BuildingBlocks.Swagger +{ + public class ConfigureSwaggerOptions : IConfigureOptions + { + private readonly IApiVersionDescriptionProvider _provider; + private readonly SwaggerOptions _options; + + public ConfigureSwaggerOptions(IApiVersionDescriptionProvider provider, IOptions options) + { + _provider = provider; + _options = options.Value; + } + + public void Configure(SwaggerGenOptions options) + { + foreach (var description in _provider.ApiVersionDescriptions) + { + options.SwaggerDoc(description.GroupName, CreateInfoForApiVersion(description)); + } + } + + private OpenApiInfo CreateInfoForApiVersion(ApiVersionDescription description) + { + var info = new OpenApiInfo + { + Title = _options.Title ?? "APIs", + Version =_options.Version ?? description.ApiVersion.ToString(), + Description = "An application with Swagger, Swashbuckle, and API versioning.", + Contact = new OpenApiContact { Name = "", Email = "" }, + License = new OpenApiLicense { Name = "MIT", Url = new Uri("https://opensource.org/licenses/MIT") } + }; + + if (description.IsDeprecated) + { + info.Description += " This API version has been deprecated."; + } + + return info; + } + } +} \ No newline at end of file diff --git a/src/BuildingBlocks/Swagger/ServiceCollectionExtensions.cs b/src/BuildingBlocks/Swagger/ServiceCollectionExtensions.cs new file mode 100644 index 0000000..1b47a3e --- /dev/null +++ b/src/BuildingBlocks/Swagger/ServiceCollectionExtensions.cs @@ -0,0 +1,132 @@ +using System.Reflection; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Mvc.ApiExplorer; +using Microsoft.AspNetCore.Mvc.Controllers; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; +using Microsoft.Extensions.PlatformAbstractions; +using Microsoft.OpenApi.Models; +using Swashbuckle.AspNetCore.SwaggerGen; + +namespace BuildingBlocks.Swagger; + +//https://github.com/domaindrivendev/Swashbuckle.AspNetCore/blob/master/README.md +public static class ServiceCollectionExtensions +{ + public const string HeaderName = "X-Api-Key"; + + public static IServiceCollection AddCustomSwagger(this IServiceCollection services, + IConfiguration configuration, + Assembly assembly, string swaggerSectionName = "SwaggerOptions") + { + services.AddVersionedApiExplorer(options => + { + // add the versioned api explorer, which also adds IApiVersionDescriptionProvider service + // note: the specified format code will format the version as "'v'major[.minor][-status]" + options.GroupNameFormat = "'v'VVV"; + + // note: this option is only necessary when versioning by url segment. the SubstitutionFormat + // can also be used to control the format of the API version in route templates + options.SubstituteApiVersionInUrl = true; + }); + + services.AddOptions().Bind(configuration.GetSection(swaggerSectionName)) + .ValidateDataAnnotations(); + + services.AddTransient, ConfigureSwaggerOptions>(); + + services.AddSwaggerGen( + options => + { + // options.DescribeAllParametersInCamelCase(); + options.OperationFilter(); + var xmlFile = XmlCommentsFilePath(assembly); + if (File.Exists(xmlFile)) options.IncludeXmlComments(xmlFile); + + options.AddSecurityDefinition("Bearer", new OpenApiSecurityScheme + { + Description = @"JWT Authorization header using the Bearer scheme. \r\n\r\n + Enter 'Bearer' [space] and then your token in the text input below. + \r\n\r\nExample: 'Bearer 12345abcdef'", + Name = "Authorization", + In = ParameterLocation.Header, + Type = SecuritySchemeType.ApiKey, + Scheme = "Bearer" + }); + + options.AddSecurityDefinition(HeaderName, + new OpenApiSecurityScheme + { + Description = "Api key needed to access the endpoints. X-Api-Key: My_API_Key", + In = ParameterLocation.Header, + Name = HeaderName, + Type = SecuritySchemeType.ApiKey + }); + + options.AddSecurityRequirement(new OpenApiSecurityRequirement + { + { + new OpenApiSecurityScheme + { + Reference = new OpenApiReference {Type = ReferenceType.SecurityScheme, Id = "Bearer"}, + Scheme = "oauth2", + Name = "Bearer", + In = ParameterLocation.Header + }, + new List() + }, + { + new OpenApiSecurityScheme + { + Name = HeaderName, + Type = SecuritySchemeType.ApiKey, + In = ParameterLocation.Header, + Reference = new OpenApiReference {Type = ReferenceType.SecurityScheme, Id = HeaderName} + }, + new string[] { } + } + }); + + //https://rimdev.io/swagger-grouping-with-controller-name-fallback-using-swashbuckle-aspnetcore/ + options.TagActionsBy(api => + { + if (api.GroupName != null) return new[] {api.GroupName}; + + if (api.ActionDescriptor is ControllerActionDescriptor controllerActionDescriptor) + return new[] {controllerActionDescriptor.ControllerName}; + + throw new InvalidOperationException("Unable to determine tag for endpoint."); + }); + + options.DocInclusionPredicate((name, api) => true); + + options.EnableAnnotations(); + }); + + + return services; + + static string XmlCommentsFilePath(Assembly assembly) + { + var basePath = PlatformServices.Default.Application.ApplicationBasePath; + var fileName = assembly.GetName().Name + ".xml"; + return Path.Combine(basePath, fileName); + } + } + + public static IApplicationBuilder UseCustomSwagger(this IApplicationBuilder app, + IApiVersionDescriptionProvider provider) + { + app.UseSwagger(); + app.UseSwaggerUI( + options => + { + foreach (var description in provider.ApiVersionDescriptions) + options.SwaggerEndpoint($"/swagger/{description.GroupName}/swagger.json", + description.GroupName.ToUpperInvariant()); + }); + + return app; + } +} diff --git a/src/BuildingBlocks/Swagger/SwaggerDefaultValues.cs b/src/BuildingBlocks/Swagger/SwaggerDefaultValues.cs new file mode 100644 index 0000000..2e12ba7 --- /dev/null +++ b/src/BuildingBlocks/Swagger/SwaggerDefaultValues.cs @@ -0,0 +1,65 @@ +using System.Linq; +using Humanizer; +using Microsoft.AspNetCore.Mvc.ApiExplorer; +using Microsoft.OpenApi.Models; +using Newtonsoft.Json; +using Swashbuckle.AspNetCore.SwaggerGen; + +namespace BuildingBlocks.Swagger +{ + public class SwaggerDefaultValues : IOperationFilter + { + public void Apply(OpenApiOperation operation, OperationFilterContext context) + { + + var apiDescription = context.ApiDescription; + + operation.Deprecated |= apiDescription.IsDeprecated(); + + // REF: https://github.com/domaindrivendev/Swashbuckle.AspNetCore/issues/1752#issue-663991077 + foreach (var responseType in context.ApiDescription.SupportedResponseTypes) + { + // REF: https://github.com/domaindrivendev/Swashbuckle.AspNetCore/blob/b7cf75e7905050305b115dd96640ddd6e74c7ac9/src/Swashbuckle.AspNetCore.SwaggerGen/SwaggerGenerator/SwaggerGenerator.cs#L383-L387 + var responseKey = responseType.IsDefaultResponse ? "default" : responseType.StatusCode.ToString(); + var response = operation.Responses[responseKey]; + + foreach (var contentType in response.Content.Keys) + { + if (responseType.ApiResponseFormats.All(x => x.MediaType != contentType)) + { + response.Content.Remove(contentType); + } + } + } + + if (operation.Parameters == null) + { + return; + } + + // REF: https://github.com/domaindrivendev/Swashbuckle.AspNetCore/issues/412 + // REF: https://github.com/domaindrivendev/Swashbuckle.AspNetCore/pull/413 + foreach (var parameter in operation.Parameters) + { + var description = apiDescription.ParameterDescriptions.First(p => p.Name == parameter.Name); + + if (parameter.Description == null) + { + parameter.Description = description.ModelMetadata?.Description; + } + + parameter.Name = description.Name.Camelize(); + + if (parameter.Schema.Default == null && description.DefaultValue != null) + { + // REF: https://github.com/Microsoft/aspnet-api-versioning/issues/429#issuecomment-605402330 + var json = JsonConvert.SerializeObject(description.DefaultValue, description.ModelMetadata + .ModelType, new JsonSerializerSettings {ReferenceLoopHandling = ReferenceLoopHandling.Ignore}); + parameter.Schema.Default = OpenApiAnyFactory.CreateFromJson(json); + } + + parameter.Required |= description.IsRequired; + } + } + } +} diff --git a/src/BuildingBlocks/Swagger/SwaggerOptions.cs b/src/BuildingBlocks/Swagger/SwaggerOptions.cs new file mode 100644 index 0000000..c385995 --- /dev/null +++ b/src/BuildingBlocks/Swagger/SwaggerOptions.cs @@ -0,0 +1,9 @@ +namespace BuildingBlocks.Swagger +{ + public class SwaggerOptions + { + public string Title { get; set; } + public string Name { get; set; } + public string Version { get; set; } + } +} \ No newline at end of file diff --git a/src/BuildingBlocks/Utils/ConfigurationExtensions.cs b/src/BuildingBlocks/Utils/ConfigurationExtensions.cs new file mode 100644 index 0000000..44f8686 --- /dev/null +++ b/src/BuildingBlocks/Utils/ConfigurationExtensions.cs @@ -0,0 +1,22 @@ +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; + +namespace BuildingBlocks.Utils; + +public static class ConfigurationExtensions +{ + public static TModel GetOptions(this IConfiguration configuration, string section) where TModel : new() + { + var model = new TModel(); + configuration.GetSection(section).Bind(model); + return model; + } + + public static TModel GetOptions(this IServiceCollection service, string section) where TModel : new() + { + var model = new TModel(); + var configuration = service.BuildServiceProvider().GetService(); + configuration?.GetSection(section).Bind(model); + return model; + } +} \ No newline at end of file diff --git a/src/BuildingBlocks/Utils/ConfigurationHelper.cs b/src/BuildingBlocks/Utils/ConfigurationHelper.cs new file mode 100644 index 0000000..908857c --- /dev/null +++ b/src/BuildingBlocks/Utils/ConfigurationHelper.cs @@ -0,0 +1,20 @@ +using Microsoft.Extensions.Configuration; + +namespace BuildingBlocks.Utils +{ + public static class ConfigurationHelper + { + public static IConfiguration GetConfiguration(string basePath = null) + { + basePath ??= Directory.GetCurrentDirectory(); + var environmentVariable = Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT"); + + return new ConfigurationBuilder() + .SetBasePath(basePath) + .AddJsonFile("appsettings.json", optional: false, reloadOnChange: true) + .AddJsonFile($"appsettings.{environmentVariable}.json", optional: true) + .AddEnvironmentVariables() + .Build(); + } + } +} \ No newline at end of file diff --git a/src/BuildingBlocks/Utils/EnumExtensions.cs b/src/BuildingBlocks/Utils/EnumExtensions.cs new file mode 100644 index 0000000..ec35658 --- /dev/null +++ b/src/BuildingBlocks/Utils/EnumExtensions.cs @@ -0,0 +1,28 @@ +using System; +using System.ComponentModel; + +namespace BuildingBlocks.Utils +{ + //https://stackoverflow.com/a/19621488/581476 + public static class EnumExtensions + { + // This extension method is broken out so you can use a similar pattern with + // other MetaData elements in the future. This is your base method for each. + public static T GetAttribute(this Enum value) where T : Attribute { + var type = value.GetType(); + var memberInfo = type.GetMember(value.ToString()); + var attributes = memberInfo[0].GetCustomAttributes(typeof(T), false); + return attributes.Length > 0 + ? (T)attributes[0] + : null; + } + + // This method creates a specific call to the above method, requesting the + // Description MetaData attribute. + public static string ToName(this Enum value) { + var attribute = value.GetAttribute(); + return attribute == null ? value.ToString() : attribute.Description; + } + + } +} \ No newline at end of file diff --git a/src/BuildingBlocks/Utils/NoSynchronizationContextScope.cs b/src/BuildingBlocks/Utils/NoSynchronizationContextScope.cs new file mode 100644 index 0000000..5742d0b --- /dev/null +++ b/src/BuildingBlocks/Utils/NoSynchronizationContextScope.cs @@ -0,0 +1,24 @@ +namespace BuildingBlocks.Utils; + +public static class NoSynchronizationContextScope +{ + public static Disposable Enter() + { + var context = SynchronizationContext.Current; + SynchronizationContext.SetSynchronizationContext(null); + return new Disposable(context); + } + + public struct Disposable: IDisposable + { + private readonly SynchronizationContext? synchronizationContext; + + public Disposable(SynchronizationContext? synchronizationContext) + { + this.synchronizationContext = synchronizationContext; + } + + public void Dispose() => + SynchronizationContext.SetSynchronizationContext(synchronizationContext); + } +} \ No newline at end of file diff --git a/src/BuildingBlocks/Utils/ObjectExtensions.cs b/src/BuildingBlocks/Utils/ObjectExtensions.cs new file mode 100644 index 0000000..4c26baf --- /dev/null +++ b/src/BuildingBlocks/Utils/ObjectExtensions.cs @@ -0,0 +1,18 @@ +using System; +using System.Linq; +using System.Web; + +namespace BuildingBlocks.Utils +{ + public static class ObjectExtensions + { + public static string GetQueryString(this object obj) + { + var properties = from p in obj.GetType().GetProperties() + where p.GetValue(obj, null) != null + select p.Name + "=" + HttpUtility.UrlEncode(p.GetValue(obj, null).ToString()); + + return String.Join("&", properties.ToArray()); + } + } +} \ No newline at end of file diff --git a/src/BuildingBlocks/Utils/ReflectionHelpers.cs b/src/BuildingBlocks/Utils/ReflectionHelpers.cs new file mode 100644 index 0000000..f4e8e16 --- /dev/null +++ b/src/BuildingBlocks/Utils/ReflectionHelpers.cs @@ -0,0 +1,60 @@ +using System; +using System.Collections.Concurrent; +using System.Linq; +using System.Reflection; + +namespace BuildingBlocks.Utils +{ + public static class ReflectionHelpers + { + private static readonly ConcurrentDictionary TypeCacheKeys = new(); + private static readonly ConcurrentDictionary PrettyPrintCache = new(); + + public static string GetCacheKey(this Type type) + { + return TypeCacheKeys.GetOrAdd(type, t => $"{t.PrettyPrint()}"); + } + public static string PrettyPrint(this Type type) + { + return PrettyPrintCache.GetOrAdd( + type, + t => + { + try + { + return PrettyPrintRecursive(t, 0); + } + catch (System.Exception) + { + return t.Name; + } + }); + } + + public static bool IsActionDelegate(this Type sourceType) + { + if (sourceType.IsSubclassOf(typeof(MulticastDelegate)) && + sourceType.GetMethod("Invoke").ReturnType == typeof(void)) + return true; + return false; + } + private static string PrettyPrintRecursive(Type type, int depth) + { + if (depth > 3) + { + return type.Name; + } + + var nameParts = type.Name.Split('`'); + if (nameParts.Length == 1) + { + return nameParts[0]; + } + + var genericArguments = type.GetTypeInfo().GetGenericArguments(); + return !type.IsConstructedGenericType + ? $"{nameParts[0]}<{new string(',', genericArguments.Length - 1)}>" + : $"{nameParts[0]}<{string.Join(",", genericArguments.Select(t => PrettyPrintRecursive(t, depth + 1)))}>"; + } + } +} \ No newline at end of file diff --git a/src/BuildingBlocks/Utils/TypeProvider.cs b/src/BuildingBlocks/Utils/TypeProvider.cs new file mode 100644 index 0000000..a31f57a --- /dev/null +++ b/src/BuildingBlocks/Utils/TypeProvider.cs @@ -0,0 +1,37 @@ +using System.Reflection; +using System.Runtime.CompilerServices; + +namespace BuildingBlocks.Utils; + +public static class TypeProvider +{ + private static bool IsRecord(this Type objectType) + { + return objectType.GetMethod("$") != null || + ((TypeInfo)objectType) + .DeclaredProperties.FirstOrDefault(x => x.Name == "EqualityContract")? + .GetMethod?.GetCustomAttribute(typeof(CompilerGeneratedAttribute)) != null; + } + + public static Type? GetTypeFromAnyReferencingAssembly(string typeName) + { + var referencedAssemblies = Assembly.GetEntryAssembly()? + .GetReferencedAssemblies() + .Select(a => a.FullName); + + if (referencedAssemblies == null) + return null; + + return AppDomain.CurrentDomain.GetAssemblies() + .Where(a => referencedAssemblies.Contains(a.FullName)) + .SelectMany(a => a.GetTypes().Where(x => x.FullName == typeName || x.Name == typeName)) + .FirstOrDefault(); + } + + public static Type? GetFirstMatchingTypeFromCurrentDomainAssembly(string typeName) + { + return AppDomain.CurrentDomain.GetAssemblies() + .SelectMany(a => a.GetTypes().Where(x => x.FullName == typeName || x.Name == typeName)) + .FirstOrDefault(); + } +} diff --git a/src/BuildingBlocks/Validation/Extensions.cs b/src/BuildingBlocks/Validation/Extensions.cs new file mode 100644 index 0000000..522742d --- /dev/null +++ b/src/BuildingBlocks/Validation/Extensions.cs @@ -0,0 +1,22 @@ +using BuildingBlocks.Exception; +using FluentValidation; +using FluentValidation.Results; +using ValidationException = BuildingBlocks.Exception.ValidationException; + +namespace BuildingBlocks.Validation +{ + public static class Extensions + { + /// + /// Ref https://www.jerriepelser.com/blog/validation-response-aspnet-core-webapi + /// + public static async Task HandleValidationAsync(this IValidator validator, TRequest request) + { + var validationResult = await validator.ValidateAsync(request); + if (!validationResult.IsValid) + { + throw new BadRequestException(validationResult.Errors.FirstOrDefault()?.ErrorMessage); + } + } + } +} diff --git a/src/BuildingBlocks/Validation/ValidationBehavior.cs b/src/BuildingBlocks/Validation/ValidationBehavior.cs new file mode 100644 index 0000000..f8a23bf --- /dev/null +++ b/src/BuildingBlocks/Validation/ValidationBehavior.cs @@ -0,0 +1,27 @@ +using FluentValidation; +using MediatR; +using Microsoft.Extensions.DependencyInjection; + +namespace BuildingBlocks.Validation; + +public sealed class ValidationBehavior : IPipelineBehavior + where TRequest : class, IRequest +{ + private IValidator _validator; + private readonly IServiceProvider _serviceProvider; + + public ValidationBehavior(IServiceProvider serviceProvider) + { + _serviceProvider = serviceProvider; + } + public async Task Handle(TRequest request, CancellationToken cancellationToken, RequestHandlerDelegate next) + { + _validator = _serviceProvider.GetService>(); + if (_validator is null) + return await next(); + + await _validator.HandleValidationAsync(request); + + return await next(); + } +} \ No newline at end of file diff --git a/src/BuildingBlocks/Validation/ValidationError.cs b/src/BuildingBlocks/Validation/ValidationError.cs new file mode 100644 index 0000000..1620ed4 --- /dev/null +++ b/src/BuildingBlocks/Validation/ValidationError.cs @@ -0,0 +1,15 @@ +namespace BuildingBlocks.Validation +{ + public class ValidationError + { + public string Field { get; } + + public string Message { get; } + + public ValidationError(string field, string message) + { + Field = field != string.Empty ? field : null; + Message = message; + } + } +} \ No newline at end of file diff --git a/src/BuildingBlocks/Validation/ValidationResultModel.cs b/src/BuildingBlocks/Validation/ValidationResultModel.cs new file mode 100644 index 0000000..03979fa --- /dev/null +++ b/src/BuildingBlocks/Validation/ValidationResultModel.cs @@ -0,0 +1,28 @@ +using System.Collections.Generic; +using System.Linq; +using System.Net; +using System.Text.Json; +using FluentValidation.Results; + +namespace BuildingBlocks.Validation +{ + public class ValidationResultModel + { + public int StatusCode { get; set; } = (int) HttpStatusCode.BadRequest; + public string Message { get; set; } = "Validation Failed."; + + public List Errors { get; } + + public ValidationResultModel(ValidationResult validationResult = null) + { + Errors = validationResult.Errors + .Select(error => new ValidationError(error.PropertyName, error.ErrorMessage)) + .ToList(); + } + + public override string ToString() + { + return JsonSerializer.Serialize(this); + } + } +} \ No newline at end of file diff --git a/src/BuildingBlocks/Web/AppOptions.cs b/src/BuildingBlocks/Web/AppOptions.cs new file mode 100644 index 0000000..b8a3bb7 --- /dev/null +++ b/src/BuildingBlocks/Web/AppOptions.cs @@ -0,0 +1,6 @@ +namespace BuildingBlocks.Web; + +public class AppOptions +{ + public string Name { get; set; } +} diff --git a/src/BuildingBlocks/Web/BaseController.cs b/src/BuildingBlocks/Web/BaseController.cs new file mode 100644 index 0000000..bbbaadc --- /dev/null +++ b/src/BuildingBlocks/Web/BaseController.cs @@ -0,0 +1,22 @@ +using AutoMapper; +using MediatR; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.DependencyInjection; + +namespace BuildingBlocks.Web; + +[Route(BaseApiPath)] +[ApiController] +[ApiVersion("1.0")] +public abstract class BaseController : ControllerBase +{ + protected const string BaseApiPath = "api/v{version:apiVersion}"; + private IMapper _mapper; + + private IMediator _mediator; + + protected IMediator Mediator => + _mediator ??= HttpContext.RequestServices.GetService(); + + protected IMapper Mapper => _mapper ??= HttpContext.RequestServices.GetService(); +} diff --git a/src/BuildingBlocks/Web/Extensions.cs b/src/BuildingBlocks/Web/Extensions.cs new file mode 100644 index 0000000..88fc73d --- /dev/null +++ b/src/BuildingBlocks/Web/Extensions.cs @@ -0,0 +1,62 @@ +using System; +using System.Linq; +using System.Net; +using System.Net.Http.Headers; +using JetBrains.Annotations; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Versioning; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; + +namespace BuildingBlocks.Web +{ + public static class Extensions + { + private const string CorrelationId = "correlationId"; + + public static void AddCustomVersioning(this IServiceCollection services, + Action configurator = null) + { + //https://www.meziantou.net/versioning-an-asp-net-core-api.htm + //https://exceptionnotfound.net/overview-of-api-versioning-in-asp-net-core-3-0/ + services.AddApiVersioning(options => + { + // Add the headers "api-supported-versions" and "api-deprecated-versions" + // This is better for discoverability + options.ReportApiVersions = true; + + // AssumeDefaultVersionWhenUnspecified should only be enabled when supporting legacy services that did not previously + // support API versioning. Forcing existing clients to specify an explicit API version for an + // existing service introduces a breaking change. Conceptually, clients in this situation are + // bound to some API version of a service, but they don't know what it is and never explicit request it. + options.AssumeDefaultVersionWhenUnspecified = true; + options.DefaultApiVersion = new ApiVersion(1, 0); + + // // Defines how an API version is read from the current HTTP request + options.ApiVersionReader = ApiVersionReader.Combine(new HeaderApiVersionReader("api-version"), + new UrlSegmentApiVersionReader()); + + configurator?.Invoke(options); + }); + } + + public static IApplicationBuilder UseCorrelationId(this IApplicationBuilder app) + => app.Use(async (ctx, next) => + { + if (!ctx.Request.Headers.TryGetValue(CorrelationId, out var correlationId)) + { + correlationId = Guid.NewGuid().ToString("N"); + } + + ctx.Items[CorrelationId] = correlationId.ToString(); + await next(); + }); + + public static string GetCorrelationId(this HttpContext context) + { + return context.Items.TryGetValue(CorrelationId, out var correlationId) ? correlationId as string : null; + } + } +} diff --git a/src/BuildingBlocks/Web/SlugifyParameterTransformer.cs b/src/BuildingBlocks/Web/SlugifyParameterTransformer.cs new file mode 100644 index 0000000..9aecf77 --- /dev/null +++ b/src/BuildingBlocks/Web/SlugifyParameterTransformer.cs @@ -0,0 +1,15 @@ +using System.Text.RegularExpressions; +using Microsoft.AspNetCore.Routing; + +namespace BuildingBlocks.Web; + +public class SlugifyParameterTransformer : IOutboundParameterTransformer +{ + public string TransformOutbound(object value) + { + // Slugify value + return value == null + ? null + : Regex.Replace(value.ToString() ?? string.Empty, "([a-z])([A-Z])", "$1-$2").ToLower(); + } +} diff --git a/src/Services/Booking/.dockerignore b/src/Services/Booking/.dockerignore new file mode 100644 index 0000000..820e869 --- /dev/null +++ b/src/Services/Booking/.dockerignore @@ -0,0 +1,25 @@ +**/.classpath +**/.dockerignore +**/.env +**/.git +**/.gitignore +**/.project +**/.settings +**/.toolstarget +**/.vs +**/.vscode +**/*.*proj.user +**/*.dbmdl +**/*.jfm +**/azds.yaml +**/bin/ +**/charts +**/docker-compose* +**/Dockerfile* +**/node_modules +**/npm-debug.log +**/obj/ +**/secrets.dev.yaml +**/values.dev.yaml +LICENSE +README.md \ No newline at end of file diff --git a/src/Services/Booking/Dockerfile b/src/Services/Booking/Dockerfile new file mode 100644 index 0000000..85f22ea --- /dev/null +++ b/src/Services/Booking/Dockerfile @@ -0,0 +1,41 @@ +FROM mcr.microsoft.com/dotnet/sdk:6.0 AS builder +WORKDIR /app + +# Setup working directory for the project +WORKDIR /app +COPY ./src/BuildingBlocks/BuildingBlocks.csproj ./BuildingBlocks/ +COPY ./src/Services/Booking/src/Booking/Booking.csproj ./Services/Booking/src/Booking/ +COPY ./src/Services/Booking/src/Booking.Api/Booking.Api.csproj ./Services/Booking/src/Booking.Api/ + + +# Restore nuget packages +RUN dotnet restore ./Services/Booking/src/Booking.Api/Booking.Api.csproj + +# Copy project files +COPY ./src/BuildingBlocks ./BuildingBlocks/ +COPY ./src/Services/Booking/src/Booking/ ./Services/Booking/src/Booking/ +COPY ./src/Services/Booking/src/Booking.Api/ ./Services/Booking/src/Booking.Api/ + +# Build project with Release configuration +# and no restore, as we did it already + +RUN ls +RUN dotnet build -c Release --no-restore ./Services/Booking/src/Booking.Api/Booking.Api.csproj + +WORKDIR /app/Services/Booking/src/Booking.Api + +# 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 /app/Services/Booking/src/Booking.Api/out . + +ENV ASPNETCORE_URLS https://*:5010, http://*:6010 +ENV ASPNETCORE_ENVIRONMENT docker + +ENTRYPOINT ["dotnet", "Booking.Api.dll"] + diff --git a/src/Services/Booking/src/Booking.Api/Booking.Api.csproj b/src/Services/Booking/src/Booking.Api/Booking.Api.csproj new file mode 100644 index 0000000..fe73e9a --- /dev/null +++ b/src/Services/Booking/src/Booking.Api/Booking.Api.csproj @@ -0,0 +1,13 @@ + + + + net6.0 + enable + enable + + + + + + + diff --git a/src/Services/Booking/src/Booking.Api/Program.cs b/src/Services/Booking/src/Booking.Api/Program.cs new file mode 100644 index 0000000..340f300 --- /dev/null +++ b/src/Services/Booking/src/Booking.Api/Program.cs @@ -0,0 +1,84 @@ +using Booking; +using Booking.Configuration; +using Booking.Data; +using Booking.Extensions; +using BuildingBlocks.Domain; +using BuildingBlocks.EFCore; +using BuildingBlocks.EventStoreDB; +using BuildingBlocks.IdsGenerator; +using BuildingBlocks.Jwt; +using BuildingBlocks.Logging; +using BuildingBlocks.Mapster; +using BuildingBlocks.MassTransit; +using BuildingBlocks.OpenTelemetry; +using BuildingBlocks.Swagger; +using BuildingBlocks.Utils; +using BuildingBlocks.Web; +using Figgle; +using FluentValidation; +using Hellang.Middleware.ProblemDetails; +using Microsoft.AspNetCore.Mvc.ApiExplorer; +using Prometheus; +using Serilog; + +var builder = WebApplication.CreateBuilder(args); +var configuration = builder.Configuration; + +var appOptions = builder.Services.GetOptions("AppOptions"); +builder.Services.Configure(options => configuration.GetSection("Grpc").Bind(options)); + +Console.WriteLine(FiggleFonts.Standard.Render(appOptions.Name)); + +builder.Services.AddCustomDbContext(configuration, typeof(BookingRoot).Assembly); + +builder.AddCustomSerilog(); +builder.Services.AddJwt(); +builder.Services.AddControllers(); +builder.Services.AddHttpContextAccessor(); +builder.Services.AddCustomSwagger(builder.Configuration, typeof(BookingRoot).Assembly); +builder.Services.AddCustomVersioning(); +builder.Services.AddCustomMediatR(); +builder.Services.AddValidatorsFromAssembly(typeof(BookingRoot).Assembly); +builder.Services.AddCustomProblemDetails(); +builder.Services.AddCustomMapster(typeof(BookingRoot).Assembly); +builder.Services.AddHttpContextAccessor(); + +builder.Services.AddTransient(); +builder.Services.AddTransient(); + +builder.Services.AddCustomMassTransit(typeof(BookingRoot).Assembly); +builder.Services.AddCustomOpenTelemetry(); +builder.Services.AddTransient(); +SnowFlakIdGenerator.Configure(3); + +// EventStoreDB Configuration +builder.Services.AddEventStore(configuration, typeof(BookingRoot).Assembly) + .AddEventStoreDBSubscriptionToAll(); + +var app = builder.Build(); + +if (app.Environment.IsDevelopment()) +{ + var provider = app.Services.GetService(); + app.UseCustomSwagger(provider); +} + +app.UseSerilogRequestLogging(); +app.UseMigrations(); +app.UseCorrelationId(); +app.UseRouting(); +app.UseHttpMetrics(); +app.UseHttpsRedirection(); +app.UseAuthentication(); +app.UseAuthorization(); +app.UseProblemDetails(); + +app.UseEndpoints(endpoints => +{ + endpoints.MapControllers(); + endpoints.MapMetrics(); +}); + +app.MapGet("/", x => x.Response.WriteAsync(appOptions.Name)); + +app.Run(); diff --git a/src/Services/Booking/src/Booking.Api/Properties/launchSettings.json b/src/Services/Booking/src/Booking.Api/Properties/launchSettings.json new file mode 100644 index 0000000..0207d20 --- /dev/null +++ b/src/Services/Booking/src/Booking.Api/Properties/launchSettings.json @@ -0,0 +1,23 @@ +{ + "$schema": "https://json.schemastore.org/launchsettings.json", + "iisSettings": { + "windowsAuthentication": false, + "anonymousAuthentication": true, + "iisExpress": { + "applicationUrl": "http://localhost:53934", + "sslPort": 44392 + } + }, + "profiles": { + "Booking.Api": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "launchUrl": "swagger", + "applicationUrl": "https://localhost:5010;http://localhost:6010", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} diff --git a/src/Services/Booking/src/Booking.Api/appsettings.Development.json b/src/Services/Booking/src/Booking.Api/appsettings.Development.json new file mode 100644 index 0000000..0c208ae --- /dev/null +++ b/src/Services/Booking/src/Booking.Api/appsettings.Development.json @@ -0,0 +1,8 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + } +} diff --git a/src/Services/Booking/src/Booking.Api/appsettings.docker.json b/src/Services/Booking/src/Booking.Api/appsettings.docker.json new file mode 100644 index 0000000..c51b078 --- /dev/null +++ b/src/Services/Booking/src/Booking.Api/appsettings.docker.json @@ -0,0 +1,23 @@ +{ + "App": "Booking-Service", + "Logging": { + "LogLevel": { + "Default": "Debug", + "Microsoft.AspNetCore": "Warning" + } + }, + "ConnectionStrings": { + "BookingConnection": "Server=db;Database=BookingDB;User ID=sa;Password=@Aa123456" + }, + "RabbitMq": { + "HostName": "rabbitmq", + "ExchangeName": "booking", + "UserName": "guest", + "Password": "guest" + }, + "Grpc": { + "FlightAddress": "https://localhost:5003", + "PassengerAddress": "https://localhost:5003" + }, + "AllowedHosts": "*" +} diff --git a/src/Services/Booking/src/Booking.Api/appsettings.json b/src/Services/Booking/src/Booking.Api/appsettings.json new file mode 100644 index 0000000..72103ab --- /dev/null +++ b/src/Services/Booking/src/Booking.Api/appsettings.json @@ -0,0 +1,35 @@ +{ + "AppOptions": { + "Name": "Booking-Service" + }, + "Serilog": { + "MinimumLevel": { + "Default": "Information", + "Override": { + "Microsoft": "Warning", + "Microsoft.Hosting.Lifetime": "Information" + } + } + }, + "ConnectionStrings": { + "DefaultConnection": "Server=.\\sqlexpress;Database=BookingDB;Trusted_Connection=True;MultipleActiveResultSets=true" + }, + "Jwt": { + "Authority": "https://localhost:5005", + "Audience": "booking-api" +}, + "RabbitMq": { + "HostName": "localhost", + "ExchangeName": "booking", + "UserName": "guest", + "Password": "guest" + }, + "Grpc": { + "FlightAddress": "https://localhost:5003", + "PassengerAddress": "https://localhost:5012" + }, + "EventStore": { + "ConnectionString": "esdb://localhost:2113?tls=false" + }, + "AllowedHosts": "*" +} diff --git a/src/Services/Booking/src/Booking/Booking.csproj b/src/Services/Booking/src/Booking/Booking.csproj new file mode 100644 index 0000000..1cd1e12 --- /dev/null +++ b/src/Services/Booking/src/Booking/Booking.csproj @@ -0,0 +1,23 @@ + + + + net6.0 + enable + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + + + + diff --git a/src/Services/Booking/src/Booking/Booking/Dtos/CreateReservationResponseDto.cs b/src/Services/Booking/src/Booking/Booking/Dtos/CreateReservationResponseDto.cs new file mode 100644 index 0000000..676c180 --- /dev/null +++ b/src/Services/Booking/src/Booking/Booking/Dtos/CreateReservationResponseDto.cs @@ -0,0 +1,16 @@ +namespace Booking.Booking.Dtos; + +public record BookingResponseDto +{ + public long Id { get; init; } + public string Name { get; init; } + public string FlightNumber { get; init; } + public long AircraftId { get; init; } + + public decimal Price { get; init; } + public DateTime FlightDate { get; init; } + public string SeatNumber { get; init; } + public long DepartureAirportId { get; init; } + public long ArriveAirportId { get; init; } + public string Description { get; init; } +} diff --git a/src/Services/Booking/src/Booking/Booking/Events/Domain/BookingCreatedDomainEvent.cs b/src/Services/Booking/src/Booking/Booking/Events/Domain/BookingCreatedDomainEvent.cs new file mode 100644 index 0000000..12561bb --- /dev/null +++ b/src/Services/Booking/src/Booking/Booking/Events/Domain/BookingCreatedDomainEvent.cs @@ -0,0 +1,6 @@ +using Booking.Booking.Models.ValueObjects; +using BuildingBlocks.Domain.Event; + +namespace Booking.Booking.Events.Domain; + +public record BookingCreatedDomainEvent(long Id, PassengerInfo PassengerInfo, Trip Trip, bool IsDeleted) : IDomainEvent; diff --git a/src/Services/Booking/src/Booking/Booking/Exceptions/BookingAlreadyExistException.cs b/src/Services/Booking/src/Booking/Booking/Exceptions/BookingAlreadyExistException.cs new file mode 100644 index 0000000..a577a98 --- /dev/null +++ b/src/Services/Booking/src/Booking/Booking/Exceptions/BookingAlreadyExistException.cs @@ -0,0 +1,10 @@ +using BuildingBlocks.Exception; + +namespace Booking.Booking.Exceptions; + +public class BookingAlreadyExistException : ConflictException +{ + public BookingAlreadyExistException(string code = default) : base("Booking already exist!", code) + { + } +} diff --git a/src/Services/Booking/src/Booking/Booking/Exceptions/FlightNotFoundException.cs b/src/Services/Booking/src/Booking/Booking/Exceptions/FlightNotFoundException.cs new file mode 100644 index 0000000..0bc6d60 --- /dev/null +++ b/src/Services/Booking/src/Booking/Booking/Exceptions/FlightNotFoundException.cs @@ -0,0 +1,10 @@ +using BuildingBlocks.Exception; + +namespace Booking.Booking.Exceptions; + +public class FlightNotFoundException : NotFoundException +{ + public FlightNotFoundException() : base("Flight doesn't exist!") + { + } +} diff --git a/src/Services/Booking/src/Booking/Booking/Exceptions/PassengerNotFoundException.cs b/src/Services/Booking/src/Booking/Booking/Exceptions/PassengerNotFoundException.cs new file mode 100644 index 0000000..a20999d --- /dev/null +++ b/src/Services/Booking/src/Booking/Booking/Exceptions/PassengerNotFoundException.cs @@ -0,0 +1,10 @@ +using BuildingBlocks.Exception; + +namespace Booking.Booking.Exceptions; + +public class PassengerNotFoundException: NotFoundException +{ + public PassengerNotFoundException() : base("Flight doesn't exist! ") + { + } +} \ No newline at end of file diff --git a/src/Services/Booking/src/Booking/Booking/Features/BookingMappings.cs b/src/Services/Booking/src/Booking/Booking/Features/BookingMappings.cs new file mode 100644 index 0000000..b47bce0 --- /dev/null +++ b/src/Services/Booking/src/Booking/Booking/Features/BookingMappings.cs @@ -0,0 +1,24 @@ +using Booking.Booking.Dtos; +using Mapster; + +namespace Booking.Booking.Features; + +public class BookingMappings : IRegister +{ + public void Register(TypeAdapterConfig config) + { + config.Default.NameMatchingStrategy(NameMatchingStrategy.Flexible); + + config.NewConfig() + .Map(d => d.Name, s => s.PassengerInfo.Name) + .Map(d => d.Description, s => s.Trip.Description) + .Map(d => d.DepartureAirportId, s => s.Trip.DepartureAirportId) + .Map(d => d.ArriveAirportId, s => s.Trip.ArriveAirportId) + .Map(d => d.FlightNumber, s => s.Trip.FlightNumber) + .Map(d => d.FlightDate, s => s.Trip.FlightDate) + .Map(d => d.Price, s => s.Trip.Price) + .Map(d => d.SeatNumber, s => s.Trip.SeatNumber) + .Map(d => d.AircraftId, s => s.Trip.AircraftId); + } +} + diff --git a/src/Services/Booking/src/Booking/Booking/Features/CreateBooking/CreateBookingCommand.cs b/src/Services/Booking/src/Booking/Booking/Features/CreateBooking/CreateBookingCommand.cs new file mode 100644 index 0000000..06c39f4 --- /dev/null +++ b/src/Services/Booking/src/Booking/Booking/Features/CreateBooking/CreateBookingCommand.cs @@ -0,0 +1,11 @@ +using BuildingBlocks.IdsGenerator; +using MediatR; + +namespace Booking.Booking.Features.CreateBooking; + +public record CreateBookingCommand + (long PassengerId, long FlightId, string Description) : IRequest +{ + public long Id { get; set; } = SnowFlakIdGenerator.NewId(); +} + diff --git a/src/Services/Booking/src/Booking/Booking/Features/CreateBooking/CreateBookingCommandHandler.cs b/src/Services/Booking/src/Booking/Booking/Features/CreateBooking/CreateBookingCommandHandler.cs new file mode 100644 index 0000000..82b42d3 --- /dev/null +++ b/src/Services/Booking/src/Booking/Booking/Features/CreateBooking/CreateBookingCommandHandler.cs @@ -0,0 +1,67 @@ +using Ardalis.GuardClauses; +using Booking.Booking.Exceptions; +using Booking.Booking.Models.ValueObjects; +using Booking.Configuration; +using BuildingBlocks.Contracts.Grpc; +using BuildingBlocks.EventStoreDB.Repository; +using Grpc.Net.Client; +using MagicOnion.Client; +using MapsterMapper; +using MediatR; +using Microsoft.Extensions.Options; + +namespace Booking.Booking.Features.CreateBooking; + +public class CreateBookingCommandHandler : IRequestHandler +{ + private readonly IEventStoreDBRepository _eventStoreDbRepository; + private readonly IFlightGrpcService _flightGrpcService; + private readonly IPassengerGrpcService _passengerGrpcService; + + + public CreateBookingCommandHandler( + IOptions grpcOptions, + IEventStoreDBRepository eventStoreDbRepository) + { + _eventStoreDbRepository = eventStoreDbRepository; + + var channelFlight = GrpcChannel.ForAddress(grpcOptions.Value.FlightAddress); + _flightGrpcService = new Lazy(() => MagicOnionClient.Create(channelFlight)).Value; + + var channelPassenger = GrpcChannel.ForAddress(grpcOptions.Value.PassengerAddress); + _passengerGrpcService = new Lazy(() => MagicOnionClient.Create(channelPassenger)).Value; + } + + public async Task Handle(CreateBookingCommand command, + CancellationToken cancellationToken) + { + Guard.Against.Null(command, nameof(command)); + + var flight = await _flightGrpcService.GetById(command.FlightId); + if (flight is null) + throw new FlightNotFoundException(); + var passenger = await _passengerGrpcService.GetById(command.PassengerId); + + var emptySeat = (await _flightGrpcService.GetAvailableSeats(command.FlightId))?.First(); + + var reservation = await _eventStoreDbRepository.Find(command.Id, cancellationToken); + + if (reservation is not null && !reservation.IsDeleted) + throw new BookingAlreadyExistException(); + + var aggrigate = Models.Booking.Create(command.Id, new PassengerInfo(passenger.Name), new Trip( + flight.FlightNumber, flight.AircraftId, flight.DepartureAirportId, + flight.ArriveAirportId, flight.FlightDate, flight.Price, command.Description, emptySeat?.SeatNumber)); + + await _flightGrpcService.ReserveSeat(new ReserveSeatRequestDto + { + FlightId = flight.Id, SeatNumber = emptySeat?.SeatNumber + }); + + var result = await _eventStoreDbRepository.Add( + aggrigate, + cancellationToken); + + return result; + } +} diff --git a/src/Services/Booking/src/Booking/Booking/Features/CreateBooking/CreateBookingCommandValidator.cs b/src/Services/Booking/src/Booking/Booking/Features/CreateBooking/CreateBookingCommandValidator.cs new file mode 100644 index 0000000..bffa3ed --- /dev/null +++ b/src/Services/Booking/src/Booking/Booking/Features/CreateBooking/CreateBookingCommandValidator.cs @@ -0,0 +1,14 @@ +using FluentValidation; + +namespace Booking.Booking.Features.CreateBooking; + +public class CreateBookingCommandValidator : AbstractValidator +{ + public CreateBookingCommandValidator() + { + CascadeMode = CascadeMode.Stop; + + RuleFor(x => x.FlightId).NotNull().WithMessage("FlightId is required!"); + RuleFor(x => x.PassengerId).NotNull().WithMessage("PassengerId is required!"); + } +} diff --git a/src/Services/Booking/src/Booking/Booking/Features/CreateBooking/CreateBookingEndpoint.cs b/src/Services/Booking/src/Booking/Booking/Features/CreateBooking/CreateBookingEndpoint.cs new file mode 100644 index 0000000..77b6fcc --- /dev/null +++ b/src/Services/Booking/src/Booking/Booking/Features/CreateBooking/CreateBookingEndpoint.cs @@ -0,0 +1,24 @@ +using BuildingBlocks.Web; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Swashbuckle.AspNetCore.Annotations; + +namespace Booking.Booking.Features.CreateBooking; + +[Route(BaseApiPath + "/booking")] +public class CreateBookingEndpoint : BaseController +{ + [HttpPost] + [Authorize] + [ProducesResponseType(StatusCodes.Status201Created)] + [ProducesResponseType(StatusCodes.Status400BadRequest)] + [SwaggerOperation(Summary = "Create new Reservation", Description = "Create new Reservation")] + public async Task CreateReservation([FromBody] CreateBookingCommand command, + CancellationToken cancellationToken) + { + var result = await Mediator.Send(command, cancellationToken); + + return Ok(result); + } +} diff --git a/src/Services/Booking/src/Booking/Booking/Models/Booking.cs b/src/Services/Booking/src/Booking/Booking/Models/Booking.cs new file mode 100644 index 0000000..3f127dc --- /dev/null +++ b/src/Services/Booking/src/Booking/Booking/Models/Booking.cs @@ -0,0 +1,54 @@ +using Booking.Booking.Events.Domain; +using Booking.Booking.Models.ValueObjects; +using BuildingBlocks.Domain.Model; + +namespace Booking.Booking.Models; + +public class Booking : Aggregate +{ + public Booking() + { + } + + public Trip Trip { get; private set; } + public PassengerInfo PassengerInfo { get; private set; } + + public static Booking Create(long id, PassengerInfo passengerInfo, Trip trip, bool isDeleted = false) + { + var booking = new Booking() + { + Id = id, + Trip = trip, + PassengerInfo = passengerInfo, + IsDeleted = isDeleted + }; + + var @event = new BookingCreatedDomainEvent(booking.Id, booking.PassengerInfo, booking.Trip, booking.IsDeleted); + + booking.AddDomainEvent(@event); + booking.Apply(@event); + + return booking; + } + + public override void When(object @event) + { + switch (@event) + { + case BookingCreatedDomainEvent reservationCreated: + { + Apply(reservationCreated); + return; + } + } + } + + private void Apply(BookingCreatedDomainEvent @event) + { + Id = @event.Id; + Trip = @event.Trip; + PassengerInfo = @event.PassengerInfo; + IsDeleted = @event.IsDeleted; + Version++; + } +} diff --git a/src/Services/Booking/src/Booking/Booking/Models/ValueObjects/PassengerInfo.cs b/src/Services/Booking/src/Booking/Booking/Models/ValueObjects/PassengerInfo.cs new file mode 100644 index 0000000..5e9c527 --- /dev/null +++ b/src/Services/Booking/src/Booking/Booking/Models/ValueObjects/PassengerInfo.cs @@ -0,0 +1,3 @@ +namespace Booking.Booking.Models.ValueObjects; + +public record PassengerInfo(string Name); diff --git a/src/Services/Booking/src/Booking/Booking/Models/ValueObjects/Trip.cs b/src/Services/Booking/src/Booking/Booking/Models/ValueObjects/Trip.cs new file mode 100644 index 0000000..4a302e4 --- /dev/null +++ b/src/Services/Booking/src/Booking/Booking/Models/ValueObjects/Trip.cs @@ -0,0 +1,4 @@ +namespace Booking.Booking.Models.ValueObjects; + +public record Trip(string FlightNumber, long AircraftId, long DepartureAirportId, long ArriveAirportId, + DateTime FlightDate, decimal Price, string Description, string SeatNumber); diff --git a/src/Services/Booking/src/Booking/BookingProjection.cs b/src/Services/Booking/src/Booking/BookingProjection.cs new file mode 100644 index 0000000..82edb22 --- /dev/null +++ b/src/Services/Booking/src/Booking/BookingProjection.cs @@ -0,0 +1,44 @@ +using Booking.Booking.Events.Domain; +using Booking.Data; +using BuildingBlocks.EventStoreDB.Events; +using BuildingBlocks.EventStoreDB.Projections; +using MediatR; +using Microsoft.EntityFrameworkCore; + +namespace Booking; + +public class BookingProjection : IProjectionProcessor +{ + private readonly BookingDbContext _bookingDbContext; + + public BookingProjection(BookingDbContext bookingDbContext) + { + _bookingDbContext = bookingDbContext; + } + + public async Task ProcessEventAsync(StreamEvent streamEvent, CancellationToken cancellationToken = default) + where T : INotification + { + switch (streamEvent.Data) + { + case BookingCreatedDomainEvent reservationCreatedDomainEvent: + await Apply(reservationCreatedDomainEvent, cancellationToken); + break; + } + } + + private async Task Apply(BookingCreatedDomainEvent @event, CancellationToken cancellationToken = default) + { + var reservation = + await _bookingDbContext.Bookings.SingleOrDefaultAsync(x => x.Id == @event.Id && !x.IsDeleted, + cancellationToken); + + if (reservation == null) + { + var model = Booking.Models.Booking.Create(@event.Id, @event.PassengerInfo, @event.Trip, @event.IsDeleted); + + await _bookingDbContext.Set().AddAsync(model, cancellationToken); + await _bookingDbContext.SaveChangesAsync(cancellationToken); + } + } +} diff --git a/src/Services/Booking/src/Booking/BookingRoot.cs b/src/Services/Booking/src/Booking/BookingRoot.cs new file mode 100644 index 0000000..73eb27c --- /dev/null +++ b/src/Services/Booking/src/Booking/BookingRoot.cs @@ -0,0 +1,6 @@ +namespace Booking; + +public class BookingRoot +{ + +} diff --git a/src/Services/Booking/src/Booking/Configuration/GrpcOptions.cs b/src/Services/Booking/src/Booking/Configuration/GrpcOptions.cs new file mode 100644 index 0000000..4f4adf7 --- /dev/null +++ b/src/Services/Booking/src/Booking/Configuration/GrpcOptions.cs @@ -0,0 +1,7 @@ +namespace Booking.Configuration; + +public class GrpcOptions +{ + public string FlightAddress { get; set; } + public string PassengerAddress { get; set; } +} diff --git a/src/Services/Booking/src/Booking/Configuration/RefitOptions.cs b/src/Services/Booking/src/Booking/Configuration/RefitOptions.cs new file mode 100644 index 0000000..f512276 --- /dev/null +++ b/src/Services/Booking/src/Booking/Configuration/RefitOptions.cs @@ -0,0 +1,7 @@ +namespace Booking.Configuration; + +public class RefitOptions +{ + public string FlightAddress { get; set; } + public string PassengerAddress { get; set; } +} \ No newline at end of file diff --git a/src/Services/Booking/src/Booking/Data/BookingDbContext.cs b/src/Services/Booking/src/Booking/Data/BookingDbContext.cs new file mode 100644 index 0000000..7fa98fb --- /dev/null +++ b/src/Services/Booking/src/Booking/Data/BookingDbContext.cs @@ -0,0 +1,21 @@ +using System.Reflection; +using BuildingBlocks.EFCore; +using Microsoft.AspNetCore.Http; +using Microsoft.EntityFrameworkCore; + +namespace Booking.Data; + +public class BookingDbContext : AppDbContextBase +{ + public BookingDbContext(DbContextOptions options, IHttpContextAccessor httpContextAccessor) : base(options, httpContextAccessor) + { + } + + public DbSet Bookings => Set(); + + protected override void OnModelCreating(ModelBuilder builder) + { + builder.ApplyConfigurationsFromAssembly(Assembly.GetExecutingAssembly()); + base.OnModelCreating(builder); + } +} diff --git a/src/Services/Booking/src/Booking/Data/Configurations/BookingConfiguration.cs b/src/Services/Booking/src/Booking/Data/Configurations/BookingConfiguration.cs new file mode 100644 index 0000000..01679d6 --- /dev/null +++ b/src/Services/Booking/src/Booking/Data/Configurations/BookingConfiguration.cs @@ -0,0 +1,32 @@ +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; + +namespace Booking.Data.Configurations; + +public class BookingConfiguration : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder builder) + { + builder.ToTable("Booking", "dbo"); + + builder.HasKey(r => r.Id); + builder.Property(r => r.Id).ValueGeneratedNever(); + + builder.OwnsOne(c => c.Trip, x => + { + x.Property(c => c.Description); + x.Property(c => c.Price); + x.Property(c => c.AircraftId); + x.Property(c => c.FlightDate); + x.Property(c => c.FlightNumber); + x.Property(c => c.SeatNumber); + x.Property(c => c.ArriveAirportId); + x.Property(c => c.DepartureAirportId); + }); + + builder.OwnsOne(c => c.PassengerInfo, x => + { + x.Property(c => c.Name); + }); + } +} diff --git a/src/Services/Booking/src/Booking/Data/DesignTimeDbContextFactory.cs b/src/Services/Booking/src/Booking/Data/DesignTimeDbContextFactory.cs new file mode 100644 index 0000000..488c53d --- /dev/null +++ b/src/Services/Booking/src/Booking/Data/DesignTimeDbContextFactory.cs @@ -0,0 +1,16 @@ +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Design; + +namespace Booking.Data; + +public class DesignTimeDbContextFactory : IDesignTimeDbContextFactory +{ + public BookingDbContext CreateDbContext(string[] args) + { + var builder = new DbContextOptionsBuilder(); + + builder.UseSqlServer( + "Data Source=.\\sqlexpress;Initial Catalog=BookingDB;Persist Security Info=False;Integrated Security=SSPI"); + return new BookingDbContext(builder.Options, null); + } +} diff --git a/src/Services/Booking/src/Booking/Data/Migrations/20220507150115_initial.Designer.cs b/src/Services/Booking/src/Booking/Data/Migrations/20220507150115_initial.Designer.cs new file mode 100644 index 0000000..cd3f373 --- /dev/null +++ b/src/Services/Booking/src/Booking/Data/Migrations/20220507150115_initial.Designer.cs @@ -0,0 +1,117 @@ +// +using System; +using Booking.Data; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace Booking.Data.Migrations +{ + [DbContext(typeof(BookingDbContext))] + [Migration("20220507150115_initial")] + partial class initial + { + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "6.0.1") + .HasAnnotation("Relational:MaxIdentifierLength", 128); + + SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder, 1L, 1); + + modelBuilder.Entity("Booking.Booking.Models.Booking", b => + { + b.Property("Id") + .HasColumnType("bigint"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("bigint"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("LastModified") + .HasColumnType("datetime2"); + + b.Property("LastModifiedBy") + .HasColumnType("bigint"); + + b.Property("Version") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.ToTable("Booking", "dbo"); + }); + + modelBuilder.Entity("Booking.Booking.Models.Booking", b => + { + b.OwnsOne("Booking.Booking.Models.ValueObjects.PassengerInfo", "PassengerInfo", b1 => + { + b1.Property("BookingId") + .HasColumnType("bigint"); + + b1.Property("Name") + .HasColumnType("nvarchar(max)"); + + b1.HasKey("BookingId"); + + b1.ToTable("Booking", "dbo"); + + b1.WithOwner() + .HasForeignKey("BookingId"); + }); + + b.OwnsOne("Booking.Booking.Models.ValueObjects.Trip", "Trip", b1 => + { + b1.Property("BookingId") + .HasColumnType("bigint"); + + b1.Property("AircraftId") + .HasColumnType("bigint"); + + b1.Property("ArriveAirportId") + .HasColumnType("bigint"); + + b1.Property("DepartureAirportId") + .HasColumnType("bigint"); + + b1.Property("Description") + .HasColumnType("nvarchar(max)"); + + b1.Property("FlightDate") + .HasColumnType("datetime2"); + + b1.Property("FlightNumber") + .HasColumnType("nvarchar(max)"); + + b1.Property("Price") + .HasColumnType("decimal(18,2)"); + + b1.Property("SeatNumber") + .HasColumnType("nvarchar(max)"); + + b1.HasKey("BookingId"); + + b1.ToTable("Booking", "dbo"); + + b1.WithOwner() + .HasForeignKey("BookingId"); + }); + + b.Navigation("PassengerInfo"); + + b.Navigation("Trip"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/Services/Booking/src/Booking/Data/Migrations/20220507150115_initial.cs b/src/Services/Booking/src/Booking/Data/Migrations/20220507150115_initial.cs new file mode 100644 index 0000000..d93dc16 --- /dev/null +++ b/src/Services/Booking/src/Booking/Data/Migrations/20220507150115_initial.cs @@ -0,0 +1,50 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Booking.Data.Migrations +{ + public partial class initial : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.EnsureSchema( + name: "dbo"); + + migrationBuilder.CreateTable( + name: "Booking", + schema: "dbo", + columns: table => new + { + Id = table.Column(type: "bigint", nullable: false), + Trip_FlightNumber = table.Column(type: "nvarchar(max)", nullable: true), + Trip_AircraftId = table.Column(type: "bigint", nullable: true), + Trip_DepartureAirportId = table.Column(type: "bigint", nullable: true), + Trip_ArriveAirportId = table.Column(type: "bigint", nullable: true), + Trip_FlightDate = table.Column(type: "datetime2", nullable: true), + Trip_Price = table.Column(type: "decimal(18,2)", nullable: true), + Trip_Description = table.Column(type: "nvarchar(max)", nullable: true), + Trip_SeatNumber = table.Column(type: "nvarchar(max)", nullable: true), + PassengerInfo_Name = table.Column(type: "nvarchar(max)", nullable: true), + CreatedAt = table.Column(type: "datetime2", nullable: true), + CreatedBy = table.Column(type: "bigint", nullable: true), + LastModified = table.Column(type: "datetime2", nullable: true), + LastModifiedBy = table.Column(type: "bigint", nullable: true), + Version = table.Column(type: "bigint", nullable: false), + IsDeleted = table.Column(type: "bit", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Booking", x => x.Id); + }); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "Booking", + schema: "dbo"); + } + } +} diff --git a/src/Services/Booking/src/Booking/Data/Migrations/BookingDbContextModelSnapshot.cs b/src/Services/Booking/src/Booking/Data/Migrations/BookingDbContextModelSnapshot.cs new file mode 100644 index 0000000..e8fd0f9 --- /dev/null +++ b/src/Services/Booking/src/Booking/Data/Migrations/BookingDbContextModelSnapshot.cs @@ -0,0 +1,115 @@ +// +using System; +using Booking.Data; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace Booking.Data.Migrations +{ + [DbContext(typeof(BookingDbContext))] + partial class BookingDbContextModelSnapshot : ModelSnapshot + { + protected override void BuildModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "6.0.1") + .HasAnnotation("Relational:MaxIdentifierLength", 128); + + SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder, 1L, 1); + + modelBuilder.Entity("Booking.Booking.Models.Booking", b => + { + b.Property("Id") + .HasColumnType("bigint"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("bigint"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("LastModified") + .HasColumnType("datetime2"); + + b.Property("LastModifiedBy") + .HasColumnType("bigint"); + + b.Property("Version") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.ToTable("Booking", "dbo"); + }); + + modelBuilder.Entity("Booking.Booking.Models.Booking", b => + { + b.OwnsOne("Booking.Booking.Models.ValueObjects.PassengerInfo", "PassengerInfo", b1 => + { + b1.Property("BookingId") + .HasColumnType("bigint"); + + b1.Property("Name") + .HasColumnType("nvarchar(max)"); + + b1.HasKey("BookingId"); + + b1.ToTable("Booking", "dbo"); + + b1.WithOwner() + .HasForeignKey("BookingId"); + }); + + b.OwnsOne("Booking.Booking.Models.ValueObjects.Trip", "Trip", b1 => + { + b1.Property("BookingId") + .HasColumnType("bigint"); + + b1.Property("AircraftId") + .HasColumnType("bigint"); + + b1.Property("ArriveAirportId") + .HasColumnType("bigint"); + + b1.Property("DepartureAirportId") + .HasColumnType("bigint"); + + b1.Property("Description") + .HasColumnType("nvarchar(max)"); + + b1.Property("FlightDate") + .HasColumnType("datetime2"); + + b1.Property("FlightNumber") + .HasColumnType("nvarchar(max)"); + + b1.Property("Price") + .HasColumnType("decimal(18,2)"); + + b1.Property("SeatNumber") + .HasColumnType("nvarchar(max)"); + + b1.HasKey("BookingId"); + + b1.ToTable("Booking", "dbo"); + + b1.WithOwner() + .HasForeignKey("BookingId"); + }); + + b.Navigation("PassengerInfo"); + + b.Navigation("Trip"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/Services/Booking/src/Booking/Data/readme.md b/src/Services/Booking/src/Booking/Data/readme.md new file mode 100644 index 0000000..4ab73f7 --- /dev/null +++ b/src/Services/Booking/src/Booking/Data/readme.md @@ -0,0 +1,2 @@ +dotnet ef migrations add initial --context BookingDbContext -o "Data\Migrations" +dotnet ef database update --context BookingDbContext diff --git a/src/Services/Booking/src/Booking/EventMapper.cs b/src/Services/Booking/src/Booking/EventMapper.cs new file mode 100644 index 0000000..7aebd40 --- /dev/null +++ b/src/Services/Booking/src/Booking/EventMapper.cs @@ -0,0 +1,23 @@ +using Booking.Booking.Events.Domain; +using BuildingBlocks.Contracts.EventBus.Messages; +using BuildingBlocks.Domain; +using BuildingBlocks.Domain.Event; + +namespace Booking; + +public sealed class EventMapper : IEventMapper +{ + public IEnumerable MapAll(IEnumerable events) + { + return events.Select(Map); + } + + public IIntegrationEvent Map(IDomainEvent @event) + { + return @event switch + { + BookingCreatedDomainEvent e => new BookingCreated(e.Id), + _ => null + }; + } +} diff --git a/src/Services/Booking/src/Booking/Extensions/MediatRExtensions.cs b/src/Services/Booking/src/Booking/Extensions/MediatRExtensions.cs new file mode 100644 index 0000000..8586739 --- /dev/null +++ b/src/Services/Booking/src/Booking/Extensions/MediatRExtensions.cs @@ -0,0 +1,20 @@ +using BuildingBlocks.EFCore; +using BuildingBlocks.Logging; +using BuildingBlocks.Validation; +using MediatR; +using Microsoft.Extensions.DependencyInjection; + +namespace Booking.Extensions; + +public static class MediatRExtensions +{ + public static IServiceCollection AddCustomMediatR(this IServiceCollection services) + { + services.AddMediatR(typeof(BookingRoot).Assembly); + services.AddScoped(typeof(IPipelineBehavior<,>), typeof(ValidationBehavior<,>)); + services.AddScoped(typeof(IPipelineBehavior<,>), typeof(LoggingBehavior<,>)); + services.AddScoped(typeof(IPipelineBehavior<,>), typeof(EfTxBehavior<,>)); + + return services; + } +} diff --git a/src/Services/Booking/src/Booking/Extensions/MigrationsExtensions.cs b/src/Services/Booking/src/Booking/Extensions/MigrationsExtensions.cs new file mode 100644 index 0000000..7e30a27 --- /dev/null +++ b/src/Services/Booking/src/Booking/Extensions/MigrationsExtensions.cs @@ -0,0 +1,36 @@ +using Booking.Data; +using BuildingBlocks.EFCore; +using Microsoft.AspNetCore.Builder; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.DependencyInjection; + +namespace Booking.Extensions; + +public static class MigrationsExtensions +{ + public static IApplicationBuilder UseMigrations(this IApplicationBuilder app) + { + MigrateDatabase(app.ApplicationServices); + SeedData(app.ApplicationServices); + + return app; + } + + private static void MigrateDatabase(IServiceProvider serviceProvider) + { + using var scope = serviceProvider.CreateScope(); + + var context = scope.ServiceProvider.GetRequiredService(); + context.Database.Migrate(); + } + + private static void SeedData(IServiceProvider serviceProvider) + { + using var scope = serviceProvider.CreateScope(); + var seeders = scope.ServiceProvider.GetServices(); + foreach (var seeder in seeders) + { + seeder.SeedAllAsync().GetAwaiter().GetResult(); + } + } +} diff --git a/src/Services/Booking/src/Booking/Extensions/ProblemDetailsExtensions.cs b/src/Services/Booking/src/Booking/Extensions/ProblemDetailsExtensions.cs new file mode 100644 index 0000000..bb54a74 --- /dev/null +++ b/src/Services/Booking/src/Booking/Extensions/ProblemDetailsExtensions.cs @@ -0,0 +1,89 @@ +using BuildingBlocks.Exception; +using Grpc.Core; +using Hellang.Middleware.ProblemDetails; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Newtonsoft.Json; + +namespace Booking.Extensions; + +public static class ProblemDetailsExtensions +{ + public static IServiceCollection AddCustomProblemDetails(this IServiceCollection services) + { + services.AddProblemDetails(x => + { + // Control when an exception is included + x.IncludeExceptionDetails = (ctx, _) => + { + // Fetch services from HttpContext.RequestServices + var env = ctx.RequestServices.GetRequiredService(); + return env.IsDevelopment() || env.IsStaging(); + }; + x.Map(ex => new ProblemDetails + { + Title = "Application rule broken", + Status = StatusCodes.Status409Conflict, + Detail = ex.Message, + Type = "https://somedomain/application-rule-validation-error" + }); + + // Exception will produce and returns from our FluentValidation RequestValidationBehavior + x.Map(ex => new ProblemDetails + { + Title = "input validation rules broken", + Status = StatusCodes.Status400BadRequest, + Detail = JsonConvert.SerializeObject(ex.ValidationResultModel.Errors), + Type = "https://somedomain/input-validation-rules-error" + }); + x.Map(ex => new ProblemDetails + { + Title = "bad request exception", + Status = StatusCodes.Status400BadRequest, + Detail = ex.Message, + Type = "https://somedomain/bad-request-error" + }); + x.Map(ex => new ProblemDetails + { + Title = "not found exception", + Status = StatusCodes.Status404NotFound, + Detail = ex.Message, + Type = "https://somedomain/not-found-error" + }); + x.Map(ex => new ProblemDetails + { + Title = "api server exception", + Status = StatusCodes.Status400BadRequest, + Detail = ex.Message, + Type = "https://somedomain/api-server-error" + }); + x.Map(ex => new ProblemDetails + { + Title = "application exception", + Status = StatusCodes.Status500InternalServerError, + Detail = ex.Message, + Type = "https://somedomain/application-error" + }); + x.Map(ex => new ProblemDetails + { + Status = (int)ex.StatusCode, + Title = "identity exception", + Detail = ex.Message, + Type = "https://somedomain/identity-error" + }); + + x.Map(ex => new ProblemDetails + { + Status = StatusCodes.Status400BadRequest, + Title = "grpc exception", + Detail = ex.Status.Detail, + Type = "https://somedomain/grpc-error" + }); + + x.MapToStatusCode(StatusCodes.Status400BadRequest); + }); + return services; + } +} diff --git a/src/Services/Flight/.dockerignore b/src/Services/Flight/.dockerignore new file mode 100644 index 0000000..820e869 --- /dev/null +++ b/src/Services/Flight/.dockerignore @@ -0,0 +1,25 @@ +**/.classpath +**/.dockerignore +**/.env +**/.git +**/.gitignore +**/.project +**/.settings +**/.toolstarget +**/.vs +**/.vscode +**/*.*proj.user +**/*.dbmdl +**/*.jfm +**/azds.yaml +**/bin/ +**/charts +**/docker-compose* +**/Dockerfile* +**/node_modules +**/npm-debug.log +**/obj/ +**/secrets.dev.yaml +**/values.dev.yaml +LICENSE +README.md \ No newline at end of file diff --git a/src/Services/Flight/Dockerfile b/src/Services/Flight/Dockerfile new file mode 100644 index 0000000..d354f07 --- /dev/null +++ b/src/Services/Flight/Dockerfile @@ -0,0 +1,42 @@ +FROM mcr.microsoft.com/dotnet/sdk:6.0 AS builder +WORKDIR /app + +# Setup working directory for the project +WORKDIR /app +COPY ./src/BuildingBlocks/BuildingBlocks.csproj ./BuildingBlocks/ +COPY ./src/Services/Flight/src/Flight/Flight.csproj ./Services/Flight/src/Flight/ +COPY ./src/Services/Flight/src/Flight.Api/Flight.Api.csproj ./Services/Flight/src/Flight.Api/ + + +# Restore nuget packages +RUN dotnet restore ./Services/Flight/src/Flight.Api/Flight.Api.csproj + +# Copy project files +COPY ./src/BuildingBlocks ./BuildingBlocks/ +COPY ./src/Services/Flight/src/Flight/ ./Services/Flight/src/Flight/ +COPY ./src/Services/Flight/src/Flight.Api/ ./Services/Flight/src/Flight.Api/ + +# Build project with Release configuration +# and no restore, as we did it already + +RUN ls +RUN dotnet build -c Release --no-restore ./Services/Flight/src/Flight.Api/Flight.Api.csproj + +WORKDIR /app/Services/Flight/src/Flight.Api + +# 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 /app/Services/Flight/src/Flight.Api/out . + + +ENV ASPNETCORE_URLS https://*:5003,http://*:5004 +ENV ASPNETCORE_ENVIRONMENT docker + +ENTRYPOINT ["dotnet", "Flight.Api.dll"] + diff --git a/src/Services/Flight/src/Flight.Api/Flight.Api.csproj b/src/Services/Flight/src/Flight.Api/Flight.Api.csproj new file mode 100644 index 0000000..7db9fe4 --- /dev/null +++ b/src/Services/Flight/src/Flight.Api/Flight.Api.csproj @@ -0,0 +1,23 @@ + + + + net6.0 + enable + enable + + + + + + + + + + + + + Always + + + + diff --git a/src/Services/Flight/src/Flight.Api/Program.cs b/src/Services/Flight/src/Flight.Api/Program.cs new file mode 100644 index 0000000..25c5716 --- /dev/null +++ b/src/Services/Flight/src/Flight.Api/Program.cs @@ -0,0 +1,101 @@ +using System.Reflection; +using BuildingBlocks.Caching; +using BuildingBlocks.Domain; +using BuildingBlocks.EFCore; +using BuildingBlocks.Exception; +using BuildingBlocks.IdsGenerator; +using BuildingBlocks.Jwt; +using BuildingBlocks.Logging; +using BuildingBlocks.Mapster; +using BuildingBlocks.MassTransit; +using BuildingBlocks.Mongo; +using BuildingBlocks.OpenTelemetry; +using BuildingBlocks.Swagger; +using BuildingBlocks.Utils; +using BuildingBlocks.Web; +using Figgle; +using Flight; +using Flight.Data; +using Flight.Data.Seed; +using Flight.Extensions; +using FluentValidation; +using Hellang.Middleware.ProblemDetails; +using Microsoft.AspNetCore.Mvc.ApiExplorer; +using Prometheus; +using Serilog; + + +var builder = WebApplication.CreateBuilder(args); +var configuration = builder.Configuration; + +var appOptions = builder.Services.GetOptions("AppOptions"); +Console.WriteLine(FiggleFonts.Standard.Render(appOptions.Name)); + +builder.Services.AddCustomDbContext(configuration, typeof(FlightRoot).Assembly); +builder.Services.AddMongoDbContext(configuration); + +builder.Services.AddScoped(); +builder.AddCustomSerilog(); +builder.Services.AddJwt(); +builder.Services.AddControllers(); +builder.Services.AddCustomSwagger(builder.Configuration, typeof(FlightRoot).Assembly); +builder.Services.AddCustomVersioning(); +builder.Services.AddCustomMediatR(); +builder.Services.AddValidatorsFromAssembly(typeof(FlightRoot).Assembly); +builder.Services.AddCustomProblemDetails(); +builder.Services.AddCustomMapster(typeof(FlightRoot).Assembly); +builder.Services.AddHttpContextAccessor(); +builder.Services.AddTransient(); +builder.Services.AddCustomMassTransit(typeof(FlightRoot).Assembly); +builder.Services.AddCustomOpenTelemetry(); +builder.Services.AddRouting(options => options.LowercaseUrls = true); + +builder.Services.AddGrpc(options => +{ + options.Interceptors.Add(); +}); + +builder.Services.AddMagicOnion(); + +SnowFlakIdGenerator.Configure(1); + +builder.Services.AddCachingRequest(new List +{ + typeof(FlightRoot).Assembly +}); + +builder.Services.AddEasyCaching(options => { options.UseInMemory(configuration, "mem"); }); + +var app = builder.Build(); + +if (app.Environment.IsDevelopment()) +{ + var provider = app.Services.GetService(); + app.UseCustomSwagger(provider); +} + +app.UseSerilogRequestLogging(); +app.UseCorrelationId(); +app.UseRouting(); +app.UseHttpMetrics(); +app.UseMigrations(); +app.UseProblemDetails(); +app.UseHttpsRedirection(); +app.UseAuthentication(); +app.UseAuthorization(); + + + +app.UseEndpoints(endpoints => +{ + endpoints.MapControllers(); + endpoints.MapMetrics(); + endpoints.MapMagicOnionService(); +}); + +app.MapGet("/", x => x.Response.WriteAsync(appOptions.Name)); +app.Run(); + +public partial class Program +{ +} diff --git a/src/Services/Flight/src/Flight.Api/Properties/launchSettings.json b/src/Services/Flight/src/Flight.Api/Properties/launchSettings.json new file mode 100644 index 0000000..031b217 --- /dev/null +++ b/src/Services/Flight/src/Flight.Api/Properties/launchSettings.json @@ -0,0 +1,31 @@ +{ + "$schema": "https://json.schemastore.org/launchsettings.json", + "iisSettings": { + "windowsAuthentication": false, + "anonymousAuthentication": true, + "iisExpress": { + "applicationUrl": "http://localhost:59936", + "sslPort": 44319 + } + }, + "profiles": { + "Flight.Api": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "launchUrl": "swagger", + "applicationUrl": "https://localhost:5003;http://localhost:5004", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "IIS Express": { + "commandName": "IISExpress", + "launchBrowser": true, + "launchUrl": "swagger", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} diff --git a/src/Services/Flight/src/Flight.Api/appsettings.Development.json b/src/Services/Flight/src/Flight.Api/appsettings.Development.json new file mode 100644 index 0000000..a6e86ac --- /dev/null +++ b/src/Services/Flight/src/Flight.Api/appsettings.Development.json @@ -0,0 +1,8 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Debug", + "Microsoft.AspNetCore": "Warning" + } + } +} diff --git a/src/Services/Flight/src/Flight.Api/appsettings.docker.json b/src/Services/Flight/src/Flight.Api/appsettings.docker.json new file mode 100644 index 0000000..947da65 --- /dev/null +++ b/src/Services/Flight/src/Flight.Api/appsettings.docker.json @@ -0,0 +1,23 @@ +{ + "App": "Flight-Service", + "Logging": { + "LogLevel": { + "Default": "Debug", + "Microsoft.AspNetCore": "Warning" + } + }, + "ConnectionStrings": { + "FlightConnection": "Server=db;Database=FlightDB;User ID=sa;Password=@Aa123456" + }, + "Jwt": { + "Authority": "https://localhost:5005", + "Audience": "flight-api" + }, + "RabbitMq": { + "HostName": "rabbitmq", + "ExchangeName": "flight", + "UserName": "guest", + "Password": "guest" + }, + "AllowedHosts": "*" +} diff --git a/src/Services/Flight/src/Flight.Api/appsettings.json b/src/Services/Flight/src/Flight.Api/appsettings.json new file mode 100644 index 0000000..a228d34 --- /dev/null +++ b/src/Services/Flight/src/Flight.Api/appsettings.json @@ -0,0 +1,34 @@ +{ + "AppOptions": { + "Name": "Flight-Service" + }, + "Serilog": { + "MinimumLevel": { + "Default": "Information", + "Override": { + "Microsoft": "Warning", + "Microsoft.Hosting.Lifetime": "Information" + } + } + }, + "ConnectionStrings": { + "DefaultConnection": "Server=.\\sqlexpress;Database=FlightDB;Trusted_Connection=True;MultipleActiveResultSets=true" + }, + "Jwt": { + "Authority": "https://localhost:5005", + "Audience": "flight-api" + }, + "RabbitMq": { + "HostName": "localhost", + "ExchangeName": "flight", + "UserName": "guest", + "Password": "guest" + }, + "Kestrel": { + "EndpointDefaults": { + "Protocols": "Http2", + "Url": "https://localhost:5003" + } + }, + "AllowedHosts": "*" +} diff --git a/src/Services/Flight/src/Flight.Api/appsettings.test.json b/src/Services/Flight/src/Flight.Api/appsettings.test.json new file mode 100644 index 0000000..4332d17 --- /dev/null +++ b/src/Services/Flight/src/Flight.Api/appsettings.test.json @@ -0,0 +1,11 @@ +{ + "ConnectionStrings": { + "FlightConnection": "Server=db;Database=FlightDB;User ID=sa;Password=@Aa123456" + }, + "RabbitMq": { + "HostName": "rabbitmq", + "ExchangeName": "flight", + "UserName": "guest", + "Password": "guest" + } +} diff --git a/src/Services/Flight/src/Flight/Aircrafts/Dtos/AircraftResponseDto.cs b/src/Services/Flight/src/Flight/Aircrafts/Dtos/AircraftResponseDto.cs new file mode 100644 index 0000000..55cfdf0 --- /dev/null +++ b/src/Services/Flight/src/Flight/Aircrafts/Dtos/AircraftResponseDto.cs @@ -0,0 +1,8 @@ +namespace Flight.Aircrafts.Dtos; + +public record AircraftResponseDto +{ + public string Name { get; init; } + public string Model { get; init; } + public int ManufacturingYear { get; init; } +} diff --git a/src/Services/Flight/src/Flight/Aircrafts/Events/AircraftCreatedDomainEvent.cs b/src/Services/Flight/src/Flight/Aircrafts/Events/AircraftCreatedDomainEvent.cs new file mode 100644 index 0000000..4d23d8d --- /dev/null +++ b/src/Services/Flight/src/Flight/Aircrafts/Events/AircraftCreatedDomainEvent.cs @@ -0,0 +1,5 @@ +using BuildingBlocks.Domain.Event; + +namespace Flight.Aircrafts.Events; + +public record AircraftCreatedDomainEvent(long Id, string Name, string Model, int ManufacturingYear) : IDomainEvent; diff --git a/src/Services/Flight/src/Flight/Aircrafts/Exceptions/AircraftAlreadyExistException.cs b/src/Services/Flight/src/Flight/Aircrafts/Exceptions/AircraftAlreadyExistException.cs new file mode 100644 index 0000000..49cb76b --- /dev/null +++ b/src/Services/Flight/src/Flight/Aircrafts/Exceptions/AircraftAlreadyExistException.cs @@ -0,0 +1,11 @@ +using System.Net; +using BuildingBlocks.Exception; + +namespace Flight.Aircrafts.Exceptions; + +public class AircraftAlreadyExistException : AppException +{ + public AircraftAlreadyExistException() : base("Flight already exist!", HttpStatusCode.Conflict) + { + } +} diff --git a/src/Services/Flight/src/Flight/Aircrafts/Features/CreateAircraft/CreateAircraftCommand.cs b/src/Services/Flight/src/Flight/Aircrafts/Features/CreateAircraft/CreateAircraftCommand.cs new file mode 100644 index 0000000..260ab03 --- /dev/null +++ b/src/Services/Flight/src/Flight/Aircrafts/Features/CreateAircraft/CreateAircraftCommand.cs @@ -0,0 +1,10 @@ +using BuildingBlocks.IdsGenerator; +using Flight.Aircrafts.Dtos; +using MediatR; + +namespace Flight.Aircrafts.Features.CreateAircraft; + +public record CreateAircraftCommand(string Name, string Model, int ManufacturingYear) : IRequest +{ + public long Id { get; set; } = SnowFlakIdGenerator.NewId(); +} diff --git a/src/Services/Flight/src/Flight/Aircrafts/Features/CreateAircraft/CreateAircraftCommandHandler.cs b/src/Services/Flight/src/Flight/Aircrafts/Features/CreateAircraft/CreateAircraftCommandHandler.cs new file mode 100644 index 0000000..8409e12 --- /dev/null +++ b/src/Services/Flight/src/Flight/Aircrafts/Features/CreateAircraft/CreateAircraftCommandHandler.cs @@ -0,0 +1,42 @@ +using System.Threading; +using System.Threading.Tasks; +using Ardalis.GuardClauses; +using Flight.Aircrafts.Dtos; +using Flight.Aircrafts.Exceptions; +using Flight.Aircrafts.Models; +using Flight.Data; +using MapsterMapper; +using MediatR; +using Microsoft.EntityFrameworkCore; + +namespace Flight.Aircrafts.Features.CreateAircraft; + +public class CreateAircraftCommandHandler : IRequestHandler +{ + private readonly FlightDbContext _flightDbContext; + private readonly IMapper _mapper; + + public CreateAircraftCommandHandler(IMapper mapper, FlightDbContext flightDbContext) + { + _mapper = mapper; + _flightDbContext = flightDbContext; + } + + public async Task Handle(CreateAircraftCommand command, CancellationToken cancellationToken) + { + Guard.Against.Null(command, nameof(command)); + + var aircraft = await _flightDbContext.Aircraft.SingleOrDefaultAsync(x => x.Model == command.Model, cancellationToken); + + if (aircraft is not null) + throw new AircraftAlreadyExistException(); + + var aircraftEntity = Aircraft.Create(command.Id, command.Name, command.Model, command.ManufacturingYear); + + var newAircraft = await _flightDbContext.Aircraft.AddAsync(aircraftEntity, cancellationToken); + + await _flightDbContext.SaveChangesAsync(cancellationToken); + + return _mapper.Map(newAircraft.Entity); + } +} diff --git a/src/Services/Flight/src/Flight/Aircrafts/Features/CreateAircraft/CreateAircraftCommandValidator.cs b/src/Services/Flight/src/Flight/Aircrafts/Features/CreateAircraft/CreateAircraftCommandValidator.cs new file mode 100644 index 0000000..6614e3f --- /dev/null +++ b/src/Services/Flight/src/Flight/Aircrafts/Features/CreateAircraft/CreateAircraftCommandValidator.cs @@ -0,0 +1,15 @@ +using FluentValidation; + +namespace Flight.Aircrafts.Features.CreateAircraft; + +public class CreateAircraftCommandValidator : AbstractValidator +{ + public CreateAircraftCommandValidator() + { + CascadeMode = CascadeMode.Stop; + + RuleFor(x => x.Model).NotEmpty().WithMessage("Model is required"); + RuleFor(x => x.Name).NotEmpty().WithMessage("Name is required"); + RuleFor(x => x.ManufacturingYear).NotEmpty().WithMessage("ManufacturingYear is required"); + } +} diff --git a/src/Services/Flight/src/Flight/Aircrafts/Features/CreateAircraft/CreateAircraftEndpoint.cs b/src/Services/Flight/src/Flight/Aircrafts/Features/CreateAircraft/CreateAircraftEndpoint.cs new file mode 100644 index 0000000..1d504cd --- /dev/null +++ b/src/Services/Flight/src/Flight/Aircrafts/Features/CreateAircraft/CreateAircraftEndpoint.cs @@ -0,0 +1,23 @@ +using System.Threading; +using System.Threading.Tasks; +using BuildingBlocks.Web; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Swashbuckle.AspNetCore.Annotations; + +namespace Flight.Aircrafts.Features.CreateAircraft; + +[Route(BaseApiPath + "/flight/aircraft")] +public class CreateAircraftEndpoint : BaseController +{ + [HttpPost] + [ProducesResponseType(StatusCodes.Status201Created)] + [ProducesResponseType(StatusCodes.Status400BadRequest)] + [SwaggerOperation(Summary = "Create new aircraft", Description = "Create new aircraft")] + public async Task Create([FromBody] CreateAircraftCommand command, CancellationToken cancellationToken) + { + var result = await Mediator.Send(command, cancellationToken); + + return Ok(result); + } +} diff --git a/src/Services/Flight/src/Flight/Aircrafts/Models/Aircraft.cs b/src/Services/Flight/src/Flight/Aircrafts/Models/Aircraft.cs new file mode 100644 index 0000000..9f7d7ba --- /dev/null +++ b/src/Services/Flight/src/Flight/Aircrafts/Models/Aircraft.cs @@ -0,0 +1,37 @@ +using BuildingBlocks.Domain.Model; +using BuildingBlocks.IdsGenerator; +using Flight.Aircrafts.Events; + +namespace Flight.Aircrafts.Models; + +public class Aircraft : Aggregate +{ + public Aircraft() + { + } + + public string Name { get; private set; } + public string Model { get; private set; } + public int ManufacturingYear { get; private set; } + + public static Aircraft Create(long id, string name, string model, int manufacturingYear) + { + var aircraft = new Aircraft + { + Id = id, + Name = name, + Model = model, + ManufacturingYear = manufacturingYear + }; + + var @event = new AircraftCreatedDomainEvent( + aircraft.Id, + aircraft.Name, + aircraft.Model, + aircraft.ManufacturingYear); + + aircraft.AddDomainEvent(@event); + + return aircraft; + } +} diff --git a/src/Services/Flight/src/Flight/Airports/Dtos/AirportResponseDto.cs b/src/Services/Flight/src/Flight/Airports/Dtos/AirportResponseDto.cs new file mode 100644 index 0000000..0d86a7a --- /dev/null +++ b/src/Services/Flight/src/Flight/Airports/Dtos/AirportResponseDto.cs @@ -0,0 +1,7 @@ +namespace Flight.Airports.Dtos; +public record AirportResponseDto +{ + public string Name { get; init; } + public string Address { get; init; } + public string Code { get; init; } +} diff --git a/src/Services/Flight/src/Flight/Airports/Events/AirportCreatedDomainEvent.cs b/src/Services/Flight/src/Flight/Airports/Events/AirportCreatedDomainEvent.cs new file mode 100644 index 0000000..2fcdad9 --- /dev/null +++ b/src/Services/Flight/src/Flight/Airports/Events/AirportCreatedDomainEvent.cs @@ -0,0 +1,5 @@ +using BuildingBlocks.Domain.Event; + +namespace Flight.Airports.Events; + +public record AirportCreatedDomainEvent(long Id, string Name, string Address, string Code) : IDomainEvent; diff --git a/src/Services/Flight/src/Flight/Airports/Exceptions/AirportAlreadyExistException.cs b/src/Services/Flight/src/Flight/Airports/Exceptions/AirportAlreadyExistException.cs new file mode 100644 index 0000000..cdd8874 --- /dev/null +++ b/src/Services/Flight/src/Flight/Airports/Exceptions/AirportAlreadyExistException.cs @@ -0,0 +1,10 @@ +using BuildingBlocks.Exception; + +namespace Flight.Airports.Exceptions; + +public class AirportAlreadyExistException : ConflictException +{ + public AirportAlreadyExistException(string code = default) : base("Airport already exist!", code) + { + } +} diff --git a/src/Services/Flight/src/Flight/Airports/Features/CreateAirport/CreateAirportCommand.cs b/src/Services/Flight/src/Flight/Airports/Features/CreateAirport/CreateAirportCommand.cs new file mode 100644 index 0000000..2f80cfa --- /dev/null +++ b/src/Services/Flight/src/Flight/Airports/Features/CreateAirport/CreateAirportCommand.cs @@ -0,0 +1,10 @@ +using BuildingBlocks.IdsGenerator; +using Flight.Airports.Dtos; +using MediatR; + +namespace Flight.Airports.Features.CreateAirport; + +public record CreateAirportCommand(string Name, string Address, string Code) : IRequest +{ + public long Id { get; set; } = SnowFlakIdGenerator.NewId(); +} diff --git a/src/Services/Flight/src/Flight/Airports/Features/CreateAirport/CreateAirportCommandHandler.cs b/src/Services/Flight/src/Flight/Airports/Features/CreateAirport/CreateAirportCommandHandler.cs new file mode 100644 index 0000000..503bbd3 --- /dev/null +++ b/src/Services/Flight/src/Flight/Airports/Features/CreateAirport/CreateAirportCommandHandler.cs @@ -0,0 +1,41 @@ +using System.Threading; +using System.Threading.Tasks; +using Ardalis.GuardClauses; +using Flight.Airports.Dtos; +using Flight.Airports.Exceptions; +using Flight.Data; +using MapsterMapper; +using MediatR; +using Microsoft.EntityFrameworkCore; + +namespace Flight.Airports.Features.CreateAirport; + +public class CreateAirportCommandHandler : IRequestHandler +{ + private readonly FlightDbContext _flightDbContext; + private readonly IMapper _mapper; + + public CreateAirportCommandHandler(IMapper mapper, FlightDbContext flightDbContext) + { + _mapper = mapper; + _flightDbContext = flightDbContext; + } + + public async Task Handle(CreateAirportCommand command, CancellationToken cancellationToken) + { + Guard.Against.Null(command, nameof(command)); + + var airport = await _flightDbContext.Airports.SingleOrDefaultAsync(x => x.Code == command.Code, cancellationToken); + + if (airport is not null) + throw new AirportAlreadyExistException(); + + var airportEntity = Models.Airport.Create(command.Id, command.Name, command.Code, command.Address); + + var newAirport = await _flightDbContext.Airports.AddAsync(airportEntity, cancellationToken); + + await _flightDbContext.SaveChangesAsync(cancellationToken); + + return _mapper.Map(newAirport.Entity); + } +} diff --git a/src/Services/Flight/src/Flight/Airports/Features/CreateAirport/CreateAirportCommandValidator.cs b/src/Services/Flight/src/Flight/Airports/Features/CreateAirport/CreateAirportCommandValidator.cs new file mode 100644 index 0000000..51b5eb6 --- /dev/null +++ b/src/Services/Flight/src/Flight/Airports/Features/CreateAirport/CreateAirportCommandValidator.cs @@ -0,0 +1,15 @@ +using FluentValidation; + +namespace Flight.Airports.Features.CreateAirport; + +public class CreateAirportCommandValidator : AbstractValidator +{ + public CreateAirportCommandValidator() + { + CascadeMode = CascadeMode.Stop; + + RuleFor(x => x.Code).NotEmpty().WithMessage("Code is required"); + RuleFor(x => x.Name).NotEmpty().WithMessage("Name is required"); + RuleFor(x => x.Address).NotEmpty().WithMessage("Address is required"); + } +} diff --git a/src/Services/Flight/src/Flight/Airports/Features/CreateAirport/CreateAirportEndpoint.cs b/src/Services/Flight/src/Flight/Airports/Features/CreateAirport/CreateAirportEndpoint.cs new file mode 100644 index 0000000..b84f441 --- /dev/null +++ b/src/Services/Flight/src/Flight/Airports/Features/CreateAirport/CreateAirportEndpoint.cs @@ -0,0 +1,23 @@ +using System.Threading; +using System.Threading.Tasks; +using BuildingBlocks.Web; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Swashbuckle.AspNetCore.Annotations; + +namespace Flight.Airports.Features.CreateAirport; + +[Route(BaseApiPath + "/flight/airport")] +public class CreateAirportEndpoint : BaseController +{ + [HttpPost] + [ProducesResponseType(StatusCodes.Status201Created)] + [ProducesResponseType(StatusCodes.Status400BadRequest)] + [SwaggerOperation(Summary = "Create new airport", Description = "Create new airport")] + public async Task Create([FromBody] CreateAirportCommand command, CancellationToken cancellationToken) + { + var result = await Mediator.Send(command, cancellationToken); + + return Ok(result); + } +} diff --git a/src/Services/Flight/src/Flight/Airports/Models/Airport.cs b/src/Services/Flight/src/Flight/Airports/Models/Airport.cs new file mode 100644 index 0000000..7837cff --- /dev/null +++ b/src/Services/Flight/src/Flight/Airports/Models/Airport.cs @@ -0,0 +1,37 @@ +using BuildingBlocks.Domain.Model; +using BuildingBlocks.IdsGenerator; +using Flight.Airports.Events; + +namespace Flight.Airports.Models; + +public class Airport : Aggregate +{ + public Airport() + { + } + + public string Name { get; private set; } + public string Address { get; private set; } + public string Code { get; private set; } + + public static Airport Create(long id, string name, string address, string code) + { + var airport = new Airport + { + Id = id, + Name = name, + Address = address, + Code = code + }; + + var @event = new AirportCreatedDomainEvent( + airport.Id, + airport.Name, + airport.Address, + airport.Code); + + airport.AddDomainEvent(@event); + + return airport; + } +} diff --git a/src/Services/Flight/src/Flight/Data/Configurations/AircraftConfiguration.cs b/src/Services/Flight/src/Flight/Data/Configurations/AircraftConfiguration.cs new file mode 100644 index 0000000..911fc8d --- /dev/null +++ b/src/Services/Flight/src/Flight/Data/Configurations/AircraftConfiguration.cs @@ -0,0 +1,15 @@ +using Flight.Aircrafts.Models; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; + +namespace Flight.Data.Configurations; + +public class AircraftConfiguration : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder builder) + { + builder.ToTable("Aircraft", "dbo"); + builder.HasKey(r => r.Id); + builder.Property(r => r.Id).ValueGeneratedNever(); + } +} diff --git a/src/Services/Flight/src/Flight/Data/Configurations/AirportConfiguration.cs b/src/Services/Flight/src/Flight/Data/Configurations/AirportConfiguration.cs new file mode 100644 index 0000000..0513096 --- /dev/null +++ b/src/Services/Flight/src/Flight/Data/Configurations/AirportConfiguration.cs @@ -0,0 +1,16 @@ +using Flight.Airports.Models; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; + +namespace Flight.Data.Configurations; + +public class AirportConfiguration: IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder builder) + { + builder.ToTable("Airport", "dbo"); + + builder.HasKey(r => r.Id); + builder.Property(r => r.Id).ValueGeneratedNever(); + } +} diff --git a/src/Services/Flight/src/Flight/Data/Configurations/FlightConfiguration.cs b/src/Services/Flight/src/Flight/Data/Configurations/FlightConfiguration.cs new file mode 100644 index 0000000..90e152a --- /dev/null +++ b/src/Services/Flight/src/Flight/Data/Configurations/FlightConfiguration.cs @@ -0,0 +1,39 @@ +using Flight.Aircrafts.Models; +using Flight.Airports.Models; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; + +namespace Flight.Data.Configurations; + +public class FlightConfiguration : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder builder) + { + builder.ToTable("Flight", "dbo"); + + builder.HasKey(r => r.Id); + builder.Property(r => r.Id).ValueGeneratedNever(); + + builder + .HasOne() + .WithMany() + .HasForeignKey(p => p.AircraftId); + + builder + .HasOne() + .WithMany() + .HasForeignKey(d => d.DepartureAirportId) + .HasForeignKey(a => a.ArriveAirportId); + + // // https://docs.microsoft.com/en-us/ef/core/modeling/shadow-properties + // // https://docs.microsoft.com/en-us/ef/core/modeling/owned-entities + // builder.OwnsMany(p => p.Seats, a => + // { + // a.WithOwner().HasForeignKey("FlightId"); + // a.Property("Id"); + // a.HasKey("Id"); + // a.Property("FlightId"); + // a.ToTable("Seat"); + // }); + } +} diff --git a/src/Services/Flight/src/Flight/Data/Configurations/SeatConfiguration.cs b/src/Services/Flight/src/Flight/Data/Configurations/SeatConfiguration.cs new file mode 100644 index 0000000..493ae90 --- /dev/null +++ b/src/Services/Flight/src/Flight/Data/Configurations/SeatConfiguration.cs @@ -0,0 +1,21 @@ +using Flight.Seats.Models; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; + +namespace Flight.Data.Configurations; + +public class SeatConfiguration : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder builder) + { + builder.ToTable("Seat", "dbo"); + + builder.HasKey(r => r.Id); + builder.Property(r => r.Id).ValueGeneratedNever(); + + builder + .HasOne() + .WithMany() + .HasForeignKey(p => p.FlightId); + } +} diff --git a/src/Services/Flight/src/Flight/Data/DesignTimeDbContextFactory.cs b/src/Services/Flight/src/Flight/Data/DesignTimeDbContextFactory.cs new file mode 100644 index 0000000..546ce72 --- /dev/null +++ b/src/Services/Flight/src/Flight/Data/DesignTimeDbContextFactory.cs @@ -0,0 +1,17 @@ +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Design; + +namespace Flight.Data +{ + public class DesignTimeDbContextFactory : IDesignTimeDbContextFactory + { + public FlightDbContext CreateDbContext(string[] args) + { + var builder = new DbContextOptionsBuilder(); + + builder.UseSqlServer( + "Data Source=.\\sqlexpress;Initial Catalog=FlightDB;Persist Security Info=False;Integrated Security=SSPI"); + return new FlightDbContext(builder.Options, null); + } + } +} diff --git a/src/Services/Flight/src/Flight/Data/FlightDbContext.cs b/src/Services/Flight/src/Flight/Data/FlightDbContext.cs new file mode 100644 index 0000000..dae7c09 --- /dev/null +++ b/src/Services/Flight/src/Flight/Data/FlightDbContext.cs @@ -0,0 +1,28 @@ +using System.Reflection; +using BuildingBlocks.EFCore; +using Flight.Aircrafts.Models; +using Flight.Airports.Models; +using Flight.Seats.Models; +using Microsoft.AspNetCore.Http; +using Microsoft.EntityFrameworkCore; + +namespace Flight.Data +{ + public sealed class FlightDbContext : AppDbContextBase + { + public FlightDbContext(DbContextOptions options, IHttpContextAccessor httpContextAccessor) : base(options, httpContextAccessor) + { + } + + public DbSet Flights => Set(); + public DbSet Airports => Set(); + public DbSet Aircraft => Set(); + public DbSet Seats => Set(); + + protected override void OnModelCreating(ModelBuilder builder) + { + builder.ApplyConfigurationsFromAssembly(Assembly.GetExecutingAssembly()); + base.OnModelCreating(builder); + } + } +} diff --git a/src/Services/Flight/src/Flight/Data/FlightReadDbContext.cs b/src/Services/Flight/src/Flight/Data/FlightReadDbContext.cs new file mode 100644 index 0000000..951394d --- /dev/null +++ b/src/Services/Flight/src/Flight/Data/FlightReadDbContext.cs @@ -0,0 +1,17 @@ +using BuildingBlocks.Mongo; +using Flight.Flights.Models.Reads; +using Humanizer; +using Microsoft.Extensions.Options; +using MongoDB.Driver; + +namespace Flight.Data; + +public class FlightReadDbContext : MongoDbContext +{ + public FlightReadDbContext(IOptions options) : base(options.Value) + { + Flight = GetCollection(nameof(Flight).Underscore()); + } + + public IMongoCollection Flight { get; } +} diff --git a/src/Services/Flight/src/Flight/Data/Migrations/20220303140107_Init.Designer.cs b/src/Services/Flight/src/Flight/Data/Migrations/20220303140107_Init.Designer.cs new file mode 100644 index 0000000..72cd263 --- /dev/null +++ b/src/Services/Flight/src/Flight/Data/Migrations/20220303140107_Init.Designer.cs @@ -0,0 +1,221 @@ +// +using System; +using Flight.Data; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace Flight.Data.Migrations +{ + [DbContext(typeof(FlightDbContext))] + [Migration("20220303140107_Init")] + partial class Init + { + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "6.0.1") + .HasAnnotation("Relational:MaxIdentifierLength", 128); + + SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder, 1L, 1); + + modelBuilder.Entity("BuildingBlocks.Outbox.OutboxMessage", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("CorrelationId") + .HasColumnType("nvarchar(max)"); + + b.Property("Data") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("EventType") + .IsRequired() + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("OccurredOn") + .HasColumnType("datetime2"); + + b.Property("ProcessedOn") + .HasColumnType("datetime2"); + + b.Property("Type") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("OutboxMessages", (string)null); + }); + + modelBuilder.Entity("Flight.Aircraft.Models.Aircraft", b => + { + b.Property("Id") + .HasColumnType("bigint"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("LastModified") + .HasColumnType("datetime2"); + + b.Property("ManufacturingYear") + .HasColumnType("int"); + + b.Property("Model") + .HasColumnType("nvarchar(max)"); + + b.Property("Name") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("Aircraft", "dbo"); + }); + + modelBuilder.Entity("Flight.Airport.Models.Airport", b => + { + b.Property("Id") + .HasColumnType("bigint"); + + b.Property("Address") + .HasColumnType("nvarchar(max)"); + + b.Property("Code") + .HasColumnType("nvarchar(max)"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("LastModified") + .HasColumnType("datetime2"); + + b.Property("Name") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("Airport", "dbo"); + }); + + modelBuilder.Entity("Flight.Flight.Models.Flight", b => + { + b.Property("Id") + .HasColumnType("bigint"); + + b.Property("AircraftId") + .HasColumnType("bigint"); + + b.Property("ArriveAirportId") + .HasColumnType("bigint"); + + b.Property("ArriveDate") + .HasColumnType("datetime2"); + + b.Property("DepartureAirportId") + .HasColumnType("bigint"); + + b.Property("DepartureDate") + .HasColumnType("datetime2"); + + b.Property("DurationMinutes") + .HasColumnType("decimal(18,2)"); + + b.Property("FlightDate") + .HasColumnType("datetime2"); + + b.Property("FlightNumber") + .HasColumnType("nvarchar(max)"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("LastModified") + .HasColumnType("datetime2"); + + b.Property("Price") + .HasColumnType("decimal(18,2)"); + + b.Property("Status") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("AircraftId"); + + b.HasIndex("ArriveAirportId"); + + b.ToTable("Flight", "dbo"); + }); + + modelBuilder.Entity("Flight.Flight.Models.Seat", b => + { + b.Property("Id") + .HasColumnType("bigint"); + + b.Property("Class") + .HasColumnType("int"); + + b.Property("FlightId") + .HasColumnType("bigint"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("LastModified") + .HasColumnType("datetime2"); + + b.Property("SeatNumber") + .HasColumnType("nvarchar(max)"); + + b.Property("Type") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("FlightId"); + + b.ToTable("Seat", "dbo"); + }); + + modelBuilder.Entity("Flight.Flight.Models.Flight", b => + { + b.HasOne("Flight.Aircraft.Models.Aircraft", null) + .WithMany() + .HasForeignKey("AircraftId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Flight.Airport.Models.Airport", null) + .WithMany() + .HasForeignKey("ArriveAirportId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Flight.Flight.Models.Seat", b => + { + b.HasOne("Flight.Flight.Models.Flight", null) + .WithMany() + .HasForeignKey("FlightId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/Services/Flight/src/Flight/Data/Migrations/20220303140107_Init.cs b/src/Services/Flight/src/Flight/Data/Migrations/20220303140107_Init.cs new file mode 100644 index 0000000..155eb66 --- /dev/null +++ b/src/Services/Flight/src/Flight/Data/Migrations/20220303140107_Init.cs @@ -0,0 +1,171 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Flight.Data.Migrations +{ + public partial class Init : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.EnsureSchema( + name: "dbo"); + + migrationBuilder.CreateTable( + name: "Aircraft", + schema: "dbo", + columns: table => new + { + Id = table.Column(type: "bigint", nullable: false), + Name = table.Column(type: "nvarchar(max)", nullable: true), + Model = table.Column(type: "nvarchar(max)", nullable: true), + ManufacturingYear = table.Column(type: "int", nullable: false), + LastModified = table.Column(type: "datetime2", nullable: false), + IsDeleted = table.Column(type: "bit", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Aircraft", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "Airport", + schema: "dbo", + columns: table => new + { + Id = table.Column(type: "bigint", nullable: false), + Name = table.Column(type: "nvarchar(max)", nullable: true), + Address = table.Column(type: "nvarchar(max)", nullable: true), + Code = table.Column(type: "nvarchar(max)", nullable: true), + LastModified = table.Column(type: "datetime2", nullable: false), + IsDeleted = table.Column(type: "bit", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Airport", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "OutboxMessages", + columns: table => new + { + Id = table.Column(type: "uniqueidentifier", nullable: false), + Name = table.Column(type: "nvarchar(max)", nullable: false), + OccurredOn = table.Column(type: "datetime2", nullable: false), + Type = table.Column(type: "nvarchar(max)", nullable: false), + Data = table.Column(type: "nvarchar(max)", nullable: false), + ProcessedOn = table.Column(type: "datetime2", nullable: true), + EventType = table.Column(type: "varchar(50)", unicode: false, maxLength: 50, nullable: false), + CorrelationId = table.Column(type: "nvarchar(max)", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_OutboxMessages", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "Flight", + schema: "dbo", + columns: table => new + { + Id = table.Column(type: "bigint", nullable: false), + FlightNumber = table.Column(type: "nvarchar(max)", nullable: true), + AircraftId = table.Column(type: "bigint", nullable: false), + DepartureDate = table.Column(type: "datetime2", nullable: false), + DepartureAirportId = table.Column(type: "bigint", nullable: false), + ArriveDate = table.Column(type: "datetime2", nullable: false), + ArriveAirportId = table.Column(type: "bigint", nullable: false), + DurationMinutes = table.Column(type: "decimal(18,2)", nullable: false), + FlightDate = table.Column(type: "datetime2", nullable: false), + Status = table.Column(type: "int", nullable: false), + Price = table.Column(type: "decimal(18,2)", nullable: false), + LastModified = table.Column(type: "datetime2", nullable: false), + IsDeleted = table.Column(type: "bit", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Flight", x => x.Id); + table.ForeignKey( + name: "FK_Flight_Aircraft_AircraftId", + column: x => x.AircraftId, + principalSchema: "dbo", + principalTable: "Aircraft", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_Flight_Airport_ArriveAirportId", + column: x => x.ArriveAirportId, + principalSchema: "dbo", + principalTable: "Airport", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "Seat", + schema: "dbo", + columns: table => new + { + Id = table.Column(type: "bigint", nullable: false), + SeatNumber = table.Column(type: "nvarchar(max)", nullable: true), + Type = table.Column(type: "int", nullable: false), + Class = table.Column(type: "int", nullable: false), + FlightId = table.Column(type: "bigint", nullable: false), + LastModified = table.Column(type: "datetime2", nullable: false), + IsDeleted = table.Column(type: "bit", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Seat", x => x.Id); + table.ForeignKey( + name: "FK_Seat_Flight_FlightId", + column: x => x.FlightId, + principalSchema: "dbo", + principalTable: "Flight", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateIndex( + name: "IX_Flight_AircraftId", + schema: "dbo", + table: "Flight", + column: "AircraftId"); + + migrationBuilder.CreateIndex( + name: "IX_Flight_ArriveAirportId", + schema: "dbo", + table: "Flight", + column: "ArriveAirportId"); + + migrationBuilder.CreateIndex( + name: "IX_Seat_FlightId", + schema: "dbo", + table: "Seat", + column: "FlightId"); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "OutboxMessages"); + + migrationBuilder.DropTable( + name: "Seat", + schema: "dbo"); + + migrationBuilder.DropTable( + name: "Flight", + schema: "dbo"); + + migrationBuilder.DropTable( + name: "Aircraft", + schema: "dbo"); + + migrationBuilder.DropTable( + name: "Airport", + schema: "dbo"); + } + } +} diff --git a/src/Services/Flight/src/Flight/Data/Migrations/20220303172333_ModifiedBy-to-entities.Designer.cs b/src/Services/Flight/src/Flight/Data/Migrations/20220303172333_ModifiedBy-to-entities.Designer.cs new file mode 100644 index 0000000..acdf976 --- /dev/null +++ b/src/Services/Flight/src/Flight/Data/Migrations/20220303172333_ModifiedBy-to-entities.Designer.cs @@ -0,0 +1,233 @@ +// +using System; +using Flight.Data; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace Flight.Data.Migrations +{ + [DbContext(typeof(FlightDbContext))] + [Migration("20220303172333_ModifiedBy-to-entities")] + partial class ModifiedBytoentities + { + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "6.0.1") + .HasAnnotation("Relational:MaxIdentifierLength", 128); + + SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder, 1L, 1); + + modelBuilder.Entity("BuildingBlocks.Outbox.OutboxMessage", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("CorrelationId") + .HasColumnType("nvarchar(max)"); + + b.Property("Data") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("EventType") + .IsRequired() + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("OccurredOn") + .HasColumnType("datetime2"); + + b.Property("ProcessedOn") + .HasColumnType("datetime2"); + + b.Property("Type") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("OutboxMessages", (string)null); + }); + + modelBuilder.Entity("Flight.Aircraft.Models.Aircraft", b => + { + b.Property("Id") + .HasColumnType("bigint"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("LastModified") + .HasColumnType("datetime2"); + + b.Property("ManufacturingYear") + .HasColumnType("int"); + + b.Property("Model") + .HasColumnType("nvarchar(max)"); + + b.Property("ModifiedBy") + .HasColumnType("int"); + + b.Property("Name") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("Aircraft", "dbo"); + }); + + modelBuilder.Entity("Flight.Airport.Models.Airport", b => + { + b.Property("Id") + .HasColumnType("bigint"); + + b.Property("Address") + .HasColumnType("nvarchar(max)"); + + b.Property("Code") + .HasColumnType("nvarchar(max)"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("LastModified") + .HasColumnType("datetime2"); + + b.Property("ModifiedBy") + .HasColumnType("int"); + + b.Property("Name") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("Airport", "dbo"); + }); + + modelBuilder.Entity("Flight.Flight.Models.Flight", b => + { + b.Property("Id") + .HasColumnType("bigint"); + + b.Property("AircraftId") + .HasColumnType("bigint"); + + b.Property("ArriveAirportId") + .HasColumnType("bigint"); + + b.Property("ArriveDate") + .HasColumnType("datetime2"); + + b.Property("DepartureAirportId") + .HasColumnType("bigint"); + + b.Property("DepartureDate") + .HasColumnType("datetime2"); + + b.Property("DurationMinutes") + .HasColumnType("decimal(18,2)"); + + b.Property("FlightDate") + .HasColumnType("datetime2"); + + b.Property("FlightNumber") + .HasColumnType("nvarchar(max)"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("LastModified") + .HasColumnType("datetime2"); + + b.Property("ModifiedBy") + .HasColumnType("int"); + + b.Property("Price") + .HasColumnType("decimal(18,2)"); + + b.Property("Status") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("AircraftId"); + + b.HasIndex("ArriveAirportId"); + + b.ToTable("Flight", "dbo"); + }); + + modelBuilder.Entity("Flight.Flight.Models.Seat", b => + { + b.Property("Id") + .HasColumnType("bigint"); + + b.Property("Class") + .HasColumnType("int"); + + b.Property("FlightId") + .HasColumnType("bigint"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("LastModified") + .HasColumnType("datetime2"); + + b.Property("ModifiedBy") + .HasColumnType("int"); + + b.Property("SeatNumber") + .HasColumnType("nvarchar(max)"); + + b.Property("Type") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("FlightId"); + + b.ToTable("Seat", "dbo"); + }); + + modelBuilder.Entity("Flight.Flight.Models.Flight", b => + { + b.HasOne("Flight.Aircraft.Models.Aircraft", null) + .WithMany() + .HasForeignKey("AircraftId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Flight.Airport.Models.Airport", null) + .WithMany() + .HasForeignKey("ArriveAirportId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Flight.Flight.Models.Seat", b => + { + b.HasOne("Flight.Flight.Models.Flight", null) + .WithMany() + .HasForeignKey("FlightId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/Services/Flight/src/Flight/Data/Migrations/20220303172333_ModifiedBy-to-entities.cs b/src/Services/Flight/src/Flight/Data/Migrations/20220303172333_ModifiedBy-to-entities.cs new file mode 100644 index 0000000..4ae976f --- /dev/null +++ b/src/Services/Flight/src/Flight/Data/Migrations/20220303172333_ModifiedBy-to-entities.cs @@ -0,0 +1,63 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Flight.Data.Migrations +{ + public partial class ModifiedBytoentities : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "ModifiedBy", + schema: "dbo", + table: "Seat", + type: "int", + nullable: true); + + migrationBuilder.AddColumn( + name: "ModifiedBy", + schema: "dbo", + table: "Flight", + type: "int", + nullable: true); + + migrationBuilder.AddColumn( + name: "ModifiedBy", + schema: "dbo", + table: "Airport", + type: "int", + nullable: true); + + migrationBuilder.AddColumn( + name: "ModifiedBy", + schema: "dbo", + table: "Aircraft", + type: "int", + nullable: true); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "ModifiedBy", + schema: "dbo", + table: "Seat"); + + migrationBuilder.DropColumn( + name: "ModifiedBy", + schema: "dbo", + table: "Flight"); + + migrationBuilder.DropColumn( + name: "ModifiedBy", + schema: "dbo", + table: "Airport"); + + migrationBuilder.DropColumn( + name: "ModifiedBy", + schema: "dbo", + table: "Aircraft"); + } + } +} diff --git a/src/Services/Flight/src/Flight/Data/Migrations/20220303182534_Change-corrolationId-type-outbox.Designer.cs b/src/Services/Flight/src/Flight/Data/Migrations/20220303182534_Change-corrolationId-type-outbox.Designer.cs new file mode 100644 index 0000000..ad18e82 --- /dev/null +++ b/src/Services/Flight/src/Flight/Data/Migrations/20220303182534_Change-corrolationId-type-outbox.Designer.cs @@ -0,0 +1,233 @@ +// +using System; +using Flight.Data; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace Flight.Data.Migrations +{ + [DbContext(typeof(FlightDbContext))] + [Migration("20220303182534_Change-corrolationId-type-outbox")] + partial class ChangecorrolationIdtypeoutbox + { + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "6.0.1") + .HasAnnotation("Relational:MaxIdentifierLength", 128); + + SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder, 1L, 1); + + modelBuilder.Entity("BuildingBlocks.Outbox.OutboxMessage", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("CorrelationId") + .HasColumnType("uniqueidentifier"); + + b.Property("Data") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("EventType") + .IsRequired() + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("OccurredOn") + .HasColumnType("datetime2"); + + b.Property("ProcessedOn") + .HasColumnType("datetime2"); + + b.Property("Type") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("OutboxMessages", (string)null); + }); + + modelBuilder.Entity("Flight.Aircraft.Models.Aircraft", b => + { + b.Property("Id") + .HasColumnType("bigint"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("LastModified") + .HasColumnType("datetime2"); + + b.Property("ManufacturingYear") + .HasColumnType("int"); + + b.Property("Model") + .HasColumnType("nvarchar(max)"); + + b.Property("ModifiedBy") + .HasColumnType("int"); + + b.Property("Name") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("Aircraft", "dbo"); + }); + + modelBuilder.Entity("Flight.Airport.Models.Airport", b => + { + b.Property("Id") + .HasColumnType("bigint"); + + b.Property("Address") + .HasColumnType("nvarchar(max)"); + + b.Property("Code") + .HasColumnType("nvarchar(max)"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("LastModified") + .HasColumnType("datetime2"); + + b.Property("ModifiedBy") + .HasColumnType("int"); + + b.Property("Name") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("Airport", "dbo"); + }); + + modelBuilder.Entity("Flight.Flight.Models.Flight", b => + { + b.Property("Id") + .HasColumnType("bigint"); + + b.Property("AircraftId") + .HasColumnType("bigint"); + + b.Property("ArriveAirportId") + .HasColumnType("bigint"); + + b.Property("ArriveDate") + .HasColumnType("datetime2"); + + b.Property("DepartureAirportId") + .HasColumnType("bigint"); + + b.Property("DepartureDate") + .HasColumnType("datetime2"); + + b.Property("DurationMinutes") + .HasColumnType("decimal(18,2)"); + + b.Property("FlightDate") + .HasColumnType("datetime2"); + + b.Property("FlightNumber") + .HasColumnType("nvarchar(max)"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("LastModified") + .HasColumnType("datetime2"); + + b.Property("ModifiedBy") + .HasColumnType("int"); + + b.Property("Price") + .HasColumnType("decimal(18,2)"); + + b.Property("Status") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("AircraftId"); + + b.HasIndex("ArriveAirportId"); + + b.ToTable("Flight", "dbo"); + }); + + modelBuilder.Entity("Flight.Flight.Models.Seat", b => + { + b.Property("Id") + .HasColumnType("bigint"); + + b.Property("Class") + .HasColumnType("int"); + + b.Property("FlightId") + .HasColumnType("bigint"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("LastModified") + .HasColumnType("datetime2"); + + b.Property("ModifiedBy") + .HasColumnType("int"); + + b.Property("SeatNumber") + .HasColumnType("nvarchar(max)"); + + b.Property("Type") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("FlightId"); + + b.ToTable("Seat", "dbo"); + }); + + modelBuilder.Entity("Flight.Flight.Models.Flight", b => + { + b.HasOne("Flight.Aircraft.Models.Aircraft", null) + .WithMany() + .HasForeignKey("AircraftId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Flight.Airport.Models.Airport", null) + .WithMany() + .HasForeignKey("ArriveAirportId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Flight.Flight.Models.Seat", b => + { + b.HasOne("Flight.Flight.Models.Flight", null) + .WithMany() + .HasForeignKey("FlightId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/Services/Flight/src/Flight/Data/Migrations/20220303182534_Change-corrolationId-type-outbox.cs b/src/Services/Flight/src/Flight/Data/Migrations/20220303182534_Change-corrolationId-type-outbox.cs new file mode 100644 index 0000000..11088dd --- /dev/null +++ b/src/Services/Flight/src/Flight/Data/Migrations/20220303182534_Change-corrolationId-type-outbox.cs @@ -0,0 +1,34 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Flight.Data.Migrations +{ + public partial class ChangecorrolationIdtypeoutbox : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AlterColumn( + name: "CorrelationId", + table: "OutboxMessages", + type: "uniqueidentifier", + nullable: true, + oldClrType: typeof(string), + oldType: "nvarchar(max)", + oldNullable: true); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.AlterColumn( + name: "CorrelationId", + table: "OutboxMessages", + type: "nvarchar(max)", + nullable: true, + oldClrType: typeof(Guid), + oldType: "uniqueidentifier", + oldNullable: true); + } + } +} diff --git a/src/Services/Flight/src/Flight/Data/Migrations/20220415203349_Add-Versening.Designer.cs b/src/Services/Flight/src/Flight/Data/Migrations/20220415203349_Add-Versening.Designer.cs new file mode 100644 index 0000000..7f788a8 --- /dev/null +++ b/src/Services/Flight/src/Flight/Data/Migrations/20220415203349_Add-Versening.Designer.cs @@ -0,0 +1,245 @@ +// +using System; +using Flight.Data; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace Flight.Data.Migrations +{ + [DbContext(typeof(FlightDbContext))] + [Migration("20220415203349_Add-Versening")] + partial class AddVersening + { + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "6.0.1") + .HasAnnotation("Relational:MaxIdentifierLength", 128); + + SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder, 1L, 1); + + modelBuilder.Entity("BuildingBlocks.Outbox.OutboxMessage", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("CorrelationId") + .HasColumnType("uniqueidentifier"); + + b.Property("Data") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("EventType") + .IsRequired() + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("OccurredOn") + .HasColumnType("datetime2"); + + b.Property("ProcessedOn") + .HasColumnType("datetime2"); + + b.Property("Type") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("OutboxMessages", (string)null); + }); + + modelBuilder.Entity("Flight.Aircraft.Models.Aircraft", b => + { + b.Property("Id") + .HasColumnType("bigint"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("LastModified") + .HasColumnType("datetime2"); + + b.Property("ManufacturingYear") + .HasColumnType("int"); + + b.Property("Model") + .HasColumnType("nvarchar(max)"); + + b.Property("ModifiedBy") + .HasColumnType("int"); + + b.Property("Name") + .HasColumnType("nvarchar(max)"); + + b.Property("Version") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.ToTable("Aircraft", "dbo"); + }); + + modelBuilder.Entity("Flight.Airport.Models.Airport", b => + { + b.Property("Id") + .HasColumnType("bigint"); + + b.Property("Address") + .HasColumnType("nvarchar(max)"); + + b.Property("Code") + .HasColumnType("nvarchar(max)"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("LastModified") + .HasColumnType("datetime2"); + + b.Property("ModifiedBy") + .HasColumnType("int"); + + b.Property("Name") + .HasColumnType("nvarchar(max)"); + + b.Property("Version") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.ToTable("Airport", "dbo"); + }); + + modelBuilder.Entity("Flight.Flight.Models.Flight", b => + { + b.Property("Id") + .HasColumnType("bigint"); + + b.Property("AircraftId") + .HasColumnType("bigint"); + + b.Property("ArriveAirportId") + .HasColumnType("bigint"); + + b.Property("ArriveDate") + .HasColumnType("datetime2"); + + b.Property("DepartureAirportId") + .HasColumnType("bigint"); + + b.Property("DepartureDate") + .HasColumnType("datetime2"); + + b.Property("DurationMinutes") + .HasColumnType("decimal(18,2)"); + + b.Property("FlightDate") + .HasColumnType("datetime2"); + + b.Property("FlightNumber") + .HasColumnType("nvarchar(max)"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("LastModified") + .HasColumnType("datetime2"); + + b.Property("ModifiedBy") + .HasColumnType("int"); + + b.Property("Price") + .HasColumnType("decimal(18,2)"); + + b.Property("Status") + .HasColumnType("int"); + + b.Property("Version") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("AircraftId"); + + b.HasIndex("ArriveAirportId"); + + b.ToTable("Flight", "dbo"); + }); + + modelBuilder.Entity("Flight.Flight.Models.Seat", b => + { + b.Property("Id") + .HasColumnType("bigint"); + + b.Property("Class") + .HasColumnType("int"); + + b.Property("FlightId") + .HasColumnType("bigint"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("LastModified") + .HasColumnType("datetime2"); + + b.Property("ModifiedBy") + .HasColumnType("int"); + + b.Property("SeatNumber") + .HasColumnType("nvarchar(max)"); + + b.Property("Type") + .HasColumnType("int"); + + b.Property("Version") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("FlightId"); + + b.ToTable("Seat", "dbo"); + }); + + modelBuilder.Entity("Flight.Flight.Models.Flight", b => + { + b.HasOne("Flight.Aircraft.Models.Aircraft", null) + .WithMany() + .HasForeignKey("AircraftId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Flight.Airport.Models.Airport", null) + .WithMany() + .HasForeignKey("ArriveAirportId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Flight.Flight.Models.Seat", b => + { + b.HasOne("Flight.Flight.Models.Flight", null) + .WithMany() + .HasForeignKey("FlightId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/Services/Flight/src/Flight/Data/Migrations/20220415203349_Add-Versening.cs b/src/Services/Flight/src/Flight/Data/Migrations/20220415203349_Add-Versening.cs new file mode 100644 index 0000000..357bea5 --- /dev/null +++ b/src/Services/Flight/src/Flight/Data/Migrations/20220415203349_Add-Versening.cs @@ -0,0 +1,67 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Flight.Data.Migrations +{ + public partial class AddVersening : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "Version", + schema: "dbo", + table: "Seat", + type: "bigint", + nullable: false, + defaultValue: 0L); + + migrationBuilder.AddColumn( + name: "Version", + schema: "dbo", + table: "Flight", + type: "bigint", + nullable: false, + defaultValue: 0L); + + migrationBuilder.AddColumn( + name: "Version", + schema: "dbo", + table: "Airport", + type: "bigint", + nullable: false, + defaultValue: 0L); + + migrationBuilder.AddColumn( + name: "Version", + schema: "dbo", + table: "Aircraft", + type: "bigint", + nullable: false, + defaultValue: 0L); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "Version", + schema: "dbo", + table: "Seat"); + + migrationBuilder.DropColumn( + name: "Version", + schema: "dbo", + table: "Flight"); + + migrationBuilder.DropColumn( + name: "Version", + schema: "dbo", + table: "Airport"); + + migrationBuilder.DropColumn( + name: "Version", + schema: "dbo", + table: "Aircraft"); + } + } +} diff --git a/src/Services/Flight/src/Flight/Data/Migrations/20220416172637_Add-Audit.Designer.cs b/src/Services/Flight/src/Flight/Data/Migrations/20220416172637_Add-Audit.Designer.cs new file mode 100644 index 0000000..a564940 --- /dev/null +++ b/src/Services/Flight/src/Flight/Data/Migrations/20220416172637_Add-Audit.Designer.cs @@ -0,0 +1,269 @@ +// +using System; +using Flight.Data; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace Flight.Data.Migrations +{ + [DbContext(typeof(FlightDbContext))] + [Migration("20220416172637_Add-Audit")] + partial class AddAudit + { + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "6.0.1") + .HasAnnotation("Relational:MaxIdentifierLength", 128); + + SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder, 1L, 1); + + modelBuilder.Entity("BuildingBlocks.Outbox.OutboxMessage", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("CorrelationId") + .HasColumnType("uniqueidentifier"); + + b.Property("Data") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("EventType") + .IsRequired() + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("OccurredOn") + .HasColumnType("datetime2"); + + b.Property("ProcessedOn") + .HasColumnType("datetime2"); + + b.Property("Type") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("OutboxMessages", (string)null); + }); + + modelBuilder.Entity("Flight.Aircrafts.Models.Aircraft", b => + { + b.Property("Id") + .HasColumnType("bigint"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("int"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("LastModified") + .HasColumnType("datetime2"); + + b.Property("LastModifiedBy") + .HasColumnType("int"); + + b.Property("ManufacturingYear") + .HasColumnType("int"); + + b.Property("Model") + .HasColumnType("nvarchar(max)"); + + b.Property("Name") + .HasColumnType("nvarchar(max)"); + + b.Property("Version") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.ToTable("Aircraft", "dbo"); + }); + + modelBuilder.Entity("Flight.Airports.Models.Airport", b => + { + b.Property("Id") + .HasColumnType("bigint"); + + b.Property("Address") + .HasColumnType("nvarchar(max)"); + + b.Property("Code") + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("int"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("LastModified") + .HasColumnType("datetime2"); + + b.Property("LastModifiedBy") + .HasColumnType("int"); + + b.Property("Name") + .HasColumnType("nvarchar(max)"); + + b.Property("Version") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.ToTable("Airport", "dbo"); + }); + + modelBuilder.Entity("Flight.Flights.Models.Flight", b => + { + b.Property("Id") + .HasColumnType("bigint"); + + b.Property("AircraftId") + .HasColumnType("bigint"); + + b.Property("ArriveAirportId") + .HasColumnType("bigint"); + + b.Property("ArriveDate") + .HasColumnType("datetime2"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("int"); + + b.Property("DepartureAirportId") + .HasColumnType("bigint"); + + b.Property("DepartureDate") + .HasColumnType("datetime2"); + + b.Property("DurationMinutes") + .HasColumnType("decimal(18,2)"); + + b.Property("FlightDate") + .HasColumnType("datetime2"); + + b.Property("FlightNumber") + .HasColumnType("nvarchar(max)"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("LastModified") + .HasColumnType("datetime2"); + + b.Property("LastModifiedBy") + .HasColumnType("int"); + + b.Property("Price") + .HasColumnType("decimal(18,2)"); + + b.Property("Status") + .HasColumnType("int"); + + b.Property("Version") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("AircraftId"); + + b.HasIndex("ArriveAirportId"); + + b.ToTable("Flight", "dbo"); + }); + + modelBuilder.Entity("Flight.Seats.Models.Seat", b => + { + b.Property("Id") + .HasColumnType("bigint"); + + b.Property("Class") + .HasColumnType("int"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("int"); + + b.Property("FlightId") + .HasColumnType("bigint"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("LastModified") + .HasColumnType("datetime2"); + + b.Property("LastModifiedBy") + .HasColumnType("int"); + + b.Property("SeatNumber") + .HasColumnType("nvarchar(max)"); + + b.Property("Type") + .HasColumnType("int"); + + b.Property("Version") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("FlightId"); + + b.ToTable("Seat", "dbo"); + }); + + modelBuilder.Entity("Flight.Flights.Models.Flight", b => + { + b.HasOne("Flight.Aircrafts.Models.Aircraft", null) + .WithMany() + .HasForeignKey("AircraftId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Flight.Airports.Models.Airport", null) + .WithMany() + .HasForeignKey("ArriveAirportId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Flight.Seats.Models.Seat", b => + { + b.HasOne("Flight.Flights.Models.Flight", null) + .WithMany() + .HasForeignKey("FlightId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/Services/Flight/src/Flight/Data/Migrations/20220416172637_Add-Audit.cs b/src/Services/Flight/src/Flight/Data/Migrations/20220416172637_Add-Audit.cs new file mode 100644 index 0000000..8afc8bd --- /dev/null +++ b/src/Services/Flight/src/Flight/Data/Migrations/20220416172637_Add-Audit.cs @@ -0,0 +1,240 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Flight.Data.Migrations +{ + public partial class AddAudit : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.RenameColumn( + name: "ModifiedBy", + schema: "dbo", + table: "Seat", + newName: "LastModifiedBy"); + + migrationBuilder.RenameColumn( + name: "ModifiedBy", + schema: "dbo", + table: "Flight", + newName: "LastModifiedBy"); + + migrationBuilder.RenameColumn( + name: "ModifiedBy", + schema: "dbo", + table: "Airport", + newName: "LastModifiedBy"); + + migrationBuilder.RenameColumn( + name: "ModifiedBy", + schema: "dbo", + table: "Aircraft", + newName: "LastModifiedBy"); + + migrationBuilder.AlterColumn( + name: "LastModified", + schema: "dbo", + table: "Seat", + type: "datetime2", + nullable: true, + oldClrType: typeof(DateTime), + oldType: "datetime2"); + + migrationBuilder.AddColumn( + name: "CreatedAt", + schema: "dbo", + table: "Seat", + type: "datetime2", + nullable: true); + + migrationBuilder.AddColumn( + name: "CreatedBy", + schema: "dbo", + table: "Seat", + type: "int", + nullable: true); + + migrationBuilder.AlterColumn( + name: "LastModified", + schema: "dbo", + table: "Flight", + type: "datetime2", + nullable: true, + oldClrType: typeof(DateTime), + oldType: "datetime2"); + + migrationBuilder.AddColumn( + name: "CreatedAt", + schema: "dbo", + table: "Flight", + type: "datetime2", + nullable: true); + + migrationBuilder.AddColumn( + name: "CreatedBy", + schema: "dbo", + table: "Flight", + type: "int", + nullable: true); + + migrationBuilder.AlterColumn( + name: "LastModified", + schema: "dbo", + table: "Airport", + type: "datetime2", + nullable: true, + oldClrType: typeof(DateTime), + oldType: "datetime2"); + + migrationBuilder.AddColumn( + name: "CreatedAt", + schema: "dbo", + table: "Airport", + type: "datetime2", + nullable: true); + + migrationBuilder.AddColumn( + name: "CreatedBy", + schema: "dbo", + table: "Airport", + type: "int", + nullable: true); + + migrationBuilder.AlterColumn( + name: "LastModified", + schema: "dbo", + table: "Aircraft", + type: "datetime2", + nullable: true, + oldClrType: typeof(DateTime), + oldType: "datetime2"); + + migrationBuilder.AddColumn( + name: "CreatedAt", + schema: "dbo", + table: "Aircraft", + type: "datetime2", + nullable: true); + + migrationBuilder.AddColumn( + name: "CreatedBy", + schema: "dbo", + table: "Aircraft", + type: "int", + nullable: true); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "CreatedAt", + schema: "dbo", + table: "Seat"); + + migrationBuilder.DropColumn( + name: "CreatedBy", + schema: "dbo", + table: "Seat"); + + migrationBuilder.DropColumn( + name: "CreatedAt", + schema: "dbo", + table: "Flight"); + + migrationBuilder.DropColumn( + name: "CreatedBy", + schema: "dbo", + table: "Flight"); + + migrationBuilder.DropColumn( + name: "CreatedAt", + schema: "dbo", + table: "Airport"); + + migrationBuilder.DropColumn( + name: "CreatedBy", + schema: "dbo", + table: "Airport"); + + migrationBuilder.DropColumn( + name: "CreatedAt", + schema: "dbo", + table: "Aircraft"); + + migrationBuilder.DropColumn( + name: "CreatedBy", + schema: "dbo", + table: "Aircraft"); + + migrationBuilder.RenameColumn( + name: "LastModifiedBy", + schema: "dbo", + table: "Seat", + newName: "ModifiedBy"); + + migrationBuilder.RenameColumn( + name: "LastModifiedBy", + schema: "dbo", + table: "Flight", + newName: "ModifiedBy"); + + migrationBuilder.RenameColumn( + name: "LastModifiedBy", + schema: "dbo", + table: "Airport", + newName: "ModifiedBy"); + + migrationBuilder.RenameColumn( + name: "LastModifiedBy", + schema: "dbo", + table: "Aircraft", + newName: "ModifiedBy"); + + migrationBuilder.AlterColumn( + name: "LastModified", + schema: "dbo", + table: "Seat", + type: "datetime2", + nullable: false, + defaultValue: new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), + oldClrType: typeof(DateTime), + oldType: "datetime2", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "LastModified", + schema: "dbo", + table: "Flight", + type: "datetime2", + nullable: false, + defaultValue: new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), + oldClrType: typeof(DateTime), + oldType: "datetime2", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "LastModified", + schema: "dbo", + table: "Airport", + type: "datetime2", + nullable: false, + defaultValue: new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), + oldClrType: typeof(DateTime), + oldType: "datetime2", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "LastModified", + schema: "dbo", + table: "Aircraft", + type: "datetime2", + nullable: false, + defaultValue: new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), + oldClrType: typeof(DateTime), + oldType: "datetime2", + oldNullable: true); + } + } +} diff --git a/src/Services/Flight/src/Flight/Data/Migrations/20220418195957_Update-Audit.Designer.cs b/src/Services/Flight/src/Flight/Data/Migrations/20220418195957_Update-Audit.Designer.cs new file mode 100644 index 0000000..149d824 --- /dev/null +++ b/src/Services/Flight/src/Flight/Data/Migrations/20220418195957_Update-Audit.Designer.cs @@ -0,0 +1,269 @@ +// +using System; +using Flight.Data; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace Flight.Data.Migrations +{ + [DbContext(typeof(FlightDbContext))] + [Migration("20220418195957_Update-Audit")] + partial class UpdateAudit + { + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "6.0.1") + .HasAnnotation("Relational:MaxIdentifierLength", 128); + + SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder, 1L, 1); + + modelBuilder.Entity("BuildingBlocks.Outbox.OutboxMessage", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("CorrelationId") + .HasColumnType("uniqueidentifier"); + + b.Property("Data") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("EventType") + .IsRequired() + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("OccurredOn") + .HasColumnType("datetime2"); + + b.Property("ProcessedOn") + .HasColumnType("datetime2"); + + b.Property("Type") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("OutboxMessages", (string)null); + }); + + modelBuilder.Entity("Flight.Aircrafts.Models.Aircraft", b => + { + b.Property("Id") + .HasColumnType("bigint"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("bigint"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("LastModified") + .HasColumnType("datetime2"); + + b.Property("LastModifiedBy") + .HasColumnType("bigint"); + + b.Property("ManufacturingYear") + .HasColumnType("int"); + + b.Property("Model") + .HasColumnType("nvarchar(max)"); + + b.Property("Name") + .HasColumnType("nvarchar(max)"); + + b.Property("Version") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.ToTable("Aircraft", "dbo"); + }); + + modelBuilder.Entity("Flight.Airports.Models.Airport", b => + { + b.Property("Id") + .HasColumnType("bigint"); + + b.Property("Address") + .HasColumnType("nvarchar(max)"); + + b.Property("Code") + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("bigint"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("LastModified") + .HasColumnType("datetime2"); + + b.Property("LastModifiedBy") + .HasColumnType("bigint"); + + b.Property("Name") + .HasColumnType("nvarchar(max)"); + + b.Property("Version") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.ToTable("Airport", "dbo"); + }); + + modelBuilder.Entity("Flight.Flights.Models.Flight", b => + { + b.Property("Id") + .HasColumnType("bigint"); + + b.Property("AircraftId") + .HasColumnType("bigint"); + + b.Property("ArriveAirportId") + .HasColumnType("bigint"); + + b.Property("ArriveDate") + .HasColumnType("datetime2"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("bigint"); + + b.Property("DepartureAirportId") + .HasColumnType("bigint"); + + b.Property("DepartureDate") + .HasColumnType("datetime2"); + + b.Property("DurationMinutes") + .HasColumnType("decimal(18,2)"); + + b.Property("FlightDate") + .HasColumnType("datetime2"); + + b.Property("FlightNumber") + .HasColumnType("nvarchar(max)"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("LastModified") + .HasColumnType("datetime2"); + + b.Property("LastModifiedBy") + .HasColumnType("bigint"); + + b.Property("Price") + .HasColumnType("decimal(18,2)"); + + b.Property("Status") + .HasColumnType("int"); + + b.Property("Version") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("AircraftId"); + + b.HasIndex("ArriveAirportId"); + + b.ToTable("Flight", "dbo"); + }); + + modelBuilder.Entity("Flight.Seats.Models.Seat", b => + { + b.Property("Id") + .HasColumnType("bigint"); + + b.Property("Class") + .HasColumnType("int"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("bigint"); + + b.Property("FlightId") + .HasColumnType("bigint"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("LastModified") + .HasColumnType("datetime2"); + + b.Property("LastModifiedBy") + .HasColumnType("bigint"); + + b.Property("SeatNumber") + .HasColumnType("nvarchar(max)"); + + b.Property("Type") + .HasColumnType("int"); + + b.Property("Version") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("FlightId"); + + b.ToTable("Seat", "dbo"); + }); + + modelBuilder.Entity("Flight.Flights.Models.Flight", b => + { + b.HasOne("Flight.Aircrafts.Models.Aircraft", null) + .WithMany() + .HasForeignKey("AircraftId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Flight.Airports.Models.Airport", null) + .WithMany() + .HasForeignKey("ArriveAirportId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Flight.Seats.Models.Seat", b => + { + b.HasOne("Flight.Flights.Models.Flight", null) + .WithMany() + .HasForeignKey("FlightId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/Services/Flight/src/Flight/Data/Migrations/20220418195957_Update-Audit.cs b/src/Services/Flight/src/Flight/Data/Migrations/20220418195957_Update-Audit.cs new file mode 100644 index 0000000..d1350e4 --- /dev/null +++ b/src/Services/Flight/src/Flight/Data/Migrations/20220418195957_Update-Audit.cs @@ -0,0 +1,175 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Flight.Data.Migrations +{ + public partial class UpdateAudit : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AlterColumn( + name: "LastModifiedBy", + schema: "dbo", + table: "Seat", + type: "bigint", + nullable: true, + oldClrType: typeof(int), + oldType: "int", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "CreatedBy", + schema: "dbo", + table: "Seat", + type: "bigint", + nullable: true, + oldClrType: typeof(int), + oldType: "int", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "LastModifiedBy", + schema: "dbo", + table: "Flight", + type: "bigint", + nullable: true, + oldClrType: typeof(int), + oldType: "int", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "CreatedBy", + schema: "dbo", + table: "Flight", + type: "bigint", + nullable: true, + oldClrType: typeof(int), + oldType: "int", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "LastModifiedBy", + schema: "dbo", + table: "Airport", + type: "bigint", + nullable: true, + oldClrType: typeof(int), + oldType: "int", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "CreatedBy", + schema: "dbo", + table: "Airport", + type: "bigint", + nullable: true, + oldClrType: typeof(int), + oldType: "int", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "LastModifiedBy", + schema: "dbo", + table: "Aircraft", + type: "bigint", + nullable: true, + oldClrType: typeof(int), + oldType: "int", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "CreatedBy", + schema: "dbo", + table: "Aircraft", + type: "bigint", + nullable: true, + oldClrType: typeof(int), + oldType: "int", + oldNullable: true); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.AlterColumn( + name: "LastModifiedBy", + schema: "dbo", + table: "Seat", + type: "int", + nullable: true, + oldClrType: typeof(long), + oldType: "bigint", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "CreatedBy", + schema: "dbo", + table: "Seat", + type: "int", + nullable: true, + oldClrType: typeof(long), + oldType: "bigint", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "LastModifiedBy", + schema: "dbo", + table: "Flight", + type: "int", + nullable: true, + oldClrType: typeof(long), + oldType: "bigint", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "CreatedBy", + schema: "dbo", + table: "Flight", + type: "int", + nullable: true, + oldClrType: typeof(long), + oldType: "bigint", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "LastModifiedBy", + schema: "dbo", + table: "Airport", + type: "int", + nullable: true, + oldClrType: typeof(long), + oldType: "bigint", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "CreatedBy", + schema: "dbo", + table: "Airport", + type: "int", + nullable: true, + oldClrType: typeof(long), + oldType: "bigint", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "LastModifiedBy", + schema: "dbo", + table: "Aircraft", + type: "int", + nullable: true, + oldClrType: typeof(long), + oldType: "bigint", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "CreatedBy", + schema: "dbo", + table: "Aircraft", + type: "int", + nullable: true, + oldClrType: typeof(long), + oldType: "bigint", + oldNullable: true); + } + } +} diff --git a/src/Services/Flight/src/Flight/Data/Migrations/20220421195137_Add-Internal-Messages.Designer.cs b/src/Services/Flight/src/Flight/Data/Migrations/20220421195137_Add-Internal-Messages.Designer.cs new file mode 100644 index 0000000..55664f0 --- /dev/null +++ b/src/Services/Flight/src/Flight/Data/Migrations/20220421195137_Add-Internal-Messages.Designer.cs @@ -0,0 +1,301 @@ +// +using System; +using Flight.Data; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace Flight.Data.Migrations +{ + [DbContext(typeof(FlightDbContext))] + [Migration("20220421195137_Add-Internal-Messages")] + partial class AddInternalMessages + { + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "6.0.1") + .HasAnnotation("Relational:MaxIdentifierLength", 128); + + SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder, 1L, 1); + + modelBuilder.Entity("BuildingBlocks.InternalProcessor.InternalMessage", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("CommandType") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("CorrelationId") + .HasColumnType("uniqueidentifier"); + + b.Property("Data") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("OccurredOn") + .HasColumnType("datetime2"); + + b.Property("ProcessedOn") + .HasColumnType("datetime2"); + + b.HasKey("Id"); + + b.ToTable("InternalMessages", (string)null); + }); + + modelBuilder.Entity("BuildingBlocks.Outbox.OutboxMessage", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("CorrelationId") + .HasColumnType("uniqueidentifier"); + + b.Property("Data") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("EventType") + .IsRequired() + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("OccurredOn") + .HasColumnType("datetime2"); + + b.Property("ProcessedOn") + .HasColumnType("datetime2"); + + b.Property("Type") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("OutboxMessages", (string)null); + }); + + modelBuilder.Entity("Flight.Aircrafts.Models.Aircraft", b => + { + b.Property("Id") + .HasColumnType("bigint"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("bigint"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("LastModified") + .HasColumnType("datetime2"); + + b.Property("LastModifiedBy") + .HasColumnType("bigint"); + + b.Property("ManufacturingYear") + .HasColumnType("int"); + + b.Property("Model") + .HasColumnType("nvarchar(max)"); + + b.Property("Name") + .HasColumnType("nvarchar(max)"); + + b.Property("Version") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.ToTable("Aircraft", "dbo"); + }); + + modelBuilder.Entity("Flight.Airports.Models.Airport", b => + { + b.Property("Id") + .HasColumnType("bigint"); + + b.Property("Address") + .HasColumnType("nvarchar(max)"); + + b.Property("Code") + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("bigint"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("LastModified") + .HasColumnType("datetime2"); + + b.Property("LastModifiedBy") + .HasColumnType("bigint"); + + b.Property("Name") + .HasColumnType("nvarchar(max)"); + + b.Property("Version") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.ToTable("Airport", "dbo"); + }); + + modelBuilder.Entity("Flight.Flights.Models.Flight", b => + { + b.Property("Id") + .HasColumnType("bigint"); + + b.Property("AircraftId") + .HasColumnType("bigint"); + + b.Property("ArriveAirportId") + .HasColumnType("bigint"); + + b.Property("ArriveDate") + .HasColumnType("datetime2"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("bigint"); + + b.Property("DepartureAirportId") + .HasColumnType("bigint"); + + b.Property("DepartureDate") + .HasColumnType("datetime2"); + + b.Property("DurationMinutes") + .HasColumnType("decimal(18,2)"); + + b.Property("FlightDate") + .HasColumnType("datetime2"); + + b.Property("FlightNumber") + .HasColumnType("nvarchar(max)"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("LastModified") + .HasColumnType("datetime2"); + + b.Property("LastModifiedBy") + .HasColumnType("bigint"); + + b.Property("Price") + .HasColumnType("decimal(18,2)"); + + b.Property("Status") + .HasColumnType("int"); + + b.Property("Version") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("AircraftId"); + + b.HasIndex("ArriveAirportId"); + + b.ToTable("Flight", "dbo"); + }); + + modelBuilder.Entity("Flight.Seats.Models.Seat", b => + { + b.Property("Id") + .HasColumnType("bigint"); + + b.Property("Class") + .HasColumnType("int"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("bigint"); + + b.Property("FlightId") + .HasColumnType("bigint"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("LastModified") + .HasColumnType("datetime2"); + + b.Property("LastModifiedBy") + .HasColumnType("bigint"); + + b.Property("SeatNumber") + .HasColumnType("nvarchar(max)"); + + b.Property("Type") + .HasColumnType("int"); + + b.Property("Version") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("FlightId"); + + b.ToTable("Seat", "dbo"); + }); + + modelBuilder.Entity("Flight.Flights.Models.Flight", b => + { + b.HasOne("Flight.Aircrafts.Models.Aircraft", null) + .WithMany() + .HasForeignKey("AircraftId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Flight.Airports.Models.Airport", null) + .WithMany() + .HasForeignKey("ArriveAirportId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Flight.Seats.Models.Seat", b => + { + b.HasOne("Flight.Flights.Models.Flight", null) + .WithMany() + .HasForeignKey("FlightId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/Services/Flight/src/Flight/Data/Migrations/20220421195137_Add-Internal-Messages.cs b/src/Services/Flight/src/Flight/Data/Migrations/20220421195137_Add-Internal-Messages.cs new file mode 100644 index 0000000..4040394 --- /dev/null +++ b/src/Services/Flight/src/Flight/Data/Migrations/20220421195137_Add-Internal-Messages.cs @@ -0,0 +1,36 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Flight.Data.Migrations +{ + public partial class AddInternalMessages : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "InternalMessages", + columns: table => new + { + Id = table.Column(type: "uniqueidentifier", nullable: false), + Name = table.Column(type: "nvarchar(max)", nullable: false), + OccurredOn = table.Column(type: "datetime2", nullable: false), + CommandType = table.Column(type: "nvarchar(max)", nullable: false), + Data = table.Column(type: "nvarchar(max)", nullable: false), + ProcessedOn = table.Column(type: "datetime2", nullable: true), + CorrelationId = table.Column(type: "uniqueidentifier", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_InternalMessages", x => x.Id); + }); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "InternalMessages"); + } + } +} diff --git a/src/Services/Flight/src/Flight/Data/Migrations/20220422121403_Update-EventId.Designer.cs b/src/Services/Flight/src/Flight/Data/Migrations/20220422121403_Update-EventId.Designer.cs new file mode 100644 index 0000000..7de476a --- /dev/null +++ b/src/Services/Flight/src/Flight/Data/Migrations/20220422121403_Update-EventId.Designer.cs @@ -0,0 +1,301 @@ +// +using System; +using Flight.Data; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace Flight.Data.Migrations +{ + [DbContext(typeof(FlightDbContext))] + [Migration("20220422121403_Update-EventId")] + partial class UpdateEventId + { + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "6.0.1") + .HasAnnotation("Relational:MaxIdentifierLength", 128); + + SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder, 1L, 1); + + modelBuilder.Entity("BuildingBlocks.InternalProcessor.InternalMessage", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("CommandType") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("CorrelationId") + .HasColumnType("uniqueidentifier"); + + b.Property("Data") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("OccurredOn") + .HasColumnType("datetime2"); + + b.Property("ProcessedOn") + .HasColumnType("datetime2"); + + b.HasKey("Id"); + + b.ToTable("InternalMessages", (string)null); + }); + + modelBuilder.Entity("BuildingBlocks.Outbox.OutboxMessage", b => + { + b.Property("EventId") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("CorrelationId") + .HasColumnType("uniqueidentifier"); + + b.Property("Data") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("EventType") + .IsRequired() + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("OccurredOn") + .HasColumnType("datetime2"); + + b.Property("ProcessedOn") + .HasColumnType("datetime2"); + + b.Property("Type") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("EventId"); + + b.ToTable("OutboxMessages", (string)null); + }); + + modelBuilder.Entity("Flight.Aircrafts.Models.Aircraft", b => + { + b.Property("Id") + .HasColumnType("bigint"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("bigint"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("LastModified") + .HasColumnType("datetime2"); + + b.Property("LastModifiedBy") + .HasColumnType("bigint"); + + b.Property("ManufacturingYear") + .HasColumnType("int"); + + b.Property("Model") + .HasColumnType("nvarchar(max)"); + + b.Property("Name") + .HasColumnType("nvarchar(max)"); + + b.Property("Version") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.ToTable("Aircraft", "dbo"); + }); + + modelBuilder.Entity("Flight.Airports.Models.Airport", b => + { + b.Property("Id") + .HasColumnType("bigint"); + + b.Property("Address") + .HasColumnType("nvarchar(max)"); + + b.Property("Code") + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("bigint"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("LastModified") + .HasColumnType("datetime2"); + + b.Property("LastModifiedBy") + .HasColumnType("bigint"); + + b.Property("Name") + .HasColumnType("nvarchar(max)"); + + b.Property("Version") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.ToTable("Airport", "dbo"); + }); + + modelBuilder.Entity("Flight.Flights.Models.Flight", b => + { + b.Property("Id") + .HasColumnType("bigint"); + + b.Property("AircraftId") + .HasColumnType("bigint"); + + b.Property("ArriveAirportId") + .HasColumnType("bigint"); + + b.Property("ArriveDate") + .HasColumnType("datetime2"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("bigint"); + + b.Property("DepartureAirportId") + .HasColumnType("bigint"); + + b.Property("DepartureDate") + .HasColumnType("datetime2"); + + b.Property("DurationMinutes") + .HasColumnType("decimal(18,2)"); + + b.Property("FlightDate") + .HasColumnType("datetime2"); + + b.Property("FlightNumber") + .HasColumnType("nvarchar(max)"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("LastModified") + .HasColumnType("datetime2"); + + b.Property("LastModifiedBy") + .HasColumnType("bigint"); + + b.Property("Price") + .HasColumnType("decimal(18,2)"); + + b.Property("Status") + .HasColumnType("int"); + + b.Property("Version") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("AircraftId"); + + b.HasIndex("ArriveAirportId"); + + b.ToTable("Flight", "dbo"); + }); + + modelBuilder.Entity("Flight.Seats.Models.Seat", b => + { + b.Property("Id") + .HasColumnType("bigint"); + + b.Property("Class") + .HasColumnType("int"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("bigint"); + + b.Property("FlightId") + .HasColumnType("bigint"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("LastModified") + .HasColumnType("datetime2"); + + b.Property("LastModifiedBy") + .HasColumnType("bigint"); + + b.Property("SeatNumber") + .HasColumnType("nvarchar(max)"); + + b.Property("Type") + .HasColumnType("int"); + + b.Property("Version") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("FlightId"); + + b.ToTable("Seat", "dbo"); + }); + + modelBuilder.Entity("Flight.Flights.Models.Flight", b => + { + b.HasOne("Flight.Aircrafts.Models.Aircraft", null) + .WithMany() + .HasForeignKey("AircraftId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Flight.Airports.Models.Airport", null) + .WithMany() + .HasForeignKey("ArriveAirportId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Flight.Seats.Models.Seat", b => + { + b.HasOne("Flight.Flights.Models.Flight", null) + .WithMany() + .HasForeignKey("FlightId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/Services/Flight/src/Flight/Data/Migrations/20220422121403_Update-EventId.cs b/src/Services/Flight/src/Flight/Data/Migrations/20220422121403_Update-EventId.cs new file mode 100644 index 0000000..e05e51a --- /dev/null +++ b/src/Services/Flight/src/Flight/Data/Migrations/20220422121403_Update-EventId.cs @@ -0,0 +1,25 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Flight.Data.Migrations +{ + public partial class UpdateEventId : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.RenameColumn( + name: "Id", + table: "OutboxMessages", + newName: "EventId"); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.RenameColumn( + name: "EventId", + table: "OutboxMessages", + newName: "Id"); + } + } +} diff --git a/src/Services/Flight/src/Flight/Data/Migrations/20220422122146_Update-Internal.Designer.cs b/src/Services/Flight/src/Flight/Data/Migrations/20220422122146_Update-Internal.Designer.cs new file mode 100644 index 0000000..aa2267b --- /dev/null +++ b/src/Services/Flight/src/Flight/Data/Migrations/20220422122146_Update-Internal.Designer.cs @@ -0,0 +1,301 @@ +// +using System; +using Flight.Data; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace Flight.Data.Migrations +{ + [DbContext(typeof(FlightDbContext))] + [Migration("20220422122146_Update-Internal")] + partial class UpdateInternal + { + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "6.0.1") + .HasAnnotation("Relational:MaxIdentifierLength", 128); + + SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder, 1L, 1); + + modelBuilder.Entity("BuildingBlocks.InternalProcessor.InternalMessage", b => + { + b.Property("EventId") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("CommandType") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("CorrelationId") + .HasColumnType("uniqueidentifier"); + + b.Property("Data") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("OccurredOn") + .HasColumnType("datetime2"); + + b.Property("ProcessedOn") + .HasColumnType("datetime2"); + + b.HasKey("EventId"); + + b.ToTable("InternalMessages", (string)null); + }); + + modelBuilder.Entity("BuildingBlocks.Outbox.OutboxMessage", b => + { + b.Property("EventId") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("CorrelationId") + .HasColumnType("uniqueidentifier"); + + b.Property("Data") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("EventType") + .IsRequired() + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("OccurredOn") + .HasColumnType("datetime2"); + + b.Property("ProcessedOn") + .HasColumnType("datetime2"); + + b.Property("Type") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("EventId"); + + b.ToTable("OutboxMessages", (string)null); + }); + + modelBuilder.Entity("Flight.Aircrafts.Models.Aircraft", b => + { + b.Property("Id") + .HasColumnType("bigint"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("bigint"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("LastModified") + .HasColumnType("datetime2"); + + b.Property("LastModifiedBy") + .HasColumnType("bigint"); + + b.Property("ManufacturingYear") + .HasColumnType("int"); + + b.Property("Model") + .HasColumnType("nvarchar(max)"); + + b.Property("Name") + .HasColumnType("nvarchar(max)"); + + b.Property("Version") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.ToTable("Aircraft", "dbo"); + }); + + modelBuilder.Entity("Flight.Airports.Models.Airport", b => + { + b.Property("Id") + .HasColumnType("bigint"); + + b.Property("Address") + .HasColumnType("nvarchar(max)"); + + b.Property("Code") + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("bigint"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("LastModified") + .HasColumnType("datetime2"); + + b.Property("LastModifiedBy") + .HasColumnType("bigint"); + + b.Property("Name") + .HasColumnType("nvarchar(max)"); + + b.Property("Version") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.ToTable("Airport", "dbo"); + }); + + modelBuilder.Entity("Flight.Flights.Models.Flight", b => + { + b.Property("Id") + .HasColumnType("bigint"); + + b.Property("AircraftId") + .HasColumnType("bigint"); + + b.Property("ArriveAirportId") + .HasColumnType("bigint"); + + b.Property("ArriveDate") + .HasColumnType("datetime2"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("bigint"); + + b.Property("DepartureAirportId") + .HasColumnType("bigint"); + + b.Property("DepartureDate") + .HasColumnType("datetime2"); + + b.Property("DurationMinutes") + .HasColumnType("decimal(18,2)"); + + b.Property("FlightDate") + .HasColumnType("datetime2"); + + b.Property("FlightNumber") + .HasColumnType("nvarchar(max)"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("LastModified") + .HasColumnType("datetime2"); + + b.Property("LastModifiedBy") + .HasColumnType("bigint"); + + b.Property("Price") + .HasColumnType("decimal(18,2)"); + + b.Property("Status") + .HasColumnType("int"); + + b.Property("Version") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("AircraftId"); + + b.HasIndex("ArriveAirportId"); + + b.ToTable("Flight", "dbo"); + }); + + modelBuilder.Entity("Flight.Seats.Models.Seat", b => + { + b.Property("Id") + .HasColumnType("bigint"); + + b.Property("Class") + .HasColumnType("int"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("bigint"); + + b.Property("FlightId") + .HasColumnType("bigint"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("LastModified") + .HasColumnType("datetime2"); + + b.Property("LastModifiedBy") + .HasColumnType("bigint"); + + b.Property("SeatNumber") + .HasColumnType("nvarchar(max)"); + + b.Property("Type") + .HasColumnType("int"); + + b.Property("Version") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("FlightId"); + + b.ToTable("Seat", "dbo"); + }); + + modelBuilder.Entity("Flight.Flights.Models.Flight", b => + { + b.HasOne("Flight.Aircrafts.Models.Aircraft", null) + .WithMany() + .HasForeignKey("AircraftId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Flight.Airports.Models.Airport", null) + .WithMany() + .HasForeignKey("ArriveAirportId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Flight.Seats.Models.Seat", b => + { + b.HasOne("Flight.Flights.Models.Flight", null) + .WithMany() + .HasForeignKey("FlightId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/Services/Flight/src/Flight/Data/Migrations/20220422122146_Update-Internal.cs b/src/Services/Flight/src/Flight/Data/Migrations/20220422122146_Update-Internal.cs new file mode 100644 index 0000000..7a4648d --- /dev/null +++ b/src/Services/Flight/src/Flight/Data/Migrations/20220422122146_Update-Internal.cs @@ -0,0 +1,25 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Flight.Data.Migrations +{ + public partial class UpdateInternal : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.RenameColumn( + name: "Id", + table: "InternalMessages", + newName: "EventId"); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.RenameColumn( + name: "EventId", + table: "InternalMessages", + newName: "Id"); + } + } +} diff --git a/src/Services/Flight/src/Flight/Data/Migrations/FlightDbContextModelSnapshot.cs b/src/Services/Flight/src/Flight/Data/Migrations/FlightDbContextModelSnapshot.cs new file mode 100644 index 0000000..37f579c --- /dev/null +++ b/src/Services/Flight/src/Flight/Data/Migrations/FlightDbContextModelSnapshot.cs @@ -0,0 +1,299 @@ +// +using System; +using Flight.Data; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace Flight.Data.Migrations +{ + [DbContext(typeof(FlightDbContext))] + partial class FlightDbContextModelSnapshot : ModelSnapshot + { + protected override void BuildModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "6.0.1") + .HasAnnotation("Relational:MaxIdentifierLength", 128); + + SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder, 1L, 1); + + modelBuilder.Entity("BuildingBlocks.InternalProcessor.InternalMessage", b => + { + b.Property("EventId") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("CommandType") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("CorrelationId") + .HasColumnType("uniqueidentifier"); + + b.Property("Data") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("OccurredOn") + .HasColumnType("datetime2"); + + b.Property("ProcessedOn") + .HasColumnType("datetime2"); + + b.HasKey("EventId"); + + b.ToTable("InternalMessages", (string)null); + }); + + modelBuilder.Entity("BuildingBlocks.Outbox.OutboxMessage", b => + { + b.Property("EventId") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("CorrelationId") + .HasColumnType("uniqueidentifier"); + + b.Property("Data") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("EventType") + .IsRequired() + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("OccurredOn") + .HasColumnType("datetime2"); + + b.Property("ProcessedOn") + .HasColumnType("datetime2"); + + b.Property("Type") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("EventId"); + + b.ToTable("OutboxMessages", (string)null); + }); + + modelBuilder.Entity("Flight.Aircrafts.Models.Aircraft", b => + { + b.Property("Id") + .HasColumnType("bigint"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("bigint"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("LastModified") + .HasColumnType("datetime2"); + + b.Property("LastModifiedBy") + .HasColumnType("bigint"); + + b.Property("ManufacturingYear") + .HasColumnType("int"); + + b.Property("Model") + .HasColumnType("nvarchar(max)"); + + b.Property("Name") + .HasColumnType("nvarchar(max)"); + + b.Property("Version") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.ToTable("Aircraft", "dbo"); + }); + + modelBuilder.Entity("Flight.Airports.Models.Airport", b => + { + b.Property("Id") + .HasColumnType("bigint"); + + b.Property("Address") + .HasColumnType("nvarchar(max)"); + + b.Property("Code") + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("bigint"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("LastModified") + .HasColumnType("datetime2"); + + b.Property("LastModifiedBy") + .HasColumnType("bigint"); + + b.Property("Name") + .HasColumnType("nvarchar(max)"); + + b.Property("Version") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.ToTable("Airport", "dbo"); + }); + + modelBuilder.Entity("Flight.Flights.Models.Flight", b => + { + b.Property("Id") + .HasColumnType("bigint"); + + b.Property("AircraftId") + .HasColumnType("bigint"); + + b.Property("ArriveAirportId") + .HasColumnType("bigint"); + + b.Property("ArriveDate") + .HasColumnType("datetime2"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("bigint"); + + b.Property("DepartureAirportId") + .HasColumnType("bigint"); + + b.Property("DepartureDate") + .HasColumnType("datetime2"); + + b.Property("DurationMinutes") + .HasColumnType("decimal(18,2)"); + + b.Property("FlightDate") + .HasColumnType("datetime2"); + + b.Property("FlightNumber") + .HasColumnType("nvarchar(max)"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("LastModified") + .HasColumnType("datetime2"); + + b.Property("LastModifiedBy") + .HasColumnType("bigint"); + + b.Property("Price") + .HasColumnType("decimal(18,2)"); + + b.Property("Status") + .HasColumnType("int"); + + b.Property("Version") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("AircraftId"); + + b.HasIndex("ArriveAirportId"); + + b.ToTable("Flight", "dbo"); + }); + + modelBuilder.Entity("Flight.Seats.Models.Seat", b => + { + b.Property("Id") + .HasColumnType("bigint"); + + b.Property("Class") + .HasColumnType("int"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("bigint"); + + b.Property("FlightId") + .HasColumnType("bigint"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("LastModified") + .HasColumnType("datetime2"); + + b.Property("LastModifiedBy") + .HasColumnType("bigint"); + + b.Property("SeatNumber") + .HasColumnType("nvarchar(max)"); + + b.Property("Type") + .HasColumnType("int"); + + b.Property("Version") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("FlightId"); + + b.ToTable("Seat", "dbo"); + }); + + modelBuilder.Entity("Flight.Flights.Models.Flight", b => + { + b.HasOne("Flight.Aircrafts.Models.Aircraft", null) + .WithMany() + .HasForeignKey("AircraftId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Flight.Airports.Models.Airport", null) + .WithMany() + .HasForeignKey("ArriveAirportId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Flight.Seats.Models.Seat", b => + { + b.HasOne("Flight.Flights.Models.Flight", null) + .WithMany() + .HasForeignKey("FlightId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/Services/Flight/src/Flight/Data/Seed/FlightDataSeeder.cs b/src/Services/Flight/src/Flight/Data/Seed/FlightDataSeeder.cs new file mode 100644 index 0000000..a55e634 --- /dev/null +++ b/src/Services/Flight/src/Flight/Data/Seed/FlightDataSeeder.cs @@ -0,0 +1,97 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using BuildingBlocks.EFCore; +using Flight.Aircrafts.Models; +using Flight.Airports.Models; +using Flight.Flights.Models; +using Flight.Seats.Models; +using Microsoft.EntityFrameworkCore; + +namespace Flight.Data.Seed; + +public class FlightDataSeeder : IDataSeeder +{ + private readonly FlightDbContext _flightDbContext; + + public FlightDataSeeder(FlightDbContext flightDbContext) + { + _flightDbContext = flightDbContext; + } + + public async Task SeedAllAsync() + { + await SeedAirportAsync(); + await SeedAircraftAsync(); + await SeedFlightAsync(); + await SeedSeatAsync(); + } + + private async Task SeedAirportAsync() + { + if (!await _flightDbContext.Airports.AnyAsync()) + { + var airports = new List + { + Airport.Create(1, "Lisbon International Airport", "LIS", "12988"), + Airport.Create(2, "Sao Paulo International Airport", "BRZ", "11200") + }; + + await _flightDbContext.Airports.AddRangeAsync(airports); + await _flightDbContext.SaveChangesAsync(); + } + } + + private async Task SeedAircraftAsync() + { + if (!await _flightDbContext.Aircraft.AnyAsync()) + { + var aircrafts = new List + { + Aircraft.Create(1, "Boeing 737", "B737", 2005), + Aircraft.Create(2, "Airbus 300", "A300", 2000), + Aircraft.Create(3, "Airbus 320", "A320", 2003) + }; + + await _flightDbContext.Aircraft.AddRangeAsync(aircrafts); + await _flightDbContext.SaveChangesAsync(); + } + } + + + private async Task SeedSeatAsync() + { + if (!await _flightDbContext.Seats.AnyAsync()) + { + var seats = new List + { + Seat.Create(1 ,"12A", SeatType.Window, SeatClass.Economy, 1), + Seat.Create(2, "12B", SeatType.Window, SeatClass.Economy, 1), + Seat.Create(3, "12C", SeatType.Middle, SeatClass.Economy, 1), + Seat.Create(4, "12D", SeatType.Middle, SeatClass.Economy, 1), + Seat.Create(5, "12E", SeatType.Aisle, SeatClass.Economy, 1), + Seat.Create(6, "12F", SeatType.Aisle, SeatClass.Economy, 1) + }; + + await _flightDbContext.Seats.AddRangeAsync(seats); + await _flightDbContext.SaveChangesAsync(); + } + } + + private async Task SeedFlightAsync() + { + if (!await _flightDbContext.Flights.AnyAsync()) + { + var flights = new List + { + Flights.Models.Flight.Create(1, "BD467", 1, 1, new DateTime(2022, 1, 31, 12, 0, 0), + new DateTime(2022, 1, 31, 14, 0, 0), + 2, 120m, + new DateTime(2022, 1, 31), FlightStatus.Completed, + 8000) + }; + await _flightDbContext.Flights.AddRangeAsync(flights); + await _flightDbContext.SaveChangesAsync(); + } + } +} diff --git a/src/Services/Flight/src/Flight/Data/readme.md b/src/Services/Flight/src/Flight/Data/readme.md new file mode 100644 index 0000000..ffeca35 --- /dev/null +++ b/src/Services/Flight/src/Flight/Data/readme.md @@ -0,0 +1,2 @@ +dotnet ef migrations add Init --context FlightDbContext -o "Data\Migrations" +dotnet ef database update --context FlightDbContext diff --git a/src/Services/Flight/src/Flight/EventMapper.cs b/src/Services/Flight/src/Flight/EventMapper.cs new file mode 100644 index 0000000..86c0fdd --- /dev/null +++ b/src/Services/Flight/src/Flight/EventMapper.cs @@ -0,0 +1,28 @@ +using System.Collections.Generic; +using System.Linq; +using BuildingBlocks.Contracts.EventBus.Messages; +using BuildingBlocks.Domain; +using BuildingBlocks.Domain.Event; +using Flight.Aircrafts.Events; +using Flight.Airports.Events; +using Flight.Flights.Events.Domain; + +namespace Flight; + +// ref: https://www.ledjonbehluli.com/posts/domain_to_integration_event/ +public sealed class EventMapper : IEventMapper +{ + public IEnumerable MapAll(IEnumerable events) => events.Select(Map); + + public IIntegrationEvent Map(IDomainEvent @event) + { + return @event switch + { + FlightCreatedDomainEvent e => new FlightCreated(e.FlightNumber), + FlightUpdatedDomainEvent e => new FlightUpdated(e.FlightNumber), + AirportCreatedDomainEvent e => new AirportCreated(e.Id), + AircraftCreatedDomainEvent e => new AircraftCreated(e.Id), + _ => null + }; + } +} diff --git a/src/Services/Flight/src/Flight/Extensions/MediatRExtensions.cs b/src/Services/Flight/src/Flight/Extensions/MediatRExtensions.cs new file mode 100644 index 0000000..6ca8a8f --- /dev/null +++ b/src/Services/Flight/src/Flight/Extensions/MediatRExtensions.cs @@ -0,0 +1,22 @@ +using BuildingBlocks.Caching; +using BuildingBlocks.EFCore; +using BuildingBlocks.Logging; +using BuildingBlocks.Validation; +using MediatR; +using Microsoft.Extensions.DependencyInjection; + +namespace Flight.Extensions; + +public static class MediatRExtensions +{ + public static IServiceCollection AddCustomMediatR(this IServiceCollection services) + { + services.AddMediatR(typeof(FlightRoot).Assembly); + services.AddScoped(typeof(IPipelineBehavior<,>), typeof(ValidationBehavior<,>)); + services.AddScoped(typeof(IPipelineBehavior<,>), typeof(LoggingBehavior<,>)); + services.AddScoped(typeof(IPipelineBehavior<,>), typeof(EfTxBehavior<,>)); + services.AddScoped(typeof(IPipelineBehavior<,>), typeof(CachingBehavior<,>)); + + return services; + } +} diff --git a/src/Services/Flight/src/Flight/Extensions/MigrationsExtensions.cs b/src/Services/Flight/src/Flight/Extensions/MigrationsExtensions.cs new file mode 100644 index 0000000..a85130a --- /dev/null +++ b/src/Services/Flight/src/Flight/Extensions/MigrationsExtensions.cs @@ -0,0 +1,37 @@ +using System; +using BuildingBlocks.EFCore; +using Flight.Data; +using Microsoft.AspNetCore.Builder; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.DependencyInjection; + +namespace Flight.Extensions; + +public static class MigrationsExtensions +{ + public static IApplicationBuilder UseMigrations(this IApplicationBuilder app) + { + MigrateDatabase(app.ApplicationServices); + SeedData(app.ApplicationServices); + + return app; + } + + private static void MigrateDatabase(IServiceProvider serviceProvider) + { + using var scope = serviceProvider.CreateScope(); + + var context = scope.ServiceProvider.GetRequiredService(); + context.Database.Migrate(); + } + + private static void SeedData(IServiceProvider serviceProvider) + { + using var scope = serviceProvider.CreateScope(); + var seeders = scope.ServiceProvider.GetServices(); + foreach (var seeder in seeders) + { + seeder.SeedAllAsync().GetAwaiter().GetResult(); + } + } +} diff --git a/src/Services/Flight/src/Flight/Extensions/ProblemDetailsExtensions.cs b/src/Services/Flight/src/Flight/Extensions/ProblemDetailsExtensions.cs new file mode 100644 index 0000000..78d224e --- /dev/null +++ b/src/Services/Flight/src/Flight/Extensions/ProblemDetailsExtensions.cs @@ -0,0 +1,85 @@ +using System; +using BuildingBlocks.Exception; +using Hellang.Middleware.ProblemDetails; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Newtonsoft.Json; + +namespace Flight.Extensions; + +public static class ProblemDetailsExtensions +{ + public static IServiceCollection AddCustomProblemDetails(this IServiceCollection services) + { + services.AddProblemDetails(x => + { + // Control when an exception is included + x.IncludeExceptionDetails = (ctx, _) => + { + // Fetch services from HttpContext.RequestServices + var env = ctx.RequestServices.GetRequiredService(); + return env.IsDevelopment() || env.IsStaging(); + }; + x.Map(ex => new ProblemDetails + { + Title = "Application rule broken", + Status = StatusCodes.Status409Conflict, + Detail = ex.Message, + Type = "https://somedomain/application-rule-validation-error" + }); + + // Exception will produce and returns from our FluentValidation RequestValidationBehavior + x.Map(ex => new ProblemDetails + { + Title = "input validation rules broken", + Status = StatusCodes.Status400BadRequest, + Detail = JsonConvert.SerializeObject(ex.ValidationResultModel.Errors), + Type = "https://somedomain/input-validation-rules-error" + }); + x.Map(ex => new ProblemDetails + { + Title = "bad request exception", + Status = StatusCodes.Status400BadRequest, + Detail = ex.Message, + Type = "https://somedomain/bad-request-error" + }); + x.Map(ex => new ProblemDetails + { + Title = "not found exception", + Status = StatusCodes.Status404NotFound, + Detail = ex.Message, + Type = "https://somedomain/not-found-error" + }); + x.Map(ex => new ProblemDetails + { + Title = "api server exception", + Status = StatusCodes.Status500InternalServerError, + Detail = ex.Message, + Type = "https://somedomain/api-server-error" + }); + x.Map(ex => new ProblemDetails + { + Title = "application exception", + Status = StatusCodes.Status500InternalServerError, + Detail = ex.Message, + Type = "https://somedomain/application-error" + }); + x.Map(ex => + { + var pd = new ProblemDetails + { + Status = (int)ex.StatusCode, + Title = "identity exception", + Detail = ex.Message, + Type = "https://somedomain/identity-error" + }; + + return pd; + }); + x.MapToStatusCode(StatusCodes.Status400BadRequest); + }); + return services; + } +} diff --git a/src/Services/Flight/src/Flight/Flight.csproj b/src/Services/Flight/src/Flight/Flight.csproj new file mode 100644 index 0000000..6bc9a74 --- /dev/null +++ b/src/Services/Flight/src/Flight/Flight.csproj @@ -0,0 +1,26 @@ + + + + net6.0 + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + + + + + + + + diff --git a/src/Services/Flight/src/Flight/FlightRoot.cs b/src/Services/Flight/src/Flight/FlightRoot.cs new file mode 100644 index 0000000..5fe2291 --- /dev/null +++ b/src/Services/Flight/src/Flight/FlightRoot.cs @@ -0,0 +1,6 @@ +namespace Flight; + +public class FlightRoot +{ + +} diff --git a/src/Services/Flight/src/Flight/Flights/Dtos/FlightResponseDto.cs b/src/Services/Flight/src/Flight/Flights/Dtos/FlightResponseDto.cs new file mode 100644 index 0000000..ecbf1af --- /dev/null +++ b/src/Services/Flight/src/Flight/Flights/Dtos/FlightResponseDto.cs @@ -0,0 +1,18 @@ +using System; +using Flight.Flights.Models; + +namespace Flight.Flights.Dtos; +public record FlightResponseDto +{ + public long Id { get; init; } + public string FlightNumber { get; init; } + public long AircraftId { get; init; } + public long DepartureAirportId { get; init; } + public DateTime DepartureDate { get; init; } + public DateTime ArriveDate { get; init; } + public long ArriveAirportId { get; init; } + public decimal DurationMinutes { get; init; } + public DateTime FlightDate { get; init; } + public FlightStatus Status { get; init; } + public decimal Price { get; init; } +} diff --git a/src/Services/Flight/src/Flight/Flights/Events/Domain/FlightCreatedDomainEvent.cs b/src/Services/Flight/src/Flight/Flights/Events/Domain/FlightCreatedDomainEvent.cs new file mode 100644 index 0000000..e37feef --- /dev/null +++ b/src/Services/Flight/src/Flight/Flights/Events/Domain/FlightCreatedDomainEvent.cs @@ -0,0 +1,9 @@ +using System; +using BuildingBlocks.Domain.Event; +using Flight.Flights.Models; + +namespace Flight.Flights.Events.Domain; + +public record FlightCreatedDomainEvent(long Id, string FlightNumber, long AircraftId, DateTime DepartureDate, + long DepartureAirportId, DateTime ArriveDate, long ArriveAirportId, decimal DurationMinutes, + DateTime FlightDate, FlightStatus Status, decimal Price, bool IsDeleted) : IDomainEvent; diff --git a/src/Services/Flight/src/Flight/Flights/Events/Domain/FlightUpdatedDomainEvent.cs b/src/Services/Flight/src/Flight/Flights/Events/Domain/FlightUpdatedDomainEvent.cs new file mode 100644 index 0000000..c00b867 --- /dev/null +++ b/src/Services/Flight/src/Flight/Flights/Events/Domain/FlightUpdatedDomainEvent.cs @@ -0,0 +1,8 @@ +using System; +using BuildingBlocks.Domain.Event; +using Flight.Flights.Models; + +namespace Flight.Flights.Events.Domain; +public record FlightUpdatedDomainEvent(long Id, string FlightNumber, long AircraftId, DateTime DepartureDate, + long DepartureAirportId, DateTime ArriveDate, long ArriveAirportId, decimal DurationMinutes, + DateTime FlightDate, FlightStatus Status, decimal Price, bool IsDeleted) : IDomainEvent; diff --git a/src/Services/Flight/src/Flight/Flights/Exceptions/FlightAlreadyExistException.cs b/src/Services/Flight/src/Flight/Flights/Exceptions/FlightAlreadyExistException.cs new file mode 100644 index 0000000..dc39b3d --- /dev/null +++ b/src/Services/Flight/src/Flight/Flights/Exceptions/FlightAlreadyExistException.cs @@ -0,0 +1,10 @@ +using BuildingBlocks.Exception; + +namespace Flight.Flights.Exceptions; + +public class FlightAlreadyExistException : ConflictException +{ + public FlightAlreadyExistException(string code = default) : base("Flight already exist!", code) + { + } +} diff --git a/src/Services/Flight/src/Flight/Flights/Exceptions/FlightNotFountException.cs b/src/Services/Flight/src/Flight/Flights/Exceptions/FlightNotFountException.cs new file mode 100644 index 0000000..8198fbd --- /dev/null +++ b/src/Services/Flight/src/Flight/Flights/Exceptions/FlightNotFountException.cs @@ -0,0 +1,10 @@ +using BuildingBlocks.Exception; + +namespace Flight.Flights.Exceptions; + +public class FlightNotFountException : NotFoundException +{ + public FlightNotFountException() : base("Flight not found!") + { + } +} diff --git a/src/Services/Flight/src/Flight/Flights/Features/CreateFlight/CreateFlightCommand.cs b/src/Services/Flight/src/Flight/Flights/Features/CreateFlight/CreateFlightCommand.cs new file mode 100644 index 0000000..dd160de --- /dev/null +++ b/src/Services/Flight/src/Flight/Flights/Features/CreateFlight/CreateFlightCommand.cs @@ -0,0 +1,14 @@ +using System; +using BuildingBlocks.IdsGenerator; +using Flight.Flights.Dtos; +using Flight.Flights.Models; +using MediatR; + +namespace Flight.Flights.Features.CreateFlight; + +public record CreateFlightCommand(string FlightNumber, long AircraftId, long DepartureAirportId, + DateTime DepartureDate, DateTime ArriveDate, long ArriveAirportId, + decimal DurationMinutes, DateTime FlightDate, FlightStatus Status, decimal Price) : IRequest +{ + public long Id { get; set; } = SnowFlakIdGenerator.NewId(); +} diff --git a/src/Services/Flight/src/Flight/Flights/Features/CreateFlight/CreateFlightCommandHandler.cs b/src/Services/Flight/src/Flight/Flights/Features/CreateFlight/CreateFlightCommandHandler.cs new file mode 100644 index 0000000..a441d1f --- /dev/null +++ b/src/Services/Flight/src/Flight/Flights/Features/CreateFlight/CreateFlightCommandHandler.cs @@ -0,0 +1,42 @@ +using System.Threading; +using System.Threading.Tasks; +using Ardalis.GuardClauses; +using Flight.Data; +using Flight.Flights.Dtos; +using Flight.Flights.Exceptions; +using Flight.Flights.Models; +using MapsterMapper; +using MediatR; +using Microsoft.EntityFrameworkCore; + +namespace Flight.Flights.Features.CreateFlight; + +public class CreateFlightCommandHandler : IRequestHandler +{ + private readonly FlightDbContext _flightDbContext; + private readonly IMapper _mapper; + + public CreateFlightCommandHandler(IMapper mapper, FlightDbContext flightDbContext) + { + _mapper = mapper; + _flightDbContext = flightDbContext; + } + + public async Task Handle(CreateFlightCommand command, CancellationToken cancellationToken) + { + Guard.Against.Null(command, nameof(command)); + + var flight = await _flightDbContext.Flights.SingleOrDefaultAsync(x => x.FlightNumber == command.FlightNumber && !x.IsDeleted, + cancellationToken); + + if (flight is not null) + throw new FlightAlreadyExistException(); + + var flightEntity = Models.Flight.Create(command.Id, command.FlightNumber, command.AircraftId, command.DepartureAirportId, command.DepartureDate, + command.ArriveDate, command.ArriveAirportId, command.DurationMinutes, command.FlightDate, FlightStatus.Completed, command.Price); + + var newFlight = await _flightDbContext.Flights.AddAsync(flightEntity, cancellationToken); + + return _mapper.Map(newFlight.Entity); + } +} diff --git a/src/Services/Flight/src/Flight/Flights/Features/CreateFlight/CreateFlightCommandValidator.cs b/src/Services/Flight/src/Flight/Flights/Features/CreateFlight/CreateFlightCommandValidator.cs new file mode 100644 index 0000000..f55e221 --- /dev/null +++ b/src/Services/Flight/src/Flight/Flights/Features/CreateFlight/CreateFlightCommandValidator.cs @@ -0,0 +1,27 @@ +using Flight.Flights.Models; +using FluentValidation; + +namespace Flight.Flights.Features.CreateFlight; + +public class CreateFlightCommandValidator : AbstractValidator +{ + public CreateFlightCommandValidator() + { + CascadeMode = CascadeMode.Stop; + + RuleFor(x => x.Price).GreaterThan(0).WithMessage("Price must be greater than 0"); + + RuleFor(x => x.Status).Must(p => (p.GetType().IsEnum && + p == FlightStatus.Flying) || + p == FlightStatus.Canceled || + p == FlightStatus.Delay || + p == FlightStatus.Completed) + .WithMessage("Status must be Flying, Delay, Canceled or Completed"); + + RuleFor(x => x.AircraftId).NotEmpty().WithMessage("AircraftId must be not empty"); + RuleFor(x => x.DepartureAirportId).NotEmpty().WithMessage("DepartureAirportId must be not empty"); + RuleFor(x => x.ArriveAirportId).NotEmpty().WithMessage("ArriveAirportId must be not empty"); + RuleFor(x => x.DurationMinutes).GreaterThan(0).WithMessage("DurationMinutes must be greater than 0"); + RuleFor(x => x.FlightDate).NotEmpty().WithMessage("FlightDate must be not empty"); + } +} diff --git a/src/Services/Flight/src/Flight/Flights/Features/CreateFlight/CreateFlightEndpoint.cs b/src/Services/Flight/src/Flight/Flights/Features/CreateFlight/CreateFlightEndpoint.cs new file mode 100644 index 0000000..b3630d8 --- /dev/null +++ b/src/Services/Flight/src/Flight/Flights/Features/CreateFlight/CreateFlightEndpoint.cs @@ -0,0 +1,25 @@ +using System.Threading; +using System.Threading.Tasks; +using BuildingBlocks.Web; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Swashbuckle.AspNetCore.Annotations; + +namespace Flight.Flights.Features.CreateFlight; + +[Route(BaseApiPath + "/flight")] +public class CreateFlightEndpoint : BaseController +{ + [Authorize] + [HttpPost] + [ProducesResponseType(StatusCodes.Status201Created)] + [ProducesResponseType(StatusCodes.Status400BadRequest)] + [SwaggerOperation(Summary = "Create new flight", Description = "Create new flight")] + public async Task Create([FromBody] CreateFlightCommand command, CancellationToken cancellationToken) + { + var result = await Mediator.Send(command, cancellationToken); + + return Ok(result); + } +} diff --git a/src/Services/Flight/src/Flight/Flights/Features/FlightMappings.cs b/src/Services/Flight/src/Flight/Flights/Features/FlightMappings.cs new file mode 100644 index 0000000..979afde --- /dev/null +++ b/src/Services/Flight/src/Flight/Flights/Features/FlightMappings.cs @@ -0,0 +1,12 @@ +using Flight.Flights.Dtos; +using Mapster; + +namespace Flight.Flights.Features; + +public class FlightMappings : IRegister +{ + public void Register(TypeAdapterConfig config) + { + config.NewConfig(); + } +} diff --git a/src/Services/Flight/src/Flight/Flights/Features/GetAvailableFlights/GetAvailableFlightsEndpoint.cs b/src/Services/Flight/src/Flight/Flights/Features/GetAvailableFlights/GetAvailableFlightsEndpoint.cs new file mode 100644 index 0000000..508a98d --- /dev/null +++ b/src/Services/Flight/src/Flight/Flights/Features/GetAvailableFlights/GetAvailableFlightsEndpoint.cs @@ -0,0 +1,25 @@ +using System.Threading; +using System.Threading.Tasks; +using BuildingBlocks.Web; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Swashbuckle.AspNetCore.Annotations; + +namespace Flight.Flights.Features.GetAvailableFlights; + +[Route(BaseApiPath + "/flight/get-available-flights")] +public class GetAvailableFlightsEndpoint : BaseController +{ + [Authorize] + [HttpGet] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status400BadRequest)] + [SwaggerOperation(Summary = "Get available flights", Description = "Get available flights")] + public async Task GetAvailableFlights([FromRoute] GetAvailableFlightsQuery query, CancellationToken cancellationToken) + { + var result = await Mediator.Send(query, cancellationToken); + + return Ok(result); + } +} diff --git a/src/Services/Flight/src/Flight/Flights/Features/GetAvailableFlights/GetAvailableFlightsQuery.cs b/src/Services/Flight/src/Flight/Flights/Features/GetAvailableFlights/GetAvailableFlightsQuery.cs new file mode 100644 index 0000000..a4f67f7 --- /dev/null +++ b/src/Services/Flight/src/Flight/Flights/Features/GetAvailableFlights/GetAvailableFlightsQuery.cs @@ -0,0 +1,13 @@ +using System; +using System.Collections.Generic; +using BuildingBlocks.Caching; +using Flight.Flights.Dtos; +using MediatR; + +namespace Flight.Flights.Features.GetAvailableFlights; + +public record GetAvailableFlightsQuery : IRequest>, ICacheRequest +{ + public string CacheKey => "GetAvailableFlightsQuery"; + public DateTime? AbsoluteExpirationRelativeToNow => DateTime.Now.AddHours(1); +} diff --git a/src/Services/Flight/src/Flight/Flights/Features/GetAvailableFlights/GetAvailableFlightsQueryHandler.cs b/src/Services/Flight/src/Flight/Flights/Features/GetAvailableFlights/GetAvailableFlightsQueryHandler.cs new file mode 100644 index 0000000..2b8a8e5 --- /dev/null +++ b/src/Services/Flight/src/Flight/Flights/Features/GetAvailableFlights/GetAvailableFlightsQueryHandler.cs @@ -0,0 +1,38 @@ +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Ardalis.GuardClauses; +using Flight.Data; +using Flight.Flights.Dtos; +using Flight.Flights.Exceptions; +using MapsterMapper; +using MediatR; +using Microsoft.EntityFrameworkCore; + +namespace Flight.Flights.Features.GetAvailableFlights; + +public class GetAvailableFlightsQueryHandler : IRequestHandler> +{ + private readonly FlightDbContext _flightDbContext; + private readonly IMapper _mapper; + + public GetAvailableFlightsQueryHandler(IMapper mapper, FlightDbContext flightDbContext) + { + _mapper = mapper; + _flightDbContext = flightDbContext; + } + + public async Task> Handle(GetAvailableFlightsQuery query, + CancellationToken cancellationToken) + { + Guard.Against.Null(query, nameof(query)); + + var flight = await _flightDbContext.Flights.Where(x => !x.IsDeleted).ToListAsync(cancellationToken); + + if (!flight.Any()) + throw new FlightNotFountException(); + + return _mapper.Map>(flight); + } +} diff --git a/src/Services/Flight/src/Flight/Flights/Features/GetAvailableFlights/GetAvailableFlightsQueryValidator.cs b/src/Services/Flight/src/Flight/Flights/Features/GetAvailableFlights/GetAvailableFlightsQueryValidator.cs new file mode 100644 index 0000000..39cff6e --- /dev/null +++ b/src/Services/Flight/src/Flight/Flights/Features/GetAvailableFlights/GetAvailableFlightsQueryValidator.cs @@ -0,0 +1,7 @@ +using FluentValidation; + +namespace Flight.Flights.Features.GetAvailableFlights; + +public class GetAvailableFlightsQueryValidator : AbstractValidator +{ +} diff --git a/src/Services/Flight/src/Flight/Flights/Features/GetFlightById/GetFlightByIdEndpoint.cs b/src/Services/Flight/src/Flight/Flights/Features/GetFlightById/GetFlightByIdEndpoint.cs new file mode 100644 index 0000000..4d93d06 --- /dev/null +++ b/src/Services/Flight/src/Flight/Flights/Features/GetFlightById/GetFlightByIdEndpoint.cs @@ -0,0 +1,25 @@ +using System.Threading; +using System.Threading.Tasks; +using BuildingBlocks.Web; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Swashbuckle.AspNetCore.Annotations; + +namespace Flight.Flights.Features.GetFlightById; + +[Route(BaseApiPath + "/flight")] +public class GetFlightByIdEndpoint: BaseController +{ + [Authorize] + [HttpGet("{id}")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status400BadRequest)] + [SwaggerOperation(Summary = "Get flight by id", Description = "Get flight by id")] + public async Task GetById([FromRoute] GetFlightByIdQuery query, CancellationToken cancellationToken) + { + var result = await Mediator.Send(query, cancellationToken); + + return Ok(result); + } +} diff --git a/src/Services/Flight/src/Flight/Flights/Features/GetFlightById/GetFlightByIdQuery.cs b/src/Services/Flight/src/Flight/Flights/Features/GetFlightById/GetFlightByIdQuery.cs new file mode 100644 index 0000000..f6bc974 --- /dev/null +++ b/src/Services/Flight/src/Flight/Flights/Features/GetFlightById/GetFlightByIdQuery.cs @@ -0,0 +1,6 @@ +using Flight.Flights.Dtos; +using MediatR; + +namespace Flight.Flights.Features.GetFlightById; + +public record GetFlightByIdQuery(long Id) : IRequest; diff --git a/src/Services/Flight/src/Flight/Flights/Features/GetFlightById/GetFlightByIdQueryHandler.cs b/src/Services/Flight/src/Flight/Flights/Features/GetFlightById/GetFlightByIdQueryHandler.cs new file mode 100644 index 0000000..c1efa05 --- /dev/null +++ b/src/Services/Flight/src/Flight/Flights/Features/GetFlightById/GetFlightByIdQueryHandler.cs @@ -0,0 +1,36 @@ +using System.Threading; +using System.Threading.Tasks; +using Ardalis.GuardClauses; +using Flight.Data; +using Flight.Flights.Dtos; +using Flight.Flights.Exceptions; +using MapsterMapper; +using MediatR; +using Microsoft.EntityFrameworkCore; + +namespace Flight.Flights.Features.GetFlightById; + +public class GetFlightByIdQueryHandler : IRequestHandler +{ + private readonly FlightDbContext _flightDbContext; + private readonly IMapper _mapper; + + public GetFlightByIdQueryHandler(IMapper mapper, FlightDbContext flightDbContext) + { + _mapper = mapper; + _flightDbContext = flightDbContext; + } + + public async Task Handle(GetFlightByIdQuery query, CancellationToken cancellationToken) + { + Guard.Against.Null(query, nameof(query)); + + var flight = + await _flightDbContext.Flights.SingleOrDefaultAsync(x => x.Id == query.Id && !x.IsDeleted, cancellationToken); + + if (flight is null) + throw new FlightNotFountException(); + + return _mapper.Map(flight); + } +} diff --git a/src/Services/Flight/src/Flight/Flights/Features/GetFlightById/GetFlightByIdQueryValidator.cs b/src/Services/Flight/src/Flight/Flights/Features/GetFlightById/GetFlightByIdQueryValidator.cs new file mode 100644 index 0000000..5a8252f --- /dev/null +++ b/src/Services/Flight/src/Flight/Flights/Features/GetFlightById/GetFlightByIdQueryValidator.cs @@ -0,0 +1,13 @@ +using FluentValidation; + +namespace Flight.Flights.Features.GetFlightById; + +public class GetFlightByIdQueryValidator : AbstractValidator +{ + public GetFlightByIdQueryValidator() + { + CascadeMode = CascadeMode.Stop; + + RuleFor(x => x.Id).NotNull().WithMessage("Id is required!"); + } +} diff --git a/src/Services/Flight/src/Flight/Flights/Features/UpdateFlight/UpdateFlightCommand.cs b/src/Services/Flight/src/Flight/Flights/Features/UpdateFlight/UpdateFlightCommand.cs new file mode 100644 index 0000000..b85bdd7 --- /dev/null +++ b/src/Services/Flight/src/Flight/Flights/Features/UpdateFlight/UpdateFlightCommand.cs @@ -0,0 +1,26 @@ +using System; +using BuildingBlocks.Caching; +using Flight.Flights.Dtos; +using Flight.Flights.Models; +using MediatR; + +namespace Flight.Flights.Features.UpdateFlight; + +public record UpdateFlightCommand : IRequest, IInvalidateCacheRequest +{ + public long Id { get; init; } + public string FlightNumber { get; init; } + public long AircraftId { get; init; } + public long DepartureAirportId { get; init; } + public DateTime DepartureDate { get; init; } + public DateTime ArriveDate { get; init; } + public long ArriveAirportId { get; init; } + public decimal DurationMinutes { get; init; } + public DateTime FlightDate { get; init; } + + public FlightStatus Status { get; init; } + + public bool IsDeleted { get; init; } = false; + public decimal Price { get; init; } + public string CacheKey => "GetAvailableFlightsQuery"; +} diff --git a/src/Services/Flight/src/Flight/Flights/Features/UpdateFlight/UpdateFlightCommandHandler.cs b/src/Services/Flight/src/Flight/Flights/Features/UpdateFlight/UpdateFlightCommandHandler.cs new file mode 100644 index 0000000..2276cb6 --- /dev/null +++ b/src/Services/Flight/src/Flight/Flights/Features/UpdateFlight/UpdateFlightCommandHandler.cs @@ -0,0 +1,44 @@ +using System.Threading; +using System.Threading.Tasks; +using Ardalis.GuardClauses; +using BuildingBlocks.EventStoreDB.Repository; +using Flight.Data; +using Flight.Flights.Dtos; +using Flight.Flights.Exceptions; +using Flight.Flights.Models; +using MapsterMapper; +using MediatR; +using Microsoft.EntityFrameworkCore; + +namespace Flight.Flights.Features.UpdateFlight; + +public class UpdateFlightCommandHandler : IRequestHandler +{ + private readonly FlightDbContext _flightDbContext; + private readonly IMapper _mapper; + + public UpdateFlightCommandHandler(IMapper mapper, FlightDbContext flightDbContext) + { + _mapper = mapper; + _flightDbContext = flightDbContext; + } + + public async Task Handle(UpdateFlightCommand command, CancellationToken cancellationToken) + { + Guard.Against.Null(command, nameof(command)); + + var flight = await _flightDbContext.Flights.SingleOrDefaultAsync(x => x.FlightNumber == command.FlightNumber && !x.IsDeleted, + cancellationToken); + + if (flight is null) + throw new FlightNotFountException(); + + + flight.Update(command.Id, command.FlightNumber, command.AircraftId, command.DepartureAirportId, command.DepartureDate, + command.ArriveDate, command.ArriveAirportId, command.DurationMinutes, command.FlightDate, FlightStatus.Completed, command.Price, command.IsDeleted); + + var updateFlight = _flightDbContext.Flights.Update(flight); + + return _mapper.Map(updateFlight.Entity); + } +} diff --git a/src/Services/Flight/src/Flight/Flights/Features/UpdateFlight/UpdateFlightCommandValidator.cs b/src/Services/Flight/src/Flight/Flights/Features/UpdateFlight/UpdateFlightCommandValidator.cs new file mode 100644 index 0000000..4a48f9d --- /dev/null +++ b/src/Services/Flight/src/Flight/Flights/Features/UpdateFlight/UpdateFlightCommandValidator.cs @@ -0,0 +1,28 @@ +using Flight.Flights.Features.CreateFlight; +using Flight.Flights.Models; +using FluentValidation; + +namespace Flight.Flights.Features.UpdateFlight; + +public class UpdateFlightCommandValidator : AbstractValidator +{ + public UpdateFlightCommandValidator() + { + CascadeMode = CascadeMode.Stop; + + RuleFor(x => x.Price).GreaterThan(0).WithMessage("Price must be greater than 0"); + + RuleFor(x => x.Status).Must(p => (p.GetType().IsEnum && + p == FlightStatus.Flying) || + p == FlightStatus.Canceled || + p == FlightStatus.Delay || + p == FlightStatus.Completed) + .WithMessage("Status must be Flying, Delay, Canceled or Completed"); + + RuleFor(x => x.AircraftId).NotEmpty().WithMessage("AircraftId must be not empty"); + RuleFor(x => x.DepartureAirportId).NotEmpty().WithMessage("DepartureAirportId must be not empty"); + RuleFor(x => x.ArriveAirportId).NotEmpty().WithMessage("ArriveAirportId must be not empty"); + RuleFor(x => x.DurationMinutes).GreaterThan(0).WithMessage("DurationMinutes must be greater than 0"); + RuleFor(x => x.FlightDate).NotEmpty().WithMessage("FlightDate must be not empty"); + } +} diff --git a/src/Services/Flight/src/Flight/Flights/Features/UpdateFlight/UpdateFlightEndpoint.cs b/src/Services/Flight/src/Flight/Flights/Features/UpdateFlight/UpdateFlightEndpoint.cs new file mode 100644 index 0000000..fbc6e68 --- /dev/null +++ b/src/Services/Flight/src/Flight/Flights/Features/UpdateFlight/UpdateFlightEndpoint.cs @@ -0,0 +1,25 @@ +using System.Threading; +using System.Threading.Tasks; +using BuildingBlocks.Web; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Swashbuckle.AspNetCore.Annotations; + +namespace Flight.Flights.Features.UpdateFlight; + +[Route(BaseApiPath + "/flight")] +public class UpdateFlightEndpoint : BaseController +{ + [Authorize] + [HttpPut] + [ProducesResponseType(StatusCodes.Status201Created)] + [ProducesResponseType(StatusCodes.Status400BadRequest)] + [SwaggerOperation(Summary = "Update flight", Description = "Update flight")] + public async Task Update(UpdateFlightCommand command, CancellationToken cancellationToken) + { + var result = await Mediator.Send(command, cancellationToken); + + return Ok(result); + } +} diff --git a/src/Services/Flight/src/Flight/Flights/Models/Flight.cs b/src/Services/Flight/src/Flight/Flights/Models/Flight.cs new file mode 100644 index 0000000..7765cec --- /dev/null +++ b/src/Services/Flight/src/Flight/Flights/Models/Flight.cs @@ -0,0 +1,62 @@ +using System; +using BuildingBlocks.Domain.Model; +using Flight.Flights.Events.Domain; + +namespace Flight.Flights.Models; + +public class Flight : Aggregate +{ + public string FlightNumber { get; private set; } + public long AircraftId { get; private set; } + public DateTime DepartureDate { get; private set; } + public long DepartureAirportId { get; private set; } + public DateTime ArriveDate { get; private set; } + public long ArriveAirportId { get; private set; } + public decimal DurationMinutes { get; private set; } + public DateTime FlightDate { get; private set; } + public FlightStatus Status { get; private set; } + public decimal Price { get; private set; } + + public static Flight Create(long id, string flightNumber, long aircraftId, + long departureAirportId, DateTime departureDate, DateTime arriveDate, + long arriveAirportId, decimal durationMinutes, DateTime flightDate, FlightStatus status, + decimal price, bool isDeleted = false) + { + var flight = new Flight + { + Id = id, + FlightNumber = flightNumber, + AircraftId = aircraftId, + DepartureAirportId = departureAirportId, + DepartureDate = departureDate, + ArriveDate = arriveDate, + ArriveAirportId = arriveAirportId, + DurationMinutes = durationMinutes, + FlightDate = flightDate, + Status = status, + Price = price, + IsDeleted = isDeleted, + }; + + var @event = new FlightCreatedDomainEvent(flight.Id, flight.FlightNumber, flight.AircraftId, + flight.DepartureDate, flight.DepartureAirportId, + flight.ArriveDate, flight.ArriveAirportId, flight.DurationMinutes, flight.FlightDate, flight.Status, + flight.Price, flight.IsDeleted); + + flight.AddDomainEvent(@event); + + return flight; + } + + + public void Update(long id, string flightNumber, long aircraftId, + long departureAirportId, DateTime departureDate, DateTime arriveDate, + long arriveAirportId, decimal durationMinutes, DateTime flightDate, FlightStatus status, + decimal price, bool isDeleted = false) + { + var @event = new FlightUpdatedDomainEvent(id, flightNumber, aircraftId, departureDate, departureAirportId, + arriveDate, arriveAirportId, durationMinutes, flightDate, status, price, isDeleted); + + AddDomainEvent(@event); + } +} diff --git a/src/Services/Flight/src/Flight/Flights/Models/FlightStatus.cs b/src/Services/Flight/src/Flight/Flights/Models/FlightStatus.cs new file mode 100644 index 0000000..9eacbdf --- /dev/null +++ b/src/Services/Flight/src/Flight/Flights/Models/FlightStatus.cs @@ -0,0 +1,9 @@ +namespace Flight.Flights.Models; + +public enum FlightStatus +{ + Flying = 1, + Delay = 2, + Canceled = 3, + Completed = 4 +} diff --git a/src/Services/Flight/src/Flight/Flights/Models/Reads/FlightReadModel.cs b/src/Services/Flight/src/Flight/Flights/Models/Reads/FlightReadModel.cs new file mode 100644 index 0000000..60a062a --- /dev/null +++ b/src/Services/Flight/src/Flight/Flights/Models/Reads/FlightReadModel.cs @@ -0,0 +1,21 @@ +using System; +using MongoDB.Bson; +using MongoDB.Bson.Serialization.Attributes; + +namespace Flight.Flights.Models.Reads; + +public class FlightReadModel +{ + public long Id { get; init; } + public string FlightNumber { get; init; } + public long AircraftId { get; init; } + public DateTime DepartureDate { get; init; } + public long DepartureAirportId { get; init; } + public DateTime ArriveDate { get; init; } + public long ArriveAirportId { get; init; } + public decimal DurationMinutes { get; init; } + public DateTime FlightDate { get; init; } + public FlightStatus Status { get; init; } + public decimal Price { get; init; } + public bool IsDeleted { get; set; } +} diff --git a/src/Services/Flight/src/Flight/GrpcServer/FlightGrpcService.cs b/src/Services/Flight/src/Flight/GrpcServer/FlightGrpcService.cs new file mode 100644 index 0000000..d0dcc59 --- /dev/null +++ b/src/Services/Flight/src/Flight/GrpcServer/FlightGrpcService.cs @@ -0,0 +1,40 @@ +using System.Collections.Generic; +using BuildingBlocks.Contracts.Grpc; +using Flight.Flights.Features.GetFlightById; +using Flight.Seats.Features.GetAvailableSeats; +using Flight.Seats.Features.ReserveSeat; +using MagicOnion; +using MagicOnion.Server; +using Mapster; +using MediatR; +using SeatResponseDto = BuildingBlocks.Contracts.Grpc.SeatResponseDto; + +namespace Flight.GrpcServer; + +public class FlightGrpcService : ServiceBase, IFlightGrpcService +{ + private readonly IMediator _mediator; + + public FlightGrpcService(IMediator mediator) + { + _mediator = mediator; + } + + public async UnaryResult GetById(long id) + { + var result = await _mediator.Send(new GetFlightByIdQuery(id)); + return result.Adapt(); + } + + public async UnaryResult> GetAvailableSeats(long flightId) + { + var result = await _mediator.Send(new GetAvailableSeatsQuery(flightId)); + return result.Adapt>(); + } + + public async UnaryResult ReserveSeat(ReserveSeatRequestDto request) + { + var result = await _mediator.Send(new ReserveSeatCommand(request.FlightId, request.SeatNumber)); + return result.Adapt(); + } +} diff --git a/src/Services/Flight/src/Flight/Seats/Dtos/SeatResponseDto.cs b/src/Services/Flight/src/Flight/Seats/Dtos/SeatResponseDto.cs new file mode 100644 index 0000000..8b1603b --- /dev/null +++ b/src/Services/Flight/src/Flight/Seats/Dtos/SeatResponseDto.cs @@ -0,0 +1,12 @@ +using Flight.Seats.Models; + +namespace Flight.Seats.Dtos; + +public record SeatResponseDto +{ + public long Id { get; set; } + public string SeatNumber { get; init; } + public SeatType Type { get; init; } + public SeatClass Class { get; init; } + public long FlightId { get; init; } +} diff --git a/src/Services/Flight/src/Flight/Seats/Events/SeatCreatedDomainEvent.cs b/src/Services/Flight/src/Flight/Seats/Events/SeatCreatedDomainEvent.cs new file mode 100644 index 0000000..3c7afed --- /dev/null +++ b/src/Services/Flight/src/Flight/Seats/Events/SeatCreatedDomainEvent.cs @@ -0,0 +1,6 @@ +using BuildingBlocks.Domain.Event; +using Flight.Seats.Models; + +namespace Flight.Seats.Events; + +public record SeatCreatedDomainEvent(long Id, string SeatNumber, SeatType Type, SeatClass Class, long FlightId) : IDomainEvent; diff --git a/src/Services/Flight/src/Flight/Seats/Exceptions/AllSeatsFullException.cs b/src/Services/Flight/src/Flight/Seats/Exceptions/AllSeatsFullException.cs new file mode 100644 index 0000000..a23a4b9 --- /dev/null +++ b/src/Services/Flight/src/Flight/Seats/Exceptions/AllSeatsFullException.cs @@ -0,0 +1,10 @@ +using BuildingBlocks.Exception; + +namespace Flight.Seats.Exceptions; + +public class AllSeatsFullException : BadRequestException +{ + public AllSeatsFullException() : base("All seats are full!") + { + } +} diff --git a/src/Services/Flight/src/Flight/Seats/Exceptions/SeatAlreadyChosenException.cs b/src/Services/Flight/src/Flight/Seats/Exceptions/SeatAlreadyChosenException.cs new file mode 100644 index 0000000..4ddcff5 --- /dev/null +++ b/src/Services/Flight/src/Flight/Seats/Exceptions/SeatAlreadyChosenException.cs @@ -0,0 +1,10 @@ +using BuildingBlocks.Exception; + +namespace Flight.Seats.Exceptions; + +public class SeatAlreadyChosenException : ConflictException +{ + public SeatAlreadyChosenException(string code = default) : base("Seat already chosen!", code) + { + } +} diff --git a/src/Services/Flight/src/Flight/Seats/Exceptions/SeatAlreadyExistException.cs b/src/Services/Flight/src/Flight/Seats/Exceptions/SeatAlreadyExistException.cs new file mode 100644 index 0000000..730df1c --- /dev/null +++ b/src/Services/Flight/src/Flight/Seats/Exceptions/SeatAlreadyExistException.cs @@ -0,0 +1,10 @@ +using BuildingBlocks.Exception; + +namespace Flight.Seats.Exceptions; + +public class SeatAlreadyExistException : ConflictException +{ + public SeatAlreadyExistException(string code = default) : base("Seat already exist!", code) + { + } +} diff --git a/src/Services/Flight/src/Flight/Seats/Exceptions/SeatNumberIncorrectException.cs b/src/Services/Flight/src/Flight/Seats/Exceptions/SeatNumberIncorrectException.cs new file mode 100644 index 0000000..e434104 --- /dev/null +++ b/src/Services/Flight/src/Flight/Seats/Exceptions/SeatNumberIncorrectException.cs @@ -0,0 +1,10 @@ +using BuildingBlocks.Exception; + +namespace Flight.Seats.Exceptions; + +public class SeatNumberIncorrectException : BadRequestException +{ + public SeatNumberIncorrectException() : base("Seat number is incorrect!") + { + } +} diff --git a/src/Services/Flight/src/Flight/Seats/Features/CreateSeat/CreateSeatCommand.cs b/src/Services/Flight/src/Flight/Seats/Features/CreateSeat/CreateSeatCommand.cs new file mode 100644 index 0000000..fb44da7 --- /dev/null +++ b/src/Services/Flight/src/Flight/Seats/Features/CreateSeat/CreateSeatCommand.cs @@ -0,0 +1,11 @@ +using BuildingBlocks.IdsGenerator; +using Flight.Seats.Dtos; +using Flight.Seats.Models; +using MediatR; + +namespace Flight.Seats.Features.CreateSeat; + +public record CreateSeatCommand(string SeatNumber, SeatType Type, SeatClass Class, long FlightId) : IRequest +{ + public long Id { get; set; } = SnowFlakIdGenerator.NewId(); +} diff --git a/src/Services/Flight/src/Flight/Seats/Features/CreateSeat/CreateSeatCommandHandler.cs b/src/Services/Flight/src/Flight/Seats/Features/CreateSeat/CreateSeatCommandHandler.cs new file mode 100644 index 0000000..dd233bf --- /dev/null +++ b/src/Services/Flight/src/Flight/Seats/Features/CreateSeat/CreateSeatCommandHandler.cs @@ -0,0 +1,45 @@ +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Ardalis.GuardClauses; +using BuildingBlocks.EventStoreDB.Repository; +using Flight.Airports.Exceptions; +using Flight.Airports.Features.CreateAirport; +using Flight.Airports.Models; +using Flight.Data; +using Flight.Seats.Dtos; +using Flight.Seats.Exceptions; +using Flight.Seats.Models; +using MapsterMapper; +using MediatR; +using Microsoft.EntityFrameworkCore; + +namespace Flight.Seats.Features.CreateSeat; + +public class CreateSeatCommandHandler : IRequestHandler +{ + private readonly FlightDbContext _flightDbContext; + private readonly IMapper _mapper; + + public CreateSeatCommandHandler(IMapper mapper, FlightDbContext flightDbContext) + { + _mapper = mapper; + _flightDbContext = flightDbContext; + } + + public async Task Handle(CreateSeatCommand command, CancellationToken cancellationToken) + { + Guard.Against.Null(command, nameof(command)); + + var seat = await _flightDbContext.Seats.SingleOrDefaultAsync(x => x.Id == command.Id && !x.IsDeleted, cancellationToken); + + if (seat is not null) + throw new SeatAlreadyExistException(); + + var seatEntity = Seat.Create(command.Id, command.SeatNumber, command.Type, command.Class, command.FlightId); + + var newSeat = await _flightDbContext.Seats.AddAsync(seatEntity, cancellationToken); + + return _mapper.Map(newSeat.Entity); + } +} diff --git a/src/Services/Flight/src/Flight/Seats/Features/CreateSeat/CreateSeatCommandValidator.cs b/src/Services/Flight/src/Flight/Seats/Features/CreateSeat/CreateSeatCommandValidator.cs new file mode 100644 index 0000000..5b08ee0 --- /dev/null +++ b/src/Services/Flight/src/Flight/Seats/Features/CreateSeat/CreateSeatCommandValidator.cs @@ -0,0 +1,21 @@ +using Flight.Airports.Features.CreateAirport; +using Flight.Seats.Models; +using FluentValidation; + +namespace Flight.Seats.Features.CreateSeat; + +public class CreateSeatCommandValidator : AbstractValidator +{ + public CreateSeatCommandValidator() + { + CascadeMode = CascadeMode.Stop; + + RuleFor(x => x.SeatNumber).NotEmpty().WithMessage("SeatNumber is required"); + RuleFor(x => x.FlightId).NotEmpty().WithMessage("FlightId is required"); + RuleFor(x => x.Class).Must(p => (p.GetType().IsEnum && + p == SeatClass.FirstClass) || + p == SeatClass.Business || + p == SeatClass.Economy) + .WithMessage("Status must be FirstClass, Business or Economy"); + } +} diff --git a/src/Services/Flight/src/Flight/Seats/Features/CreateSeat/CreateSeatEndpoint.cs b/src/Services/Flight/src/Flight/Seats/Features/CreateSeat/CreateSeatEndpoint.cs new file mode 100644 index 0000000..77f6e7b --- /dev/null +++ b/src/Services/Flight/src/Flight/Seats/Features/CreateSeat/CreateSeatEndpoint.cs @@ -0,0 +1,24 @@ +using System.Threading; +using System.Threading.Tasks; +using BuildingBlocks.Web; +using Flight.Airports.Features.CreateAirport; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Swashbuckle.AspNetCore.Annotations; + +namespace Flight.Seats.Features.CreateSeat; + +[Route(BaseApiPath + "/flight/seat")] +public class CreateSeatEndpoint : BaseController +{ + [HttpPost] + [ProducesResponseType(StatusCodes.Status201Created)] + [ProducesResponseType(StatusCodes.Status400BadRequest)] + [SwaggerOperation(Summary = "Create new seat", Description = "Create new seat")] + public async Task Create(CreateSeatCommand command, CancellationToken cancellationToken) + { + var result = await Mediator.Send(command, cancellationToken); + + return Ok(result); + } +} diff --git a/src/Services/Flight/src/Flight/Seats/Features/GetAvailableSeats/GetAvailableSeatsEndpoint.cs b/src/Services/Flight/src/Flight/Seats/Features/GetAvailableSeats/GetAvailableSeatsEndpoint.cs new file mode 100644 index 0000000..e1f1c04 --- /dev/null +++ b/src/Services/Flight/src/Flight/Seats/Features/GetAvailableSeats/GetAvailableSeatsEndpoint.cs @@ -0,0 +1,26 @@ +using System.Threading; +using System.Threading.Tasks; +using BuildingBlocks.Web; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Swashbuckle.AspNetCore.Annotations; + +namespace Flight.Seats.Features.GetAvailableSeats; + +[Route(BaseApiPath + "/flight/get-available-seats")] +public class GetAvailableSeatsEndpoint : BaseController +{ + [Authorize] + [HttpGet("{flightId}")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status400BadRequest)] + [SwaggerOperation(Summary = "Get available seats", Description = "Get available seats")] + public async Task GetAvailableSeats([FromRoute] GetAvailableSeatsQuery query, + CancellationToken cancellationToken) + { + var result = await Mediator.Send(query, cancellationToken); + + return Ok(result); + } +} diff --git a/src/Services/Flight/src/Flight/Seats/Features/GetAvailableSeats/GetAvailableSeatsQuery.cs b/src/Services/Flight/src/Flight/Seats/Features/GetAvailableSeats/GetAvailableSeatsQuery.cs new file mode 100644 index 0000000..0c9df58 --- /dev/null +++ b/src/Services/Flight/src/Flight/Seats/Features/GetAvailableSeats/GetAvailableSeatsQuery.cs @@ -0,0 +1,7 @@ +using System.Collections.Generic; +using Flight.Seats.Dtos; +using MediatR; + +namespace Flight.Seats.Features.GetAvailableSeats; + +public record GetAvailableSeatsQuery(long FlightId) : IRequest>; diff --git a/src/Services/Flight/src/Flight/Seats/Features/GetAvailableSeats/GetAvailableSeatsQueryHandler.cs b/src/Services/Flight/src/Flight/Seats/Features/GetAvailableSeats/GetAvailableSeatsQueryHandler.cs new file mode 100644 index 0000000..2d4cac7 --- /dev/null +++ b/src/Services/Flight/src/Flight/Seats/Features/GetAvailableSeats/GetAvailableSeatsQueryHandler.cs @@ -0,0 +1,38 @@ +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Ardalis.GuardClauses; +using Flight.Data; +using Flight.Seats.Dtos; +using Flight.Seats.Exceptions; +using MapsterMapper; +using MediatR; +using Microsoft.EntityFrameworkCore; + +namespace Flight.Seats.Features.GetAvailableSeats; + +public class GetAvailableSeatsQueryHandler : IRequestHandler> +{ + private readonly FlightDbContext _flightDbContext; + private readonly IMapper _mapper; + + public GetAvailableSeatsQueryHandler(IMapper mapper, FlightDbContext flightDbContext) + { + _mapper = mapper; + _flightDbContext = flightDbContext; + } + + + public async Task> Handle(GetAvailableSeatsQuery query, CancellationToken cancellationToken) + { + Guard.Against.Null(query, nameof(query)); + + var seats = await _flightDbContext.Seats.Where(x => x.FlightId == query.FlightId && !x.IsDeleted).ToListAsync(cancellationToken); + + if (!seats.Any()) + throw new AllSeatsFullException(); + + return _mapper.Map>(seats); + } +} diff --git a/src/Services/Flight/src/Flight/Seats/Features/GetAvailableSeats/GetAvailableSeatsQueryValidator.cs b/src/Services/Flight/src/Flight/Seats/Features/GetAvailableSeats/GetAvailableSeatsQueryValidator.cs new file mode 100644 index 0000000..4d35498 --- /dev/null +++ b/src/Services/Flight/src/Flight/Seats/Features/GetAvailableSeats/GetAvailableSeatsQueryValidator.cs @@ -0,0 +1,13 @@ +using FluentValidation; + +namespace Flight.Seats.Features.GetAvailableSeats; + +public class GetAvailableSeatsQueryValidator : AbstractValidator +{ + public GetAvailableSeatsQueryValidator() + { + CascadeMode = CascadeMode.Stop; + + RuleFor(x => x.FlightId).NotNull().WithMessage("FlightId is required!"); + } +} diff --git a/src/Services/Flight/src/Flight/Seats/Features/ReserveSeat/ReserveSeatCommand.cs b/src/Services/Flight/src/Flight/Seats/Features/ReserveSeat/ReserveSeatCommand.cs new file mode 100644 index 0000000..96ae1d6 --- /dev/null +++ b/src/Services/Flight/src/Flight/Seats/Features/ReserveSeat/ReserveSeatCommand.cs @@ -0,0 +1,6 @@ +using Flight.Seats.Dtos; +using MediatR; + +namespace Flight.Seats.Features.ReserveSeat; + +public record ReserveSeatCommand(long FlightId, string SeatNumber) : IRequest; diff --git a/src/Services/Flight/src/Flight/Seats/Features/ReserveSeat/ReserveSeatCommandHandler.cs b/src/Services/Flight/src/Flight/Seats/Features/ReserveSeat/ReserveSeatCommandHandler.cs new file mode 100644 index 0000000..8fdd692 --- /dev/null +++ b/src/Services/Flight/src/Flight/Seats/Features/ReserveSeat/ReserveSeatCommandHandler.cs @@ -0,0 +1,40 @@ +using System.Threading; +using System.Threading.Tasks; +using Ardalis.GuardClauses; +using Flight.Data; +using Flight.Seats.Dtos; +using Flight.Seats.Exceptions; +using MapsterMapper; +using MediatR; +using Microsoft.EntityFrameworkCore; + +namespace Flight.Seats.Features.ReserveSeat; + +public class ReserveSeatCommandHandler : IRequestHandler +{ + private readonly FlightDbContext _flightDbContext; + private readonly IMapper _mapper; + + public ReserveSeatCommandHandler(IMapper mapper, FlightDbContext flightDbContext) + { + _mapper = mapper; + _flightDbContext = flightDbContext; + } + + public async Task Handle(ReserveSeatCommand command, CancellationToken cancellationToken) + { + Guard.Against.Null(command, nameof(command)); + + var seat = await _flightDbContext.Seats.SingleOrDefaultAsync(x => x.SeatNumber == command.SeatNumber && x.FlightId == command.FlightId + && !x.IsDeleted, cancellationToken); + + if (seat is null) + throw new SeatNumberIncorrectException(); + + var reserveSeat = await seat.ReserveSeat(seat); + + var updatedSeat = _flightDbContext.Seats.Update(reserveSeat); + + return _mapper.Map(updatedSeat.Entity); + } +} diff --git a/src/Services/Flight/src/Flight/Seats/Features/ReserveSeat/ReserveSeatCommandValidator.cs b/src/Services/Flight/src/Flight/Seats/Features/ReserveSeat/ReserveSeatCommandValidator.cs new file mode 100644 index 0000000..daedbda --- /dev/null +++ b/src/Services/Flight/src/Flight/Seats/Features/ReserveSeat/ReserveSeatCommandValidator.cs @@ -0,0 +1,14 @@ +using FluentValidation; + +namespace Flight.Seats.Features.ReserveSeat; + +public class ReserveSeatCommandValidator : AbstractValidator +{ + public ReserveSeatCommandValidator() + { + CascadeMode = CascadeMode.Stop; + + RuleFor(x => x.FlightId).NotEmpty().WithMessage("FlightId must not be empty"); + RuleFor(x => x.SeatNumber).NotEmpty().WithMessage("SeatNumber must not be empty"); + } +} diff --git a/src/Services/Flight/src/Flight/Seats/Features/ReserveSeat/ReserveSeatEndpoint.cs b/src/Services/Flight/src/Flight/Seats/Features/ReserveSeat/ReserveSeatEndpoint.cs new file mode 100644 index 0000000..f95dd27 --- /dev/null +++ b/src/Services/Flight/src/Flight/Seats/Features/ReserveSeat/ReserveSeatEndpoint.cs @@ -0,0 +1,25 @@ +using System.Threading; +using System.Threading.Tasks; +using BuildingBlocks.Web; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Swashbuckle.AspNetCore.Annotations; + +namespace Flight.Seats.Features.ReserveSeat; + +[Route(BaseApiPath + "/flight/reserve-seat")] +public class ReserveSeatEndpoint : BaseController +{ + [Authorize] + [HttpPost] + [ProducesResponseType(StatusCodes.Status201Created)] + [ProducesResponseType(StatusCodes.Status400BadRequest)] + [SwaggerOperation(Summary = "Reserve seat", Description = "Reserve seat")] + public async Task ReserveSeat([FromBody] ReserveSeatCommand command, CancellationToken cancellationToken) + { + var result = await Mediator.Send(command, cancellationToken); + + return Ok(result); + } +} diff --git a/src/Services/Flight/src/Flight/Seats/Features/SeatMappings.cs b/src/Services/Flight/src/Flight/Seats/Features/SeatMappings.cs new file mode 100644 index 0000000..d7c5c7e --- /dev/null +++ b/src/Services/Flight/src/Flight/Seats/Features/SeatMappings.cs @@ -0,0 +1,14 @@ +using Flight.Seats.Dtos; +using Flight.Seats.Models; +using Mapster; + +namespace Flight.Seats.Features; + +public class SeatMappings : IRegister +{ + public void Register(TypeAdapterConfig config) + { + config.NewConfig(); + } +} + diff --git a/src/Services/Flight/src/Flight/Seats/Models/Seat.cs b/src/Services/Flight/src/Flight/Seats/Models/Seat.cs new file mode 100644 index 0000000..eb254c2 --- /dev/null +++ b/src/Services/Flight/src/Flight/Seats/Models/Seat.cs @@ -0,0 +1,34 @@ +using System; +using System.Threading.Tasks; +using BuildingBlocks.Domain.Model; + +namespace Flight.Seats.Models; + +public class Seat : Aggregate +{ + public static Seat Create(long id, string seatNumber, SeatType type, SeatClass @class, long flightId) + { + var seat = new Seat() + { + Id = id, + Class = @class, + Type = type, + SeatNumber = seatNumber, + FlightId = flightId + }; + + return seat; + } + + public Task ReserveSeat(Seat seat) + { + seat.IsDeleted = true; + seat.LastModified = DateTime.Now; + return Task.FromResult(this); + } + + public string SeatNumber { get; private set; } + public SeatType Type { get; private set; } + public SeatClass Class { get; private set; } + public long FlightId { get; private set; } +} diff --git a/src/Services/Flight/src/Flight/Seats/Models/SeatClass.cs b/src/Services/Flight/src/Flight/Seats/Models/SeatClass.cs new file mode 100644 index 0000000..1b689e1 --- /dev/null +++ b/src/Services/Flight/src/Flight/Seats/Models/SeatClass.cs @@ -0,0 +1,8 @@ +namespace Flight.Seats.Models; + +public enum SeatClass +{ + FirstClass, + Business, + Economy +} diff --git a/src/Services/Flight/src/Flight/Seats/Models/SeatType.cs b/src/Services/Flight/src/Flight/Seats/Models/SeatType.cs new file mode 100644 index 0000000..5a561c2 --- /dev/null +++ b/src/Services/Flight/src/Flight/Seats/Models/SeatType.cs @@ -0,0 +1,8 @@ +namespace Flight.Seats.Models; + +public enum SeatType +{ + Window, + Middle, + Aisle +} diff --git a/src/Services/Flight/tests/DeleteTests.cs b/src/Services/Flight/tests/DeleteTests.cs new file mode 100644 index 0000000..1dd0942 --- /dev/null +++ b/src/Services/Flight/tests/DeleteTests.cs @@ -0,0 +1,20 @@ +using System; +using System.Threading.Tasks; +using Xunit; + +namespace Integration.Test; + +[Collection(nameof(TestFixture))] +public class DeleteTests +{ + private readonly TestFixture _fixture; + + public DeleteTests(TestFixture fixture) => _fixture = fixture; + + [Fact] + public async Task Should_delete_flight() + { + var b = 2; + } + +} diff --git a/src/Services/Flight/tests/DockerTestUtilities/DockerDatabaseUtilities.cs b/src/Services/Flight/tests/DockerTestUtilities/DockerDatabaseUtilities.cs new file mode 100644 index 0000000..479ad43 --- /dev/null +++ b/src/Services/Flight/tests/DockerTestUtilities/DockerDatabaseUtilities.cs @@ -0,0 +1,70 @@ +using System.Linq; +using System.Runtime.InteropServices; +using System.Threading.Tasks; +using Ductus.FluentDocker.Builders; +using Ductus.FluentDocker.Services; +using Ductus.FluentDocker.Services.Extensions; + +namespace Integration.Test.DockerTestUtilities; + +public static class DockerDatabaseUtilities +{ + private const string ACCEPT_EULA = "Y"; + private const string SA_PASSWORD = "@Aa123456"; + private const string DB_CONTAINER_NAME = "sqldb"; + private static readonly ImageTag ImageTagForOs = new("mcr.microsoft.com/mssql/server", "2017-latest"); + + public static async Task EnsureDockerStartedAndGetPortPortAsync() + { + await DockerUtilities.CleanupRunningContainers(DB_CONTAINER_NAME); + await DockerUtilities.CleanupRunningVolumes(DB_CONTAINER_NAME); + + var hosts = new Hosts().Discover(); + var docker = hosts.FirstOrDefault(x => x.IsNative) ?? hosts.FirstOrDefault(x => x.Name == "default"); + + // create container, if one doesn't already exist + var existingContainer = docker?.GetContainers().FirstOrDefault(c => c.Name == DB_CONTAINER_NAME); + + if (existingContainer == null) + { + var container = new Builder().UseContainer() + .WithName(DB_CONTAINER_NAME) + .UseImage($"{ImageTagForOs.Image}:{ImageTagForOs.Tag}") + .ExposePort(1433, 1433) + .WithEnvironment( + $"SA_PASSWORD={SA_PASSWORD}", + $"ACCEPT_EULA={ACCEPT_EULA}") + .WaitForPort("1433/tcp", 30000 /*30s*/) + .Build(); + + container.Start(); + + await DockerUtilities.WaitUntilDatabaseAvailableAsync(GetSqlConnectionString()); + } + + return existingContainer.ToHostExposedEndpoint("1433/tcp").Port; + } + + // SQL Server 2019 does not work on macOS + M1 chip. So we use SQL Edge as a workaround until SQL Server 2022 is GA. + // See https://github.com/pdevito3/craftsman/issues/53 for details. + private static ImageTag GetImageTagForOs() + { + var sqlServerImageTag = new ImageTag("mcr.microsoft.com/mssql/server", "2019-latest"); + var sqlEdgeImageTag = new ImageTag("mcr.microsoft.com/azure-sql-edge", "latest"); + return IsRunningOnMacOsArm64() ? sqlEdgeImageTag : sqlServerImageTag; + } + + private static bool IsRunningOnMacOsArm64() + { + var isMacOs = RuntimeInformation.IsOSPlatform(OSPlatform.OSX); + var cpuArch = RuntimeInformation.ProcessArchitecture; + return isMacOs && cpuArch == Architecture.Arm64; + } + + public static string GetSqlConnectionString() + { + return DockerUtilities.GetSqlConnectionString(); + } + + private record ImageTag(string Image, string Tag); +} diff --git a/src/Services/Flight/tests/DockerTestUtilities/DockerUtilities.cs b/src/Services/Flight/tests/DockerTestUtilities/DockerUtilities.cs new file mode 100644 index 0000000..fa42694 --- /dev/null +++ b/src/Services/Flight/tests/DockerTestUtilities/DockerUtilities.cs @@ -0,0 +1,135 @@ +// based on https://blog.dangl.me/archive/running-sql-server-integration-tests-in-net-core-projects-via-docker/ + +using System; +using System.Linq; +using System.Net; +using System.Net.Sockets; +using System.Threading.Tasks; +using Docker.DotNet; +using Docker.DotNet.Models; +using Microsoft.Data.SqlClient; + +namespace Integration.Test.DockerTestUtilities; + +public static class DockerUtilities +{ + private static bool IsRunningOnWindows() + { + return Environment.OSVersion.Platform == PlatformID.Win32NT; + } + + public static DockerClient GetDockerClient() + { + var dockerUri = IsRunningOnWindows() + ? "npipe://./pipe/docker_engine" + : "unix:///var/run/docker.sock"; + return new DockerClientConfiguration(new Uri(dockerUri)) + .CreateClient(); + } + + public static async Task CleanupRunningContainers(string containerName, int hoursTillExpiration = -24) + { + var dockerClient = GetDockerClient(); + + var runningContainers = await dockerClient.Containers + .ListContainersAsync(new ContainersListParameters()); + + foreach (var runningContainer in + runningContainers.Where(cont => cont.Names.Any(n => n.Contains(containerName)))) + { + // Stopping all test containers that are older than 24 hours + var expiration = hoursTillExpiration > 0 + ? hoursTillExpiration * -1 + : hoursTillExpiration; + if (runningContainer.Created < DateTime.UtcNow.AddHours(expiration)) + try + { + await EnsureDockerContainersStoppedAndRemovedAsync(runningContainer.ID); + } + catch + { + // Ignoring failures to stop running containers + } + } + } + + public static async Task CleanupRunningVolumes(string volumeName, int hoursTillExpiration = -24) + { + var dockerClient = GetDockerClient(); + + var runningVolumes = await dockerClient.Volumes.ListAsync(); + + foreach (var runningVolume in runningVolumes.Volumes.Where(v => v.Name == volumeName)) + { + // Stopping all test volumes that are older than 24 hours + var expiration = hoursTillExpiration > 0 + ? hoursTillExpiration * -1 + : hoursTillExpiration; + if (DateTime.Parse(runningVolume.CreatedAt) < DateTime.UtcNow.AddHours(expiration)) + try + { + await EnsureDockerVolumesRemovedAsync(runningVolume.Name); + } + catch + { + // Ignoring failures to stop running containers + } + } + } + + public static async Task EnsureDockerContainersStoppedAndRemovedAsync(string dockerContainerId) + { + var dockerClient = GetDockerClient(); + await dockerClient.Containers + .StopContainerAsync(dockerContainerId, new ContainerStopParameters()); + await dockerClient.Containers + .RemoveContainerAsync(dockerContainerId, new ContainerRemoveParameters()); + } + + public static async Task EnsureDockerVolumesRemovedAsync(string volumeName) + { + var dockerClient = GetDockerClient(); + await dockerClient.Volumes.RemoveAsync(volumeName); + } + + public static async Task WaitUntilDatabaseAvailableAsync(string connectionString) + { + var start = DateTime.UtcNow; + const int maxWaitTimeSeconds = 60; + var connectionEstablished = false; + while (!connectionEstablished && start.AddSeconds(maxWaitTimeSeconds) > DateTime.UtcNow) + try + { + using var sqlConnection = new SqlConnection(connectionString); + await sqlConnection.OpenAsync(); + connectionEstablished = true; + } + catch + { + // If opening the SQL connection fails, SQL Server is not ready yet + await Task.Delay(500); + } + + if (!connectionEstablished) + throw new Exception( + $"Connection to the SQL docker database could not be established within {maxWaitTimeSeconds} seconds."); + } + + public static int GetFreePort() + { + // From https://stackoverflow.com/a/150974/4190785 + var tcpListener = new TcpListener(IPAddress.Loopback, 0); + tcpListener.Start(); + var port = ((IPEndPoint) tcpListener.LocalEndpoint).Port; + tcpListener.Stop(); + return port; + } + + public static string GetSqlConnectionString() + { + return new SqlConnectionStringBuilder() + { + ConnectionString = "Server=db;Database=FlightDB;User ID=sa;Password=@Aa123456" + }.ToString(); + } +} diff --git a/src/Services/Flight/tests/Integration.Test.csproj b/src/Services/Flight/tests/Integration.Test.csproj new file mode 100644 index 0000000..ab5e0e7 --- /dev/null +++ b/src/Services/Flight/tests/Integration.Test.csproj @@ -0,0 +1,36 @@ + + + + net6.0 + + false + + + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + + + + + + + + + + + diff --git a/src/Services/Flight/tests/TestFixture.cs b/src/Services/Flight/tests/TestFixture.cs new file mode 100644 index 0000000..0d0735b --- /dev/null +++ b/src/Services/Flight/tests/TestFixture.cs @@ -0,0 +1,120 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Security.Claims; +using System.Threading.Tasks; +using Flight.Data; +using MediatR; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc.Testing; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.VisualStudio.TestPlatform.TestHost; +using Moq; +using Respawn; +using Xunit; + +namespace Integration.Test; + +[CollectionDefinition(nameof(TestFixture))] +public class SliceFixtureCollection : ICollectionFixture +{ +} + +public class TestFixture : IAsyncLifetime +{ + private readonly Checkpoint _checkpoint; + private readonly IConfiguration _configuration; + private readonly WebApplicationFactory _factory; + private static IServiceScopeFactory _scopeFactory = null!; + + public TestFixture() + { + _factory = new FlightTestApplicationFactory(); + + _configuration = _factory.Services.GetRequiredService(); + _scopeFactory = _factory.Services.GetRequiredService(); + + _checkpoint = new Checkpoint(); + } + + class FlightTestApplicationFactory : WebApplicationFactory + { + protected override void ConfigureWebHost(IWebHostBuilder builder) + { + // builder.ConfigureAppConfiguration((_, configBuilder) => + // { + // configBuilder.AddInMemoryCollection(new Dictionary + // { + // {"ConnectionStrings:DefaultConnection", _connectionString} + // }); + // }); + + builder.ConfigureServices(services => + { + services.AddLogging(); + var httpContextAccessorService = services.FirstOrDefault(d => + d.ServiceType == typeof(IHttpContextAccessor)); + services.Remove(httpContextAccessorService); + services.AddSingleton(_ => Mock.Of()); + + services.AddSingleton(Mock.Of(w => + w.EnvironmentName == "Flight.IntegrationTest" && + w.ApplicationName == "Flight")); + + // services.AddMassTransitTestHarness(); + // + // // MassTransit Harness Setup -- Do Not Delete Comment + // services.AddMassTransitInMemoryTestHarness(cfg => + // { + // // Consumer Registration -- Do Not Delete Comment + // // cfg.AddConsumer(); + // // cfg.AddConsumerTestHarness(); + // }); + + EnsureDatabase(); + }); + } + + } + + + private static void EnsureDatabase() + { + using var scope = _scopeFactory.CreateScope(); + + var context = scope.ServiceProvider.GetService(); + + context?.Database.Migrate(); + } + + public static TScopedService GetService() + { + var scope = _scopeFactory.CreateScope(); + var service = scope.ServiceProvider.GetService(); + return service; + } + + + public static Task SendAsync(IRequest request) + { + using var scope = _scopeFactory.CreateScope(); + + var mediator = scope.ServiceProvider.GetService(); + + return mediator.Send(request); + } + + + public Task InitializeAsync() + => _checkpoint.Reset(_configuration.GetConnectionString("FlightConnection")); + + public Task DisposeAsync() + { + _factory?.Dispose(); + return Task.CompletedTask; + } +} diff --git a/src/Services/Identity/.dockerignore b/src/Services/Identity/.dockerignore new file mode 100644 index 0000000..820e869 --- /dev/null +++ b/src/Services/Identity/.dockerignore @@ -0,0 +1,25 @@ +**/.classpath +**/.dockerignore +**/.env +**/.git +**/.gitignore +**/.project +**/.settings +**/.toolstarget +**/.vs +**/.vscode +**/*.*proj.user +**/*.dbmdl +**/*.jfm +**/azds.yaml +**/bin/ +**/charts +**/docker-compose* +**/Dockerfile* +**/node_modules +**/npm-debug.log +**/obj/ +**/secrets.dev.yaml +**/values.dev.yaml +LICENSE +README.md \ No newline at end of file diff --git a/src/Services/Identity/Dockerfile b/src/Services/Identity/Dockerfile new file mode 100644 index 0000000..4a88c14 --- /dev/null +++ b/src/Services/Identity/Dockerfile @@ -0,0 +1,41 @@ +FROM mcr.microsoft.com/dotnet/sdk:6.0 AS builder +WORKDIR /app + +# Setup working directory for the project +WORKDIR /app +COPY ./src/BuildingBlocks/BuildingBlocks.csproj ./BuildingBlocks/ +COPY ./src/Services/Identity/src/Identity/Identity.csproj ./Services/Identity/src/Identity/ +COPY ./src/Services/Identity/src/Identity.Api/Identity.Api.csproj ./Services/Identity/src/Identity.Api/ + + +# Restore nuget packages +RUN dotnet restore ./Services/Identity/src/Identity.Api/Identity.Api.csproj + +# Copy project files +COPY ./src/BuildingBlocks ./BuildingBlocks/ +COPY ./src/Services/Identity/src/Identity/ ./Services/Identity/src/Identity/ +COPY ./src/Services/Identity/src/Identity.Api/ ./Services/Identity/src/Identity.Api/ + +# Build project with Release configuration +# and no restore, as we did it already + +RUN ls +RUN dotnet build -c Release --no-restore ./Services/Identity/src/Identity.Api/Identity.Api.csproj + +WORKDIR /app/Services/Identity/src/Identity.Api + +# 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 /app/Services/Identity/src/Identity.Api/out . + +ENV ASPNETCORE_URLS https://*:5005, http://*:6005 +ENV ASPNETCORE_ENVIRONMENT docker + +ENTRYPOINT ["dotnet", "Identity.Api.dll"] + diff --git a/src/Services/Identity/src/Identity.Api/Identity.Api.csproj b/src/Services/Identity/src/Identity.Api/Identity.Api.csproj new file mode 100644 index 0000000..2715513 --- /dev/null +++ b/src/Services/Identity/src/Identity.Api/Identity.Api.csproj @@ -0,0 +1,17 @@ + + + + net6.0 + enable + enable + + + + + + + + <_ContentIncludedByDefault Remove="keys\is-signing-key-C01EF7C986B61650360AFA59291E65BC.json" /> + + + diff --git a/src/Services/Identity/src/Identity.Api/Program.cs b/src/Services/Identity/src/Identity.Api/Program.cs new file mode 100644 index 0000000..5c76b8c --- /dev/null +++ b/src/Services/Identity/src/Identity.Api/Program.cs @@ -0,0 +1,81 @@ +using BuildingBlocks.Domain; +using BuildingBlocks.EFCore; +using BuildingBlocks.Logging; +using BuildingBlocks.Mapster; +using BuildingBlocks.MassTransit; +using BuildingBlocks.OpenTelemetry; +using BuildingBlocks.Swagger; +using BuildingBlocks.Utils; +using BuildingBlocks.Web; +using Figgle; +using FluentValidation; +using Hellang.Middleware.ProblemDetails; +using Identity; +using Identity.Data; +using Identity.Extensions; +using Microsoft.AspNetCore.Mvc.ApiExplorer; +using Microsoft.EntityFrameworkCore; +using Prometheus; +using Serilog; + +var builder = WebApplication.CreateBuilder(args); +var configuration = builder.Configuration; +var env = builder.Environment; + +var appOptions = builder.Services.GetOptions("AppOptions"); +Console.WriteLine(FiggleFonts.Standard.Render(appOptions.Name)); + +builder.Services.AddScoped(provider => provider.GetService()!); + +builder.Services.AddDbContext(options => + options.UseSqlServer( + configuration.GetConnectionString("DefaultConnection"), + x => x.MigrationsAssembly(typeof(IdentityRoot).Assembly.GetName().Name))); + +builder.AddCustomSerilog(); +builder.Services.AddControllers(); +builder.Services.AddCustomSwagger(builder.Configuration, typeof(IdentityRoot).Assembly); +builder.Services.AddCustomVersioning(); +builder.Services.AddCustomMediatR(); +builder.Services.AddValidatorsFromAssembly(typeof(IdentityRoot).Assembly); +builder.Services.AddCustomProblemDetails(); +builder.Services.AddCustomMapster(typeof(IdentityRoot).Assembly); +builder.Services.AddScoped(); + +builder.Services.AddTransient(); +builder.Services.AddTransient(); + +builder.Services.AddCustomMassTransit(typeof(IdentityRoot).Assembly); +builder.Services.AddCustomOpenTelemetry(); + +builder.Services.AddIdentityServer(env); + +var app = builder.Build(); + +if (app.Environment.IsDevelopment()) +{ + var provider = app.Services.GetService(); + app.UseCustomSwagger(provider); +} + +app.UseSerilogRequestLogging(); +app.UseMigrations(); +app.UseCorrelationId(); +app.UseRouting(); +app.UseHttpMetrics(); +app.UseMigrations(); +app.UseProblemDetails(); +app.UseHttpsRedirection(); +app.UseAuthentication(); +app.UseAuthorization(); +app.UseIdentityServer(); + +app.UseEndpoints(endpoints => +{ + endpoints.MapControllers(); + endpoints.MapMetrics(); +}); + +app.MapGet("/", x => x.Response.WriteAsync(appOptions.Name)); + +app.Run(); diff --git a/src/Services/Identity/src/Identity.Api/Properties/launchSettings.json b/src/Services/Identity/src/Identity.Api/Properties/launchSettings.json new file mode 100644 index 0000000..c321a06 --- /dev/null +++ b/src/Services/Identity/src/Identity.Api/Properties/launchSettings.json @@ -0,0 +1,31 @@ +{ + "$schema": "https://json.schemastore.org/launchsettings.json", + "iisSettings": { + "windowsAuthentication": false, + "anonymousAuthentication": true, + "iisExpress": { + "applicationUrl": "http://localhost:42478", + "sslPort": 44342 + } + }, + "profiles": { + "Identity.Api": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "launchUrl": "swagger", + "applicationUrl": "https://localhost:5005;http://localhost:6005", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "IIS Express": { + "commandName": "IISExpress", + "launchBrowser": false, + "launchUrl": "swagger", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} diff --git a/src/Services/Identity/src/Identity.Api/appsettings.Development.json b/src/Services/Identity/src/Identity.Api/appsettings.Development.json new file mode 100644 index 0000000..2c63c08 --- /dev/null +++ b/src/Services/Identity/src/Identity.Api/appsettings.Development.json @@ -0,0 +1,2 @@ +{ +} diff --git a/src/Services/Identity/src/Identity.Api/appsettings.docker.json b/src/Services/Identity/src/Identity.Api/appsettings.docker.json new file mode 100644 index 0000000..e431598 --- /dev/null +++ b/src/Services/Identity/src/Identity.Api/appsettings.docker.json @@ -0,0 +1,13 @@ +{ + "App": "Identity-Service", + "ConnectionStrings": { + "IdentityConnection": "Server=db;Database=IdentityDB;User ID=sa;Password=@Aa123456" + }, + "RabbitMq": { + "HostName": "rabbitmq", + "ExchangeName": "identity", + "UserName": "guest", + "Password": "guest" + }, + "AllowedHosts": "*" +} diff --git a/src/Services/Identity/src/Identity.Api/appsettings.json b/src/Services/Identity/src/Identity.Api/appsettings.json new file mode 100644 index 0000000..6571c39 --- /dev/null +++ b/src/Services/Identity/src/Identity.Api/appsettings.json @@ -0,0 +1,24 @@ +{ + "AppOptions": { + "Name": "Identity-Service" + }, + "ConnectionStrings": { + "DefaultConnection": "Server=.\\sqlexpress;Database=IdentityDB;Trusted_Connection=True;MultipleActiveResultSets=true" + }, + "RabbitMq": { + "HostName": "localhost", + "ExchangeName": "identity", + "UserName": "guest", + "Password": "guest" + }, + "Serilog": { + "MinimumLevel": { + "Default": "Information", + "Override": { + "Microsoft": "Warning", + "Microsoft.Hosting.Lifetime": "Information" + } + } + }, + "AllowedHosts": "*" +} diff --git a/src/Services/Identity/src/Identity.Api/keys/is-signing-key-31079AE9DF4ED1F2D492E52BA5A644F5.json b/src/Services/Identity/src/Identity.Api/keys/is-signing-key-31079AE9DF4ED1F2D492E52BA5A644F5.json new file mode 100644 index 0000000..2349aa7 --- /dev/null +++ b/src/Services/Identity/src/Identity.Api/keys/is-signing-key-31079AE9DF4ED1F2D492E52BA5A644F5.json @@ -0,0 +1 @@ +{"Version":1,"Id":"31079AE9DF4ED1F2D492E52BA5A644F5","Created":"2022-04-18T20:26:55.3256244Z","Algorithm":"RS256","IsX509Certificate":false,"Data":"CfDJ8P3RLEnEd5JHjUIOTYyyJAzVTUgXKNFkjwfEWELe3r9iozJI4zF_jAjqORPS_C37DAQiHHtMYbgB7_j1Wwqck7qb_wv8CuDfNW8Hx0_UdWaO6vD0Sf_hIbBmN4RrwFwdMia8IUcNCUpwL9XJl95TLzD9FjpuyuWElQ8_5ZnF3SuErstPenQa4q9MYsj9XmPhNSk-BQan-cimxIAPYm1BgZNm5DMylT98sus9ScYTgOowV2n13MW0uA7kk9JOZIIjG07UuaSnuEQei03AU9zkejYD8yLwBi98ON5s0_jW2V3ebY-01VBQXhztu5Iev_SCynNyqQW-uqEzL9SSlzX8dEa79KKmOJEo2o325AVeDOBvco2XDCCra5RkRhUIPV3ZHvQr4EZJNqvi1zelWs3-FJ9vXE02-6XZkZKd6TzhPfiMDc1Z88-eVlx-uwhPjvJyfUSU9cAgbNDREDETxUcmIAMqqbXVAUCnaPkpbxy0VZ6YGSNsD9w77L6nIKiz-0KkVtxDZKCXh5BbDwAiWza8KWKclnVUnMLGhnjANEgKzzvmb1UJr1wZ0xRZ9KXyxr9oy0kP1Be-ysFSUd-YUTSgDvuQZV_DPjH2XP99tXqmfWJ8PJN9hswaRGhp5Jxo003l93u7Ivy-XR1BEhxVoYpvV9fCh8lo-N9ayHYpYlEsdArk1qvkuGRSlkBWNeEr_ZsvODbwM8Jp5q7pLK7cUy0bRHnYwkyHReS_fh6_tTdnfcQZPWb7fLN967XwebcVNOEENn5FX9oszCiy5BG9HQDIPffIx8bkKrZCMV77e5Qria9wNwkhIi3-7SOjRD-v5azkwiV0hTToAQTYsAT8DRTJklI90PF4JbsiMvObeoJtbwPy3E-K1_4DOxAjNwQFHf_zqWEbo9vmxGF-IerSwaJNSVfUUxnnYT8JWFYHYzWGyMvqtdymk1pmBaOJ64dgTVVcao7S0PwfV4DNUzxKQX-jNzqMihaXu4y8lyGIPJycJezm7nfGJiHhhhV4EWSQ1Y2ypBGv_s1iZ5pvTCeGkthY2Q4pe-GYKrFGTnBSLWwHqsVA2gwb1McjYjUGNgu_mnzyB45ZpKrqXQDDiozbQ4Ju7DJb5Vi_EoPmeHQDj5h5sFwAWtq0NGI_lRAog2K_QnZvSV3SFDVlaKDq9PjgIORc6wi5bzbAusWbjDd_sb211QsrNfW-0SWWgYwz9qRg14OiuntGESXSq9Cmx8WgPaK5H0FQGZUlzq3Bk8ov6GUhPeZSKP4gUyfRloSyj2DBZSe_0JOXO3e4Q6BFZLqhpStyAxoUZftEW_J3pKNjCrvKCOm-Taz0EVBEH9zqLTJwwXEoR86YYNTrtpARTOHo2qco_oMSX_2O4cpAfDZz0ZMHuqxunNnannVdFv5SdAHXngHLguLwAVuUTGlrg_xnUeW4Msi9-CKXpmmEQv6BCIW8Q4S5rn0vp394QgMCCo4473jOEeOX1G4_CUOhJZydGOCEbC5cMrM_54SKBu7hz8P9MCkDLj1ZsPwPDB1YyDwLFJlxb9oEZoRC4whvTJAfX2xNP4wv73ojxwrOII2ivNrnqNX6EM41bxmqMrF9tltSut-OR2XHO-DdJduoRnJjmKJl_WaZQypbZqfVepURuDNGChqyby--xsGMA-yQBBrVigXSKliuYG7dxWBp3TJFrvD4Aym1gAQOgjlObIHknv1cA83g_rHx6vq7JfwqMFYu6H6IZ6eeTUde1IixstE7EzU_4u5LzkQb8HKSMt3j9m3dlH4OigcPvcSJy8aJoN-UCFInfnE7IX0ul_S5HAgvgLM_BEVa3KTa0u8nxAY7xaIXrwWJZ23fhiZrOkNYJI1XAiN11sjtVgVS8viZCkTabUtpEtX2nPe767IbwVzPdOG-C6W_-8uNN_j1WjYeQfsSjmAjab2jZqx3BCDOjTbHkP_vKSzdABFA6bu2Fqt3io2UVTYq-a_Jf2b4JGmuiHomXjhsH3KZp_MDw1nZ8zf1mejyuH_NHFScVzb-vO_ieZ-RDQZn32VPaaDlylSMGa4kE9-54tHZUkFR4Jy1vpyKjXBa3uIEQBE5RFn7XCKbVA4Ea9BCou5ku7E6Zod0SfjMmmknB6jzLTVSBgMP2AkFb98m943O1XXsDQstMmTZdqmNJTOirLoL2Vu1AXqsdS7O-DrVcDImR7ekSGPhUSg2LQkObRPT26sSeiygKlNDi3PktIA1duuJ066NoIHCJupl2mugbFDrZ90SBaQ9n72Ya7fjKdrSdp3izCxqlldeZPANZMMZ5KwjX9WZnl-lPNsn3iCNaEhBCqSWne7YN6xxRS6gKoMobVav-HsZ841qr5iaYzDFD2kA4R35A-Dm5zJyfSCV4NPdUEGzIIbjg1-kYv8PFR_kImuXIwGXT86BxpgEY6PRaD8-ZFZMzerGvEhS8R39NpQ-hiZ_P_ZY3wamWHlwttaTvX3fqpflHlhKV_9AI9HZ","DataProtected":true} \ No newline at end of file diff --git a/src/Services/Identity/src/Identity.Api/keys/is-signing-key-74ABBD16A9EF6567607D1F748BB91597.json b/src/Services/Identity/src/Identity.Api/keys/is-signing-key-74ABBD16A9EF6567607D1F748BB91597.json new file mode 100644 index 0000000..d66299e --- /dev/null +++ b/src/Services/Identity/src/Identity.Api/keys/is-signing-key-74ABBD16A9EF6567607D1F748BB91597.json @@ -0,0 +1 @@ +{"Version":1,"Id":"74ABBD16A9EF6567607D1F748BB91597","Created":"2022-03-29T20:44:37.9948898Z","Algorithm":"RS256","IsX509Certificate":false,"Data":"CfDJ8P3RLEnEd5JHjUIOTYyyJAzeYir0LDVxp1O__t0rzDUFxCBt5XtK6FK6fLgopH3OcjrMTbORu_DJp3lf4bf015LKnOk0yjzfzZ5xRX3qwHwz1jvdXKqzt-Pf85kiRlrNv6qiTH7UXP5xGTY3Tn7TdkVJfPmvfHtQyw1FhIPJnx5KQ469H2DRnoxmpwpspHrGlrPA5cJn3Uc7xFnTNEe0laORnilL1rxTTsKFFhJCWCcoxMozgh_SQqEhy9B7Vb4rQHn-ULb8qO4IaKDvbuCJNuGD2nJxy5lBNkdFKJD724IlGIB1nV3w5LR3YVYcxxIc1bLrbK3byHbVHsl8i9uacpN4-JP2Z16Sku9jhsQ21d1tNssE7gmxe8u6qE4RQjL2URSci8_XdSsRa0DBYNaVOSLv60M4dtwg6pJeWB6GfvJ8_JFnTDFDs51rXc77RL4tANblHLzuQl2C3gMMbJPoviWSs8R0hg2mCp0-pQWwSzNvNmtWwsMNXPgHtfIrJTGDKTQEiYIkvkf34gsBaweyea3Q5H2prmt10qVHr47LpkjGqxpHvi9eboJQkG2y-1MBptNpdiYZ4gbViplu8Ru5M4YsCmb0zZF2Q1DZsKVM-T_PFZCgyy1LjLwSHczvcflQh__6YmSwhehI7Nka86m3lK80nwAr9u0qoUyqjavte-XjNiRbUd7MHaEy2bT_jj40oqdh6sGURIkmmJljxPQfu7Scqgcs6chQSY4JXEnfLBa0AK40mefPwpk76QOJBQRBJ3PvnSLc5RMhgs2c2Vc54j4VuQU0tjiHoDPcgerj5oRIQCDiA2_ylleNmxBEomENO0FRortejvV7FNb2qzTfey1r6bTwJvN81EFfMhGUCsj7X0KkBywJMESsQlU3pbs_JChQfX-6rMjofkMYEtnupYnCutUFVO-xZoXrJvw5AOxtJn4JoPHBYeEhC29zjHvtzfSnnnDzCQxiJkWO6pyuAFVIyJUK50Bx_yuaiDicscEjShfd-fQEN_l3eFsUzTGa5rX9gfsPckH8jkpkFkXJlUKFt-nt_9FDnqi5WM6Hf_d891jocGkgiSLM2YURz5Fkyq8S6o-hrBnVf4Pz4dtfsQBw-NnOAl4FPnaknsVdDLZ-qe9gaZdN3wbHQMwFq2eTmm36M6ky47A2xO8MksDSlh4ROcrqJZOBaCWE6fFowcot1sJXFAqNw5iuEKNmtFtJU6oPWxTGY5C0BdjExksQZzqmg2JkjbqZE0zRBFBs0oSJHZsQTnBZmTELIUzMjZVO5eXfVp0tBoOWoW0KgTdHEhlHhNXkvZDoNIdQKu7RVEmhF3AmWWUYzpAW1Y_oEPAsF1Jf-msAlCiMnchbg3yxDG7n6VKAPssQQs3RGPjPLCjOKtHtK4X_3ivVvHbX0xxZhUk44DBv8HDd72rQLEenrXKuIEr_L6VzZ93BpisnxMWxJV6pRcYe1LTtu4nQGZ20hiJs98zHRHomwWJFqxIlPZrSrqTrxUAN4jCl4WSLgf-c2zRfO3AHqvlVCt518s92IUx75372znyoM7-xB5h95T2yxg9x9dlY-_-wICdlyPiQ8arAQ824Cl_2QkSU_ba30gAvWlXGYRVFpg3PSmP0mIhq-ffA8HLB0ARZfYl1_vMspd9kbPyJVCRSaFObC_L-AwvKV8f7Oqqd6bwbUYvdA_6cw1gFUeiqH2wuggOjFI3WDp-5ttVdmZnDVDK--LR3y5b2cYHAf3EG2N-7qMBwZqpOb3ivnRFX_SUlB9V9BlvSe_sLU5zJphBoSWm2-_4j9rWQjCaswlBLbScRA3r1KC5BFxFgbFFv7p4hWk_xjdAx5q4RHM6wqFfGj1WkrjRtdnMC7z1ZM7Bdsno-5yt6PQBP-y4QIvpSpefXl8E4_gBJ6dzToQwLS_Tpk7CjzlIV3oUZ5XfMV0s14Flky6ErsvBdXMzNbHwvxwCAoxbYDrjZ9U-w9gibGOOVBYmKjhkeGLGs8ejqc8TxrzEEFL1li1ZfXxeNAcu4zyNwTHsWaaLVciWaw-UuPqA4uDFxMNmxI6kfXa49L0voFkQaeMo48J2lVjH00QV8f-nXnVaZFztiTU7fYYlpK-RxRSL1q0qQOFMEKOdltqqKxKbM7grNSoBQz-ZHqQq7ZR4v7NRl-yyTGmcGvOzTaDfLP9snbDfiU2QnqH-S2pgtobbYJ_UZvGfg8AZurvNBbl-KuBOrN3yarxe5STJY9mHY_8y2c8C_foQifRSNr_oDANvMbfsukrV8HJimtMk9WF15Qaz51BCiZ9NIxiCsgaJHZ48Oi8EUput8rtTInjNjAvb33N4whgDKQVLWXE59k13dlZX7xA1V-pFbY9TvFLFsWwWS6Jrj8okejPb5Fsb9zraapVTTCIEr3VYR9BB_V7tgkldqpNatO6_qqczD4owmZ-dL6IDyE4f2u_2FCgUV1FP30UMdkRmKHAbhZ9kGCK-ZI5XS33qh","DataProtected":true} \ No newline at end of file diff --git a/src/Services/Identity/src/Identity.Api/keys/is-signing-key-9258A527F8A78F2F47D30E7F145FD59F.json b/src/Services/Identity/src/Identity.Api/keys/is-signing-key-9258A527F8A78F2F47D30E7F145FD59F.json new file mode 100644 index 0000000..e18a41b --- /dev/null +++ b/src/Services/Identity/src/Identity.Api/keys/is-signing-key-9258A527F8A78F2F47D30E7F145FD59F.json @@ -0,0 +1 @@ +{"Version":1,"Id":"9258A527F8A78F2F47D30E7F145FD59F","Created":"2022-02-15T09:58:48.5269984Z","Algorithm":"RS256","IsX509Certificate":false,"Data":"CfDJ8HYLA_44KBlEtnKYw5Y6NgZcbR_xFrM2Jsvq7lBG7H9pe38jngLl4EUknvBtq5hxuuwWtTX5qnwK_90h5-sbgF98Tio47qnnv-wA50LawMrDvcEbWMjdBXeFD4hE4QCSCi43i3oRms09g1c6gHipjTOTGmZ0Tj6wGAJZHkdJz6uBuvnWkuypYjaZG1vap_baLugSmjP71PUzn7jgFR1dQDEOcZE91-dERlCPeU7RXdDOTrUJMjD4yvfXO3VdB53YiZErMIgCv7rx1qZhzBPmyg2Z3XK86kY877HD4I-DOerJKeqPYl9vfVmFQgRRLFdOhi5ZpmUwJaRKG0zjTDDfOuvFG16WNPCrr3-_AIU3ANaCvL4Lg44t2lamlRaKlEvT5aKxphO2cVZjSWsTPJMHgHXWYM01e09i3WDbL07g35vJWsnRrrhxhiMoVJJG68-WrZVtev47fT69of6Z5_lhFcc7donENuxlN3K2V8P90Nkxu4eYbHwp267IvadONcgCMO_wHdKwZZswFUs5HYVyYVOZKtmE2T4I7JJWrOeL2iM_VWSFwkwV0hCLQyKazQZwVIe8s2s3glYY8ektc36reTZoSg4ykhanq26OLX4VKw9UzvfyV_J-EozNh2WXlhtgGqoeomjVrNp6U612ywre3QLN-lvSDjd2MoQsZ-CG4S6kyTuXNT0KxsdXs-hXUlXRNbri8fshIjSkYjCVWUKvNFvn7IMJ5AxYakWekKdPL7r75Q7HNJMMYxt8jh7fv7N8QKWuOnWWy_kA8N6qXIsHsetBeBfGBHbq92mQkfcdArVKuysqTzSy2wm9t4mLNFbzO1QPAQ9VS6aYS3DcWh2NF_pc2xlRCRt9Mb1P9IYEUQk7P2iakeYUhKH1MMNh0GuMwkCIaed4XCTEVZfSB70YtPUCawgNFP9hn5ENUVNRWBf6717gT1ioybimXghodrCOHiYxxQDDpss3e8MIl_x-BaqW8C2zYFd6xE0pVFXV5-X7O6XP2sc3ClOd1EyVnLRT1-87Uof-1KYA2kzTyPfWrnC72maTbOJzeje2f6oT98BfsgkvakOAchAkEl_FJz_Dv_KdJ9ZwC9Xv0Wan4dvSS5aR35iifXptuZ0du0pc2t6_CoHGLZxOG6SvXm7AN24XYFcWnRPvGUS2M5NG4pHJn73vOPaon1SAIBUrLiDXSWeW_27G_5UG5P1lPXYecFMjVG2vdKPEDxygu3yj1e5CDyeVmvcnI3-Hl_4skomLB14as8U950O7fr-CuS_BAli6m0mplgAPCOrfeyq_o-xWdM9vNoaEHRGA5e2ZfEIknCinSY7CuoGCnMmR0O8eT2P3aQ34TKHRRspvCVs8GAyzCZaVT8_ZeOHCl4FU8Ac5NIe3OMRPuZs1uJaBfBIVlPP9MfhKgEA_S5t8vfa3RJhiU0jMy5h9OToqnBOQsiIkY0mjSMsn3VvsqeNutK5DKoTpATX822sSHCSYXAewk7F-agvqKnM_cPVhib9Z1r0UXLS2neLZwF5ob7uI1nYU8TNb6cawcWPGBz9FoMjtxXsMsAci5luEVg1ZfGTU9kShAa3sdfRYC5E4L37_Fy_4EZyNEKEMPxx8I20PCPfqEPR_JB0geDv0Up9ZfD2ZwgwovYDVbGv3qIbgbkEH2Z7zf2e4gH9sZClvei1XigDL9gjfJ0b9IdA3QpWw6b3uleR-WbNWag5S2Phz6kPB2irBgoN7oG_gUegXQTzCif0mgeQSwbea1vXOngXBLG1PiyRk_iPKPhZMmg1UoMeqXQlXqDL4VFRnMOsYdOKr-BWsgdCM9zE3CCyVueBX0zTuZU0kM0P8hUH9RsZYdqPOXS0d4Fy0YUmQZTHWwSMbCUG1XmJ4QN3y1FKUKBvRRHZmZboYHz5EKFMHhAbk9ZHZRCpLm4hIlH81IiSgRlYm3CacJqjlAiLp0Tc0zF6oFlZFipA-nM5znhgDJMSV-CSW1FSDvYFvHolOxR4U0hlw_XG6NeytrkiAR7-wvU3ufkslsWowtcyswCNHcT7eg-tpptSXyT0RJaQhWwAc9L9lYWZnCBwknUCak23u1V7jM8ZldOM5jiQrjRvEBv-sqMto4Qo6129AatIKri04PYD9VlHzLP76KY4F-UHUcu-9g3cJp9mnbnzhmD1LvNPQmH2giY0pc9hFWW6_Ablz7GmW2cngb9xwHAgi-WBSrZYoW6l2dAdkm72ECwWmMguwT6OZdaZZzjBYixHKM-mnb4uzVT6CU-pp4-G9dYtX5EPqhKcmsmwbV6qiziV61rGYypP37spT8H_SWTFmqD-sMPg6jJj1PncSFd2mlZFBkuFJDvF6Aorf35t9-rePPRc18k14Poa8J8j_mYVJo3lEIrtRDLF3d8EvZ9UEjmgBj25gSiYhOJBPriRop1qmlVVSUjoTBIJaXlTXefbcZTqc8vguzQ5Oy8rjzTaxeP1-OgsCDO0xw7ibWEeN","DataProtected":true} \ No newline at end of file diff --git a/src/Services/Identity/src/Identity.Api/tempkey.jwk b/src/Services/Identity/src/Identity.Api/tempkey.jwk new file mode 100644 index 0000000..eea2464 --- /dev/null +++ b/src/Services/Identity/src/Identity.Api/tempkey.jwk @@ -0,0 +1 @@ +{"AdditionalData":{},"Alg":"RS256","Crv":null,"D":"af7TFMQS67FxuYbUvAAiXHqqGF7HYby1Vy-8l7FjICcmb3829IWPDTm4gjv_pCqV97w46rFaZ7qBGfQII00rfDlCYdTgh94IJARpBdEax2U3o_7uvgVjpngH88li9hQI6IkcS4xGmljZ_5R6RF_fW4NMLu3n2hQ_3UyR-TVt1xigDLVnItYyC2eOuif2TgJAA_T-rgcSRFZCyjkUSkRF4Y_l9FMqFqiRZHLZAlX0DyQStptYTice2ezkLJPixCOI4k27_Bg3HcHfkfPPPNyhfJiHPMUl1NfgZ91_RV9kC2nZmdhz0tdq-tf878iFjp-Rdk7MwjAKCwggGtFPpFNiyQ","DP":"G4Ximfbf8LZG6YUSEPALnPiHFZldxEaP27KJGS-K3QDBnOc-QBswU2G43h6salJsj5dWEkBYrfOX73sE-JYgNwpCHjPXkVJPTKaGs95DPS8j0bzGwzHoJcFVFInygxFKziahcZzrGnvjPgOUBpgmrfJ-S78779WSJU3v7GlFvHk","DQ":"w6FR407kPQmLCrGN2iGKPdNnyOP7qK9w-332QVJQNPpT4Y8nW0cgYIFn1ObZ6-q5xv_jl12FFqbKrQUsXVde_oOYe4AHq3fYHm594T-GkaNQcmNTDwc2qNj3ZhEcYTGLLf6Udvu35MqEd4Nk-IYlItgt_hlLBFmm8XDZHwxQLm0","E":"AQAB","K":null,"KeyId":"D2252FAB0F2BA4E42E1EBDBC1E281E8F","KeyOps":[],"Kid":"D2252FAB0F2BA4E42E1EBDBC1E281E8F","Kty":"RSA","N":"kt6GLyuy4a5jJrlV_4dFW-YZU0J5kT1A6raNIg4nFiY_ct9w3uOgGxEPCSDSF5a_2ldGR95XGol1ygLY81_TtjMFe7yCGUH6cnHXVO75vCuvNXp_3q5xW8L7WueVOC_iXUHf4AEjLz-DFQDHUG4s2vtrXQfGzPt_GsA54UaFBS7KjMOQ6dr-ndBhGxNie33PRfq33SlyoAr72DFL1DNqdnMEXCXLjfX8AQuvhPsYE0sZch6Qbqy2Q6zpEV7N4r36STajjuIMgKK5WfB4UBNnRwCKE1AgyRKVjBVpcyUWFMCB8OQaCZ1UgnGM8K_uD74PMCbHwnwDnKiii_iUd52POQ","Oth":null,"P":"wCLaXWinvykcdpCDfZp3P0SAOLtD48Ioa2U-Yi9y-vjfUpfrQgelpYXzj6PKlIdw8rCjcDIJVYXHJX5SV1hDjKpoSeXC1yXduNLTPDxnR4JfWxgeilNsvS1X9z9Y0KfrCtZuZBYpbq8c9PODDw6mnYlxEC9Ugg8bPPm1BSi-Yls","Q":"w6_X4c6oE78rhUv34x1cb92DL4Ub6KF1BSlOptBMLKWsggQtNZxApC1Thi4eEOHVkr378vgx564rkuAtJbBivNPd5LKXuiqalSUY6332rIIDOe3YK41VoBThtudetvbJubjRdEpcGvxO_5XVBOV1y0WbuzpzOb66tDh2iDsDYPs","QI":"YuJogiqlbtI8CRpDcBDt3PoHYl8YTE3opFp_vAM5r1_g34KckzXFVY6-bIXQRhvyGGHhF5T9obdEogd1hzOFwLbKLXVVhb4CowkMF5-GRachee2L46xpGZtGe0gp_MgEzXFbBHIjv6hZtTMfZasnqUALdBh3vbUF1L0D1M0exnI","Use":null,"X":null,"X5c":[],"X5t":null,"X5tS256":null,"X5u":null,"Y":null,"KeySize":2048,"HasPrivateKey":true,"CryptoProviderFactory":{"CryptoProviderCache":{},"CustomCryptoProvider":null,"CacheSignatureProviders":true,"SignatureProviderObjectPoolCacheSize":16}} \ No newline at end of file diff --git a/src/Services/Identity/src/Identity/Config.cs b/src/Services/Identity/src/Identity/Config.cs new file mode 100644 index 0000000..924ac68 --- /dev/null +++ b/src/Services/Identity/src/Identity/Config.cs @@ -0,0 +1,63 @@ +using System.Collections.Generic; +using Duende.IdentityServer; +using Duende.IdentityServer.Models; +using Identity.Identity.Constants; + +namespace Identity; + +public static class Config +{ + public static IEnumerable IdentityResources => + new List + { + new IdentityResources.OpenId(), + new IdentityResources.Profile(), + new IdentityResources.Email(), + new IdentityResources.Phone(), + new IdentityResources.Address(), + new(Constants.StandardScopes.Roles, new List {"role"}) + }; + + + public static IEnumerable ApiScopes => + new List + { + new(Constants.StandardScopes.FlightApi), + new(Constants.StandardScopes.PassengerApi), + new(Constants.StandardScopes.BookingApi) + }; + + + public static IList ApiResources => + new List + { + new(Constants.StandardScopes.FlightApi), + new(Constants.StandardScopes.PassengerApi), + new(Constants.StandardScopes.BookingApi) + }; + + public static IEnumerable Clients => + new List + { + new() + { + ClientId = "client", + + AllowedGrantTypes = GrantTypes.ResourceOwnerPassword, + + ClientSecrets = + { + new Secret("secret".Sha256()) + }, + + AllowedScopes = + { + IdentityServerConstants.StandardScopes.OpenId, + IdentityServerConstants.StandardScopes.Profile, + Constants.StandardScopes.FlightApi, + Constants.StandardScopes.PassengerApi, + Constants.StandardScopes.BookingApi + } + } + }; +} diff --git a/src/Services/Identity/src/Identity/Data/DesignTimeDbContextFactory.cs b/src/Services/Identity/src/Identity/Data/DesignTimeDbContextFactory.cs new file mode 100644 index 0000000..a9645a9 --- /dev/null +++ b/src/Services/Identity/src/Identity/Data/DesignTimeDbContextFactory.cs @@ -0,0 +1,15 @@ +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Design; + +namespace Identity.Data; + +public class DesignTimeDbContextFactory : IDesignTimeDbContextFactory +{ + public IdentityContext CreateDbContext(string[] args) + { + var builder = new DbContextOptionsBuilder(); + + builder.UseSqlServer("Server=.\\sqlexpress;Database=IdentityDB;Trusted_Connection=True;MultipleActiveResultSets=true"); + return new IdentityContext(builder.Options, null); + } +} diff --git a/src/Services/Identity/src/Identity/Data/IdentityContext.cs b/src/Services/Identity/src/Identity/Data/IdentityContext.cs new file mode 100644 index 0000000..c681b3e --- /dev/null +++ b/src/Services/Identity/src/Identity/Data/IdentityContext.cs @@ -0,0 +1,89 @@ +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Data; +using System.Linq; +using System.Reflection; +using System.Threading; +using System.Threading.Tasks; +using BuildingBlocks.Domain.Event; +using BuildingBlocks.Domain.Model; +using BuildingBlocks.EFCore; +using Identity.Identity.Models; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Identity; +using Microsoft.AspNetCore.Identity.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Storage; + +namespace Identity.Data; + +public sealed class IdentityContext : IdentityDbContext, long, + IdentityUserClaim, + IdentityUserRole, IdentityUserLogin, IdentityRoleClaim, IdentityUserToken>, IDbContext +{ + + private IDbContextTransaction _currentTransaction; + public IdentityContext(DbContextOptions options, IHttpContextAccessor httpContextAccessor) : base(options) + { + } + + 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 + { + 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 IReadOnlyList GetDomainEvents() + { + var domainEntities = ChangeTracker + .Entries() + .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(); + } +} diff --git a/src/Services/Identity/src/Identity/Data/IdentityDataSeeder.cs b/src/Services/Identity/src/Identity/Data/IdentityDataSeeder.cs new file mode 100644 index 0000000..65f72a6 --- /dev/null +++ b/src/Services/Identity/src/Identity/Data/IdentityDataSeeder.cs @@ -0,0 +1,72 @@ +using System; +using System.Threading.Tasks; +using BuildingBlocks.EFCore; +using Identity.Identity.Constants; +using Identity.Identity.Models; +using Microsoft.AspNetCore.Identity; + +namespace Identity.Data; + +public class IdentityDataSeeder : IDataSeeder +{ + private readonly RoleManager> _roleManager; + private readonly UserManager _userManager; + + public IdentityDataSeeder(UserManager userManager, RoleManager> roleManager) + { + _userManager = userManager; + _roleManager = roleManager; + } + + public async Task SeedAllAsync() + { + await SeedRoles(); + await SeedUsers(); + } + + private async Task SeedRoles() + { + if (await _roleManager.RoleExistsAsync(Constants.Role.Admin) == false) + await _roleManager.CreateAsync(new(Constants.Role.Admin)); + + if (await _roleManager.RoleExistsAsync(Constants.Role.User) == false) + await _roleManager.CreateAsync(new(Constants.Role.User)); + } + + private async Task SeedUsers() + { + if (await _userManager.FindByNameAsync("meysamh") == null) + { + var user = new ApplicationUser + { + FirstName = "Meysam", + LastName = "Hadeli", + UserName = "meysamh", + Email = "meysam@test.com", + SecurityStamp = Guid.NewGuid().ToString() + }; + + var result = await _userManager.CreateAsync(user, "Admin@123456"); + + if (result.Succeeded) + await _userManager.AddToRoleAsync(user, Constants.Role.Admin); + } + + if (await _userManager.FindByNameAsync("meysamh2") == null) + { + var user = new ApplicationUser + { + FirstName = "Meysam", + LastName = "Hadeli", + UserName = "meysamh2", + Email = "meysam2@test.com", + SecurityStamp = Guid.NewGuid().ToString() + }; + + var result = await _userManager.CreateAsync(user, "User@123456"); + + if (result.Succeeded) + await _userManager.AddToRoleAsync(user, Constants.Role.User); + } + } +} diff --git a/src/Services/Identity/src/Identity/Data/Migrations/20220418181648_Initial.Designer.cs b/src/Services/Identity/src/Identity/Data/Migrations/20220418181648_Initial.Designer.cs new file mode 100644 index 0000000..d535aa1 --- /dev/null +++ b/src/Services/Identity/src/Identity/Data/Migrations/20220418181648_Initial.Designer.cs @@ -0,0 +1,328 @@ +// +using System; +using Identity.Data; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace Identity.Data.Migrations +{ + [DbContext(typeof(IdentityContext))] + [Migration("20220418181648_Initial")] + partial class Initial + { + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "6.0.1") + .HasAnnotation("Relational:MaxIdentifierLength", 128); + + SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder, 1L, 1); + + modelBuilder.Entity("BuildingBlocks.Outbox.OutboxMessage", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("CorrelationId") + .HasColumnType("uniqueidentifier"); + + b.Property("Data") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("EventType") + .IsRequired() + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("OccurredOn") + .HasColumnType("datetime2"); + + b.Property("ProcessedOn") + .HasColumnType("datetime2"); + + b.Property("Type") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("OutboxMessages", (string)null); + }); + + modelBuilder.Entity("Identity.Identity.Models.ApplicationUser", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id"), 1L, 1); + + b.Property("AccessFailedCount") + .HasColumnType("int"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("nvarchar(max)"); + + b.Property("Email") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.Property("EmailConfirmed") + .HasColumnType("bit"); + + b.Property("FirstName") + .HasColumnType("nvarchar(max)"); + + b.Property("LastName") + .HasColumnType("nvarchar(max)"); + + b.Property("LockoutEnabled") + .HasColumnType("bit"); + + b.Property("LockoutEnd") + .HasColumnType("datetimeoffset"); + + b.Property("NormalizedEmail") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.Property("NormalizedUserName") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.Property("PassPortNumber") + .HasColumnType("nvarchar(max)"); + + b.Property("PasswordHash") + .HasColumnType("nvarchar(max)"); + + b.Property("PhoneNumber") + .HasColumnType("nvarchar(max)"); + + b.Property("PhoneNumberConfirmed") + .HasColumnType("bit"); + + b.Property("SecurityStamp") + .HasColumnType("nvarchar(max)"); + + b.Property("TwoFactorEnabled") + .HasColumnType("bit"); + + b.Property("UserName") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedEmail") + .HasDatabaseName("EmailIndex"); + + b.HasIndex("NormalizedUserName") + .IsUnique() + .HasDatabaseName("UserNameIndex") + .HasFilter("[NormalizedUserName] IS NOT NULL"); + + b.ToTable("AspNetUsers", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRole", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id"), 1L, 1); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("nvarchar(max)"); + + b.Property("Name") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.Property("NormalizedName") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedName") + .IsUnique() + .HasDatabaseName("RoleNameIndex") + .HasFilter("[NormalizedName] IS NOT NULL"); + + b.ToTable("AspNetRoles", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id"), 1L, 1); + + b.Property("ClaimType") + .HasColumnType("nvarchar(max)"); + + b.Property("ClaimValue") + .HasColumnType("nvarchar(max)"); + + b.Property("RoleId") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetRoleClaims", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id"), 1L, 1); + + b.Property("ClaimType") + .HasColumnType("nvarchar(max)"); + + b.Property("ClaimValue") + .HasColumnType("nvarchar(max)"); + + b.Property("UserId") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserClaims", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.Property("LoginProvider") + .HasColumnType("nvarchar(450)"); + + b.Property("ProviderKey") + .HasColumnType("nvarchar(450)"); + + b.Property("ProviderDisplayName") + .HasColumnType("nvarchar(max)"); + + b.Property("UserId") + .HasColumnType("bigint"); + + b.HasKey("LoginProvider", "ProviderKey"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserLogins", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.Property("UserId") + .HasColumnType("bigint"); + + b.Property("RoleId") + .HasColumnType("bigint"); + + b.HasKey("UserId", "RoleId"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetUserRoles", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.Property("UserId") + .HasColumnType("bigint"); + + b.Property("LoginProvider") + .HasColumnType("nvarchar(450)"); + + b.Property("Name") + .HasColumnType("nvarchar(450)"); + + b.Property("Value") + .HasColumnType("nvarchar(max)"); + + b.HasKey("UserId", "LoginProvider", "Name"); + + b.ToTable("AspNetUserTokens", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.HasOne("Identity.Identity.Models.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.HasOne("Identity.Identity.Models.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Identity.Identity.Models.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.HasOne("Identity.Identity.Models.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/Services/Identity/src/Identity/Data/Migrations/20220418181648_Initial.cs b/src/Services/Identity/src/Identity/Data/Migrations/20220418181648_Initial.cs new file mode 100644 index 0000000..70abdd1 --- /dev/null +++ b/src/Services/Identity/src/Identity/Data/Migrations/20220418181648_Initial.cs @@ -0,0 +1,247 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Identity.Data.Migrations +{ + public partial class Initial : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "AspNetRoles", + columns: table => new + { + Id = table.Column(type: "bigint", nullable: false) + .Annotation("SqlServer:Identity", "1, 1"), + Name = table.Column(type: "nvarchar(256)", maxLength: 256, nullable: true), + NormalizedName = table.Column(type: "nvarchar(256)", maxLength: 256, nullable: true), + ConcurrencyStamp = table.Column(type: "nvarchar(max)", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_AspNetRoles", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "AspNetUsers", + columns: table => new + { + Id = table.Column(type: "bigint", nullable: false) + .Annotation("SqlServer:Identity", "1, 1"), + FirstName = table.Column(type: "nvarchar(max)", nullable: true), + LastName = table.Column(type: "nvarchar(max)", nullable: true), + PassPortNumber = table.Column(type: "nvarchar(max)", nullable: true), + UserName = table.Column(type: "nvarchar(256)", maxLength: 256, nullable: true), + NormalizedUserName = table.Column(type: "nvarchar(256)", maxLength: 256, nullable: true), + Email = table.Column(type: "nvarchar(256)", maxLength: 256, nullable: true), + NormalizedEmail = table.Column(type: "nvarchar(256)", maxLength: 256, nullable: true), + EmailConfirmed = table.Column(type: "bit", nullable: false), + PasswordHash = table.Column(type: "nvarchar(max)", nullable: true), + SecurityStamp = table.Column(type: "nvarchar(max)", nullable: true), + ConcurrencyStamp = table.Column(type: "nvarchar(max)", nullable: true), + PhoneNumber = table.Column(type: "nvarchar(max)", nullable: true), + PhoneNumberConfirmed = table.Column(type: "bit", nullable: false), + TwoFactorEnabled = table.Column(type: "bit", nullable: false), + LockoutEnd = table.Column(type: "datetimeoffset", nullable: true), + LockoutEnabled = table.Column(type: "bit", nullable: false), + AccessFailedCount = table.Column(type: "int", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_AspNetUsers", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "OutboxMessages", + columns: table => new + { + Id = table.Column(type: "uniqueidentifier", nullable: false), + Name = table.Column(type: "nvarchar(max)", nullable: false), + OccurredOn = table.Column(type: "datetime2", nullable: false), + Type = table.Column(type: "nvarchar(max)", nullable: false), + Data = table.Column(type: "nvarchar(max)", nullable: false), + ProcessedOn = table.Column(type: "datetime2", nullable: true), + EventType = table.Column(type: "varchar(50)", unicode: false, maxLength: 50, nullable: false), + CorrelationId = table.Column(type: "uniqueidentifier", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_OutboxMessages", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "AspNetRoleClaims", + columns: table => new + { + Id = table.Column(type: "int", nullable: false) + .Annotation("SqlServer:Identity", "1, 1"), + RoleId = table.Column(type: "bigint", nullable: false), + ClaimType = table.Column(type: "nvarchar(max)", nullable: true), + ClaimValue = table.Column(type: "nvarchar(max)", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_AspNetRoleClaims", x => x.Id); + table.ForeignKey( + name: "FK_AspNetRoleClaims_AspNetRoles_RoleId", + column: x => x.RoleId, + principalTable: "AspNetRoles", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "AspNetUserClaims", + columns: table => new + { + Id = table.Column(type: "int", nullable: false) + .Annotation("SqlServer:Identity", "1, 1"), + UserId = table.Column(type: "bigint", nullable: false), + ClaimType = table.Column(type: "nvarchar(max)", nullable: true), + ClaimValue = table.Column(type: "nvarchar(max)", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_AspNetUserClaims", x => x.Id); + table.ForeignKey( + name: "FK_AspNetUserClaims_AspNetUsers_UserId", + column: x => x.UserId, + principalTable: "AspNetUsers", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "AspNetUserLogins", + columns: table => new + { + LoginProvider = table.Column(type: "nvarchar(450)", nullable: false), + ProviderKey = table.Column(type: "nvarchar(450)", nullable: false), + ProviderDisplayName = table.Column(type: "nvarchar(max)", nullable: true), + UserId = table.Column(type: "bigint", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_AspNetUserLogins", x => new { x.LoginProvider, x.ProviderKey }); + table.ForeignKey( + name: "FK_AspNetUserLogins_AspNetUsers_UserId", + column: x => x.UserId, + principalTable: "AspNetUsers", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "AspNetUserRoles", + columns: table => new + { + UserId = table.Column(type: "bigint", nullable: false), + RoleId = table.Column(type: "bigint", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_AspNetUserRoles", x => new { x.UserId, x.RoleId }); + table.ForeignKey( + name: "FK_AspNetUserRoles_AspNetRoles_RoleId", + column: x => x.RoleId, + principalTable: "AspNetRoles", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_AspNetUserRoles_AspNetUsers_UserId", + column: x => x.UserId, + principalTable: "AspNetUsers", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "AspNetUserTokens", + columns: table => new + { + UserId = table.Column(type: "bigint", nullable: false), + LoginProvider = table.Column(type: "nvarchar(450)", nullable: false), + Name = table.Column(type: "nvarchar(450)", nullable: false), + Value = table.Column(type: "nvarchar(max)", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_AspNetUserTokens", x => new { x.UserId, x.LoginProvider, x.Name }); + table.ForeignKey( + name: "FK_AspNetUserTokens_AspNetUsers_UserId", + column: x => x.UserId, + principalTable: "AspNetUsers", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateIndex( + name: "IX_AspNetRoleClaims_RoleId", + table: "AspNetRoleClaims", + column: "RoleId"); + + migrationBuilder.CreateIndex( + name: "RoleNameIndex", + table: "AspNetRoles", + column: "NormalizedName", + unique: true, + filter: "[NormalizedName] IS NOT NULL"); + + migrationBuilder.CreateIndex( + name: "IX_AspNetUserClaims_UserId", + table: "AspNetUserClaims", + column: "UserId"); + + migrationBuilder.CreateIndex( + name: "IX_AspNetUserLogins_UserId", + table: "AspNetUserLogins", + column: "UserId"); + + migrationBuilder.CreateIndex( + name: "IX_AspNetUserRoles_RoleId", + table: "AspNetUserRoles", + column: "RoleId"); + + migrationBuilder.CreateIndex( + name: "EmailIndex", + table: "AspNetUsers", + column: "NormalizedEmail"); + + migrationBuilder.CreateIndex( + name: "UserNameIndex", + table: "AspNetUsers", + column: "NormalizedUserName", + unique: true, + filter: "[NormalizedUserName] IS NOT NULL"); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "AspNetRoleClaims"); + + migrationBuilder.DropTable( + name: "AspNetUserClaims"); + + migrationBuilder.DropTable( + name: "AspNetUserLogins"); + + migrationBuilder.DropTable( + name: "AspNetUserRoles"); + + migrationBuilder.DropTable( + name: "AspNetUserTokens"); + + migrationBuilder.DropTable( + name: "OutboxMessages"); + + migrationBuilder.DropTable( + name: "AspNetRoles"); + + migrationBuilder.DropTable( + name: "AspNetUsers"); + } + } +} diff --git a/src/Services/Identity/src/Identity/Data/Migrations/20220421202152_Add-Internal-Messages.Designer.cs b/src/Services/Identity/src/Identity/Data/Migrations/20220421202152_Add-Internal-Messages.Designer.cs new file mode 100644 index 0000000..ce3c7bc --- /dev/null +++ b/src/Services/Identity/src/Identity/Data/Migrations/20220421202152_Add-Internal-Messages.Designer.cs @@ -0,0 +1,360 @@ +// +using System; +using Identity.Data; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace Identity.Data.Migrations +{ + [DbContext(typeof(IdentityContext))] + [Migration("20220421202152_Add-Internal-Messages")] + partial class AddInternalMessages + { + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "6.0.1") + .HasAnnotation("Relational:MaxIdentifierLength", 128); + + SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder, 1L, 1); + + modelBuilder.Entity("BuildingBlocks.InternalProcessor.InternalMessage", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("CommandType") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("CorrelationId") + .HasColumnType("uniqueidentifier"); + + b.Property("Data") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("OccurredOn") + .HasColumnType("datetime2"); + + b.Property("ProcessedOn") + .HasColumnType("datetime2"); + + b.HasKey("Id"); + + b.ToTable("InternalMessages", (string)null); + }); + + modelBuilder.Entity("BuildingBlocks.Outbox.OutboxMessage", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("CorrelationId") + .HasColumnType("uniqueidentifier"); + + b.Property("Data") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("EventType") + .IsRequired() + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("OccurredOn") + .HasColumnType("datetime2"); + + b.Property("ProcessedOn") + .HasColumnType("datetime2"); + + b.Property("Type") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("OutboxMessages", (string)null); + }); + + modelBuilder.Entity("Identity.Identity.Models.ApplicationUser", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id"), 1L, 1); + + b.Property("AccessFailedCount") + .HasColumnType("int"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("nvarchar(max)"); + + b.Property("Email") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.Property("EmailConfirmed") + .HasColumnType("bit"); + + b.Property("FirstName") + .HasColumnType("nvarchar(max)"); + + b.Property("LastName") + .HasColumnType("nvarchar(max)"); + + b.Property("LockoutEnabled") + .HasColumnType("bit"); + + b.Property("LockoutEnd") + .HasColumnType("datetimeoffset"); + + b.Property("NormalizedEmail") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.Property("NormalizedUserName") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.Property("PassPortNumber") + .HasColumnType("nvarchar(max)"); + + b.Property("PasswordHash") + .HasColumnType("nvarchar(max)"); + + b.Property("PhoneNumber") + .HasColumnType("nvarchar(max)"); + + b.Property("PhoneNumberConfirmed") + .HasColumnType("bit"); + + b.Property("SecurityStamp") + .HasColumnType("nvarchar(max)"); + + b.Property("TwoFactorEnabled") + .HasColumnType("bit"); + + b.Property("UserName") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedEmail") + .HasDatabaseName("EmailIndex"); + + b.HasIndex("NormalizedUserName") + .IsUnique() + .HasDatabaseName("UserNameIndex") + .HasFilter("[NormalizedUserName] IS NOT NULL"); + + b.ToTable("AspNetUsers", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRole", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id"), 1L, 1); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("nvarchar(max)"); + + b.Property("Name") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.Property("NormalizedName") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedName") + .IsUnique() + .HasDatabaseName("RoleNameIndex") + .HasFilter("[NormalizedName] IS NOT NULL"); + + b.ToTable("AspNetRoles", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id"), 1L, 1); + + b.Property("ClaimType") + .HasColumnType("nvarchar(max)"); + + b.Property("ClaimValue") + .HasColumnType("nvarchar(max)"); + + b.Property("RoleId") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetRoleClaims", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id"), 1L, 1); + + b.Property("ClaimType") + .HasColumnType("nvarchar(max)"); + + b.Property("ClaimValue") + .HasColumnType("nvarchar(max)"); + + b.Property("UserId") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserClaims", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.Property("LoginProvider") + .HasColumnType("nvarchar(450)"); + + b.Property("ProviderKey") + .HasColumnType("nvarchar(450)"); + + b.Property("ProviderDisplayName") + .HasColumnType("nvarchar(max)"); + + b.Property("UserId") + .HasColumnType("bigint"); + + b.HasKey("LoginProvider", "ProviderKey"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserLogins", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.Property("UserId") + .HasColumnType("bigint"); + + b.Property("RoleId") + .HasColumnType("bigint"); + + b.HasKey("UserId", "RoleId"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetUserRoles", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.Property("UserId") + .HasColumnType("bigint"); + + b.Property("LoginProvider") + .HasColumnType("nvarchar(450)"); + + b.Property("Name") + .HasColumnType("nvarchar(450)"); + + b.Property("Value") + .HasColumnType("nvarchar(max)"); + + b.HasKey("UserId", "LoginProvider", "Name"); + + b.ToTable("AspNetUserTokens", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.HasOne("Identity.Identity.Models.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.HasOne("Identity.Identity.Models.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Identity.Identity.Models.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.HasOne("Identity.Identity.Models.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/Services/Identity/src/Identity/Data/Migrations/20220421202152_Add-Internal-Messages.cs b/src/Services/Identity/src/Identity/Data/Migrations/20220421202152_Add-Internal-Messages.cs new file mode 100644 index 0000000..a156e10 --- /dev/null +++ b/src/Services/Identity/src/Identity/Data/Migrations/20220421202152_Add-Internal-Messages.cs @@ -0,0 +1,36 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Identity.Data.Migrations +{ + public partial class AddInternalMessages : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "InternalMessages", + columns: table => new + { + Id = table.Column(type: "uniqueidentifier", nullable: false), + Name = table.Column(type: "nvarchar(max)", nullable: false), + OccurredOn = table.Column(type: "datetime2", nullable: false), + CommandType = table.Column(type: "nvarchar(max)", nullable: false), + Data = table.Column(type: "nvarchar(max)", nullable: false), + ProcessedOn = table.Column(type: "datetime2", nullable: true), + CorrelationId = table.Column(type: "uniqueidentifier", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_InternalMessages", x => x.Id); + }); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "InternalMessages"); + } + } +} diff --git a/src/Services/Identity/src/Identity/Data/Migrations/20220422121534_Update-EventId.Designer.cs b/src/Services/Identity/src/Identity/Data/Migrations/20220422121534_Update-EventId.Designer.cs new file mode 100644 index 0000000..66d16f8 --- /dev/null +++ b/src/Services/Identity/src/Identity/Data/Migrations/20220422121534_Update-EventId.Designer.cs @@ -0,0 +1,360 @@ +// +using System; +using Identity.Data; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace Identity.Data.Migrations +{ + [DbContext(typeof(IdentityContext))] + [Migration("20220422121534_Update-EventId")] + partial class UpdateEventId + { + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "6.0.1") + .HasAnnotation("Relational:MaxIdentifierLength", 128); + + SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder, 1L, 1); + + modelBuilder.Entity("BuildingBlocks.InternalProcessor.InternalMessage", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("CommandType") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("CorrelationId") + .HasColumnType("uniqueidentifier"); + + b.Property("Data") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("OccurredOn") + .HasColumnType("datetime2"); + + b.Property("ProcessedOn") + .HasColumnType("datetime2"); + + b.HasKey("Id"); + + b.ToTable("InternalMessages", (string)null); + }); + + modelBuilder.Entity("BuildingBlocks.Outbox.OutboxMessage", b => + { + b.Property("EventId") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("CorrelationId") + .HasColumnType("uniqueidentifier"); + + b.Property("Data") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("EventType") + .IsRequired() + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("OccurredOn") + .HasColumnType("datetime2"); + + b.Property("ProcessedOn") + .HasColumnType("datetime2"); + + b.Property("Type") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("EventId"); + + b.ToTable("OutboxMessages", (string)null); + }); + + modelBuilder.Entity("Identity.Identity.Models.ApplicationUser", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id"), 1L, 1); + + b.Property("AccessFailedCount") + .HasColumnType("int"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("nvarchar(max)"); + + b.Property("Email") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.Property("EmailConfirmed") + .HasColumnType("bit"); + + b.Property("FirstName") + .HasColumnType("nvarchar(max)"); + + b.Property("LastName") + .HasColumnType("nvarchar(max)"); + + b.Property("LockoutEnabled") + .HasColumnType("bit"); + + b.Property("LockoutEnd") + .HasColumnType("datetimeoffset"); + + b.Property("NormalizedEmail") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.Property("NormalizedUserName") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.Property("PassPortNumber") + .HasColumnType("nvarchar(max)"); + + b.Property("PasswordHash") + .HasColumnType("nvarchar(max)"); + + b.Property("PhoneNumber") + .HasColumnType("nvarchar(max)"); + + b.Property("PhoneNumberConfirmed") + .HasColumnType("bit"); + + b.Property("SecurityStamp") + .HasColumnType("nvarchar(max)"); + + b.Property("TwoFactorEnabled") + .HasColumnType("bit"); + + b.Property("UserName") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedEmail") + .HasDatabaseName("EmailIndex"); + + b.HasIndex("NormalizedUserName") + .IsUnique() + .HasDatabaseName("UserNameIndex") + .HasFilter("[NormalizedUserName] IS NOT NULL"); + + b.ToTable("AspNetUsers", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRole", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id"), 1L, 1); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("nvarchar(max)"); + + b.Property("Name") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.Property("NormalizedName") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedName") + .IsUnique() + .HasDatabaseName("RoleNameIndex") + .HasFilter("[NormalizedName] IS NOT NULL"); + + b.ToTable("AspNetRoles", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id"), 1L, 1); + + b.Property("ClaimType") + .HasColumnType("nvarchar(max)"); + + b.Property("ClaimValue") + .HasColumnType("nvarchar(max)"); + + b.Property("RoleId") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetRoleClaims", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id"), 1L, 1); + + b.Property("ClaimType") + .HasColumnType("nvarchar(max)"); + + b.Property("ClaimValue") + .HasColumnType("nvarchar(max)"); + + b.Property("UserId") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserClaims", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.Property("LoginProvider") + .HasColumnType("nvarchar(450)"); + + b.Property("ProviderKey") + .HasColumnType("nvarchar(450)"); + + b.Property("ProviderDisplayName") + .HasColumnType("nvarchar(max)"); + + b.Property("UserId") + .HasColumnType("bigint"); + + b.HasKey("LoginProvider", "ProviderKey"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserLogins", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.Property("UserId") + .HasColumnType("bigint"); + + b.Property("RoleId") + .HasColumnType("bigint"); + + b.HasKey("UserId", "RoleId"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetUserRoles", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.Property("UserId") + .HasColumnType("bigint"); + + b.Property("LoginProvider") + .HasColumnType("nvarchar(450)"); + + b.Property("Name") + .HasColumnType("nvarchar(450)"); + + b.Property("Value") + .HasColumnType("nvarchar(max)"); + + b.HasKey("UserId", "LoginProvider", "Name"); + + b.ToTable("AspNetUserTokens", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.HasOne("Identity.Identity.Models.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.HasOne("Identity.Identity.Models.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Identity.Identity.Models.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.HasOne("Identity.Identity.Models.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/Services/Identity/src/Identity/Data/Migrations/20220422121534_Update-EventId.cs b/src/Services/Identity/src/Identity/Data/Migrations/20220422121534_Update-EventId.cs new file mode 100644 index 0000000..45cd9de --- /dev/null +++ b/src/Services/Identity/src/Identity/Data/Migrations/20220422121534_Update-EventId.cs @@ -0,0 +1,25 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Identity.Data.Migrations +{ + public partial class UpdateEventId : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.RenameColumn( + name: "Id", + table: "OutboxMessages", + newName: "EventId"); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.RenameColumn( + name: "EventId", + table: "OutboxMessages", + newName: "Id"); + } + } +} diff --git a/src/Services/Identity/src/Identity/Data/Migrations/20220422121846_Update-Internal-EventId.Designer.cs b/src/Services/Identity/src/Identity/Data/Migrations/20220422121846_Update-Internal-EventId.Designer.cs new file mode 100644 index 0000000..130976b --- /dev/null +++ b/src/Services/Identity/src/Identity/Data/Migrations/20220422121846_Update-Internal-EventId.Designer.cs @@ -0,0 +1,360 @@ +// +using System; +using Identity.Data; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace Identity.Data.Migrations +{ + [DbContext(typeof(IdentityContext))] + [Migration("20220422121846_Update-Internal-EventId")] + partial class UpdateInternalEventId + { + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "6.0.1") + .HasAnnotation("Relational:MaxIdentifierLength", 128); + + SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder, 1L, 1); + + modelBuilder.Entity("BuildingBlocks.InternalProcessor.InternalMessage", b => + { + b.Property("EventId") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("CommandType") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("CorrelationId") + .HasColumnType("uniqueidentifier"); + + b.Property("Data") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("OccurredOn") + .HasColumnType("datetime2"); + + b.Property("ProcessedOn") + .HasColumnType("datetime2"); + + b.HasKey("EventId"); + + b.ToTable("InternalMessages", (string)null); + }); + + modelBuilder.Entity("BuildingBlocks.Outbox.OutboxMessage", b => + { + b.Property("EventId") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("CorrelationId") + .HasColumnType("uniqueidentifier"); + + b.Property("Data") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("EventType") + .IsRequired() + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("OccurredOn") + .HasColumnType("datetime2"); + + b.Property("ProcessedOn") + .HasColumnType("datetime2"); + + b.Property("Type") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("EventId"); + + b.ToTable("OutboxMessages", (string)null); + }); + + modelBuilder.Entity("Identity.Identity.Models.ApplicationUser", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id"), 1L, 1); + + b.Property("AccessFailedCount") + .HasColumnType("int"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("nvarchar(max)"); + + b.Property("Email") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.Property("EmailConfirmed") + .HasColumnType("bit"); + + b.Property("FirstName") + .HasColumnType("nvarchar(max)"); + + b.Property("LastName") + .HasColumnType("nvarchar(max)"); + + b.Property("LockoutEnabled") + .HasColumnType("bit"); + + b.Property("LockoutEnd") + .HasColumnType("datetimeoffset"); + + b.Property("NormalizedEmail") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.Property("NormalizedUserName") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.Property("PassPortNumber") + .HasColumnType("nvarchar(max)"); + + b.Property("PasswordHash") + .HasColumnType("nvarchar(max)"); + + b.Property("PhoneNumber") + .HasColumnType("nvarchar(max)"); + + b.Property("PhoneNumberConfirmed") + .HasColumnType("bit"); + + b.Property("SecurityStamp") + .HasColumnType("nvarchar(max)"); + + b.Property("TwoFactorEnabled") + .HasColumnType("bit"); + + b.Property("UserName") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedEmail") + .HasDatabaseName("EmailIndex"); + + b.HasIndex("NormalizedUserName") + .IsUnique() + .HasDatabaseName("UserNameIndex") + .HasFilter("[NormalizedUserName] IS NOT NULL"); + + b.ToTable("AspNetUsers", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRole", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id"), 1L, 1); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("nvarchar(max)"); + + b.Property("Name") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.Property("NormalizedName") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedName") + .IsUnique() + .HasDatabaseName("RoleNameIndex") + .HasFilter("[NormalizedName] IS NOT NULL"); + + b.ToTable("AspNetRoles", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id"), 1L, 1); + + b.Property("ClaimType") + .HasColumnType("nvarchar(max)"); + + b.Property("ClaimValue") + .HasColumnType("nvarchar(max)"); + + b.Property("RoleId") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetRoleClaims", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id"), 1L, 1); + + b.Property("ClaimType") + .HasColumnType("nvarchar(max)"); + + b.Property("ClaimValue") + .HasColumnType("nvarchar(max)"); + + b.Property("UserId") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserClaims", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.Property("LoginProvider") + .HasColumnType("nvarchar(450)"); + + b.Property("ProviderKey") + .HasColumnType("nvarchar(450)"); + + b.Property("ProviderDisplayName") + .HasColumnType("nvarchar(max)"); + + b.Property("UserId") + .HasColumnType("bigint"); + + b.HasKey("LoginProvider", "ProviderKey"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserLogins", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.Property("UserId") + .HasColumnType("bigint"); + + b.Property("RoleId") + .HasColumnType("bigint"); + + b.HasKey("UserId", "RoleId"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetUserRoles", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.Property("UserId") + .HasColumnType("bigint"); + + b.Property("LoginProvider") + .HasColumnType("nvarchar(450)"); + + b.Property("Name") + .HasColumnType("nvarchar(450)"); + + b.Property("Value") + .HasColumnType("nvarchar(max)"); + + b.HasKey("UserId", "LoginProvider", "Name"); + + b.ToTable("AspNetUserTokens", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.HasOne("Identity.Identity.Models.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.HasOne("Identity.Identity.Models.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Identity.Identity.Models.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.HasOne("Identity.Identity.Models.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/Services/Identity/src/Identity/Data/Migrations/20220422121846_Update-Internal-EventId.cs b/src/Services/Identity/src/Identity/Data/Migrations/20220422121846_Update-Internal-EventId.cs new file mode 100644 index 0000000..6539a06 --- /dev/null +++ b/src/Services/Identity/src/Identity/Data/Migrations/20220422121846_Update-Internal-EventId.cs @@ -0,0 +1,25 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Identity.Data.Migrations +{ + public partial class UpdateInternalEventId : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.RenameColumn( + name: "Id", + table: "InternalMessages", + newName: "EventId"); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.RenameColumn( + name: "EventId", + table: "InternalMessages", + newName: "Id"); + } + } +} diff --git a/src/Services/Identity/src/Identity/Data/Migrations/IdentityContextModelSnapshot.cs b/src/Services/Identity/src/Identity/Data/Migrations/IdentityContextModelSnapshot.cs new file mode 100644 index 0000000..fdb99f4 --- /dev/null +++ b/src/Services/Identity/src/Identity/Data/Migrations/IdentityContextModelSnapshot.cs @@ -0,0 +1,358 @@ +// +using System; +using Identity.Data; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace Identity.Data.Migrations +{ + [DbContext(typeof(IdentityContext))] + partial class IdentityContextModelSnapshot : ModelSnapshot + { + protected override void BuildModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "6.0.1") + .HasAnnotation("Relational:MaxIdentifierLength", 128); + + SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder, 1L, 1); + + modelBuilder.Entity("BuildingBlocks.InternalProcessor.InternalMessage", b => + { + b.Property("EventId") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("CommandType") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("CorrelationId") + .HasColumnType("uniqueidentifier"); + + b.Property("Data") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("OccurredOn") + .HasColumnType("datetime2"); + + b.Property("ProcessedOn") + .HasColumnType("datetime2"); + + b.HasKey("EventId"); + + b.ToTable("InternalMessages", (string)null); + }); + + modelBuilder.Entity("BuildingBlocks.Outbox.OutboxMessage", b => + { + b.Property("EventId") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("CorrelationId") + .HasColumnType("uniqueidentifier"); + + b.Property("Data") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("EventType") + .IsRequired() + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("OccurredOn") + .HasColumnType("datetime2"); + + b.Property("ProcessedOn") + .HasColumnType("datetime2"); + + b.Property("Type") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("EventId"); + + b.ToTable("OutboxMessages", (string)null); + }); + + modelBuilder.Entity("Identity.Identity.Models.ApplicationUser", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id"), 1L, 1); + + b.Property("AccessFailedCount") + .HasColumnType("int"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("nvarchar(max)"); + + b.Property("Email") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.Property("EmailConfirmed") + .HasColumnType("bit"); + + b.Property("FirstName") + .HasColumnType("nvarchar(max)"); + + b.Property("LastName") + .HasColumnType("nvarchar(max)"); + + b.Property("LockoutEnabled") + .HasColumnType("bit"); + + b.Property("LockoutEnd") + .HasColumnType("datetimeoffset"); + + b.Property("NormalizedEmail") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.Property("NormalizedUserName") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.Property("PassPortNumber") + .HasColumnType("nvarchar(max)"); + + b.Property("PasswordHash") + .HasColumnType("nvarchar(max)"); + + b.Property("PhoneNumber") + .HasColumnType("nvarchar(max)"); + + b.Property("PhoneNumberConfirmed") + .HasColumnType("bit"); + + b.Property("SecurityStamp") + .HasColumnType("nvarchar(max)"); + + b.Property("TwoFactorEnabled") + .HasColumnType("bit"); + + b.Property("UserName") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedEmail") + .HasDatabaseName("EmailIndex"); + + b.HasIndex("NormalizedUserName") + .IsUnique() + .HasDatabaseName("UserNameIndex") + .HasFilter("[NormalizedUserName] IS NOT NULL"); + + b.ToTable("AspNetUsers", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRole", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id"), 1L, 1); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("nvarchar(max)"); + + b.Property("Name") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.Property("NormalizedName") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedName") + .IsUnique() + .HasDatabaseName("RoleNameIndex") + .HasFilter("[NormalizedName] IS NOT NULL"); + + b.ToTable("AspNetRoles", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id"), 1L, 1); + + b.Property("ClaimType") + .HasColumnType("nvarchar(max)"); + + b.Property("ClaimValue") + .HasColumnType("nvarchar(max)"); + + b.Property("RoleId") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetRoleClaims", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id"), 1L, 1); + + b.Property("ClaimType") + .HasColumnType("nvarchar(max)"); + + b.Property("ClaimValue") + .HasColumnType("nvarchar(max)"); + + b.Property("UserId") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserClaims", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.Property("LoginProvider") + .HasColumnType("nvarchar(450)"); + + b.Property("ProviderKey") + .HasColumnType("nvarchar(450)"); + + b.Property("ProviderDisplayName") + .HasColumnType("nvarchar(max)"); + + b.Property("UserId") + .HasColumnType("bigint"); + + b.HasKey("LoginProvider", "ProviderKey"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserLogins", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.Property("UserId") + .HasColumnType("bigint"); + + b.Property("RoleId") + .HasColumnType("bigint"); + + b.HasKey("UserId", "RoleId"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetUserRoles", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.Property("UserId") + .HasColumnType("bigint"); + + b.Property("LoginProvider") + .HasColumnType("nvarchar(450)"); + + b.Property("Name") + .HasColumnType("nvarchar(450)"); + + b.Property("Value") + .HasColumnType("nvarchar(max)"); + + b.HasKey("UserId", "LoginProvider", "Name"); + + b.ToTable("AspNetUserTokens", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.HasOne("Identity.Identity.Models.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.HasOne("Identity.Identity.Models.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Identity.Identity.Models.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.HasOne("Identity.Identity.Models.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/Services/Identity/src/Identity/Data/readme.md b/src/Services/Identity/src/Identity/Data/readme.md new file mode 100644 index 0000000..69eb2cb --- /dev/null +++ b/src/Services/Identity/src/Identity/Data/readme.md @@ -0,0 +1,2 @@ +dotnet ef migrations add initial --context IdentityContext -o "Data\Migrations" +dotnet ef database update --context IdentityContext diff --git a/src/Services/Identity/src/Identity/EventMapper.cs b/src/Services/Identity/src/Identity/EventMapper.cs new file mode 100644 index 0000000..2a5fa56 --- /dev/null +++ b/src/Services/Identity/src/Identity/EventMapper.cs @@ -0,0 +1,22 @@ +using System.Collections.Generic; +using System.Linq; +using BuildingBlocks.Domain; +using BuildingBlocks.Domain.Event; + +namespace Identity; + +public sealed class EventMapper : IEventMapper +{ + public IEnumerable MapAll(IEnumerable events) + { + return events.Select(Map); + } + + public IIntegrationEvent Map(IDomainEvent @event) + { + return @event switch + { + _ => null + }; + } +} \ No newline at end of file diff --git a/src/Services/Identity/src/Identity/Extensions/IdentityServerExtensions.cs b/src/Services/Identity/src/Identity/Extensions/IdentityServerExtensions.cs new file mode 100644 index 0000000..d2358c2 --- /dev/null +++ b/src/Services/Identity/src/Identity/Extensions/IdentityServerExtensions.cs @@ -0,0 +1,45 @@ +using Identity.Data; +using Identity.Identity.Models; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Identity; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; + +namespace Identity.Extensions; + +public static class IdentityServerExtensions +{ + public static IServiceCollection AddIdentityServer(this IServiceCollection services, IWebHostEnvironment env) + { + services.AddIdentity>(config => + { + config.Password.RequiredLength = 6; + config.Password.RequireDigit = false; + config.Password.RequireNonAlphanumeric = false; + config.Password.RequireUppercase = false; + }) + .AddEntityFrameworkStores() + .AddDefaultTokenProviders(); + + var identityServerBuilder = services.AddIdentityServer(options => + { + options.Events.RaiseErrorEvents = true; + options.Events.RaiseInformationEvents = true; + options.Events.RaiseFailureEvents = true; + options.Events.RaiseSuccessEvents = true; + }) + .AddInMemoryIdentityResources(Config.IdentityResources) + .AddInMemoryApiResources(Config.ApiResources) + .AddInMemoryApiScopes(Config.ApiScopes) + .AddInMemoryClients(Config.Clients) + .AddAspNetIdentity() + .AddResourceOwnerValidator(); + + if (env.IsDevelopment()) + { + identityServerBuilder.AddDeveloperSigningCredential(); + } + + return services; + } +} diff --git a/src/Services/Identity/src/Identity/Extensions/MediatRExtensions.cs b/src/Services/Identity/src/Identity/Extensions/MediatRExtensions.cs new file mode 100644 index 0000000..97dbbf2 --- /dev/null +++ b/src/Services/Identity/src/Identity/Extensions/MediatRExtensions.cs @@ -0,0 +1,20 @@ +using BuildingBlocks.EFCore; +using BuildingBlocks.Logging; +using BuildingBlocks.Validation; +using MediatR; +using Microsoft.Extensions.DependencyInjection; + +namespace Identity.Extensions; + +public static class MediatRExtensions +{ + public static IServiceCollection AddCustomMediatR(this IServiceCollection services) + { + services.AddMediatR(typeof(IdentityRoot).Assembly); + services.AddScoped(typeof(IPipelineBehavior<,>), typeof(ValidationBehavior<,>)); + services.AddScoped(typeof(IPipelineBehavior<,>), typeof(LoggingBehavior<,>)); + services.AddScoped(typeof(IPipelineBehavior<,>), typeof(EfIdentityTxBehavior<,>)); + + return services; + } +} diff --git a/src/Services/Identity/src/Identity/Extensions/MigrationsExtensions.cs b/src/Services/Identity/src/Identity/Extensions/MigrationsExtensions.cs new file mode 100644 index 0000000..cc65137 --- /dev/null +++ b/src/Services/Identity/src/Identity/Extensions/MigrationsExtensions.cs @@ -0,0 +1,37 @@ +using System; +using BuildingBlocks.EFCore; +using Identity.Data; +using Microsoft.AspNetCore.Builder; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.DependencyInjection; + +namespace Identity.Extensions; + +public static class MigrationsExtensions +{ + public static IApplicationBuilder UseMigrations(this IApplicationBuilder app) + { + MigrateDatabase(app.ApplicationServices); + SeedData(app.ApplicationServices); + + return app; + } + + private static void MigrateDatabase(IServiceProvider serviceProvider) + { + using var scope = serviceProvider.CreateScope(); + + var context = scope.ServiceProvider.GetRequiredService(); + context.Database.Migrate(); + } + + private static void SeedData(IServiceProvider serviceProvider) + { + using var scope = serviceProvider.CreateScope(); + var seeders = scope.ServiceProvider.GetServices(); + foreach (var seeder in seeders) + { + seeder.SeedAllAsync().GetAwaiter().GetResult(); + } + } +} diff --git a/src/Services/Identity/src/Identity/Extensions/ProblemDetailsExtensions.cs b/src/Services/Identity/src/Identity/Extensions/ProblemDetailsExtensions.cs new file mode 100644 index 0000000..7bb8e50 --- /dev/null +++ b/src/Services/Identity/src/Identity/Extensions/ProblemDetailsExtensions.cs @@ -0,0 +1,85 @@ +using System; +using BuildingBlocks.Exception; +using Hellang.Middleware.ProblemDetails; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Newtonsoft.Json; + +namespace Identity.Extensions; + +public static class ProblemDetailsExtensions +{ + public static IServiceCollection AddCustomProblemDetails(this IServiceCollection services) + { + services.AddProblemDetails(x => + { + // Control when an exception is included + x.IncludeExceptionDetails = (ctx, _) => + { + // Fetch services from HttpContext.RequestServices + var env = ctx.RequestServices.GetRequiredService(); + return env.IsDevelopment() || env.IsStaging(); + }; + x.Map(ex => new ProblemDetails + { + Title = "Application rule broken", + Status = StatusCodes.Status409Conflict, + Detail = ex.Message, + Type = "https://somedomain/application-rule-validation-error" + }); + + // Exception will produce and returns from our FluentValidation RequestValidationBehavior + x.Map(ex => new ProblemDetails + { + Title = "input validation rules broken", + Status = StatusCodes.Status400BadRequest, + Detail = JsonConvert.SerializeObject(ex.ValidationResultModel.Errors), + Type = "https://somedomain/input-validation-rules-error" + }); + x.Map(ex => new ProblemDetails + { + Title = "bad request exception", + Status = StatusCodes.Status400BadRequest, + Detail = ex.Message, + Type = "https://somedomain/bad-request-error" + }); + x.Map(ex => new ProblemDetails + { + Title = "not found exception", + Status = StatusCodes.Status404NotFound, + Detail = ex.Message, + Type = "https://somedomain/not-found-error" + }); + x.Map(ex => new ProblemDetails + { + Title = "api server exception", + Status = StatusCodes.Status500InternalServerError, + Detail = ex.Message, + Type = "https://somedomain/api-server-error" + }); + x.Map(ex => new ProblemDetails + { + Title = "application exception", + Status = StatusCodes.Status500InternalServerError, + Detail = ex.Message, + Type = "https://somedomain/application-error" + }); + x.Map(ex => + { + var pd = new ProblemDetails + { + Status = (int)ex.StatusCode, + Title = "identity exception", + Detail = ex.Message, + Type = "https://somedomain/identity-error" + }; + + return pd; + }); + x.MapToStatusCode(StatusCodes.Status400BadRequest); + }); + return services; + } +} \ No newline at end of file diff --git a/src/Services/Identity/src/Identity/Identity.csproj b/src/Services/Identity/src/Identity/Identity.csproj new file mode 100644 index 0000000..e2df875 --- /dev/null +++ b/src/Services/Identity/src/Identity/Identity.csproj @@ -0,0 +1,18 @@ + + + + net6.0 + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + diff --git a/src/Services/Identity/src/Identity/Identity/Constants/Constants.cs b/src/Services/Identity/src/Identity/Identity/Constants/Constants.cs new file mode 100644 index 0000000..665e99a --- /dev/null +++ b/src/Services/Identity/src/Identity/Identity/Constants/Constants.cs @@ -0,0 +1,18 @@ +namespace Identity.Identity.Constants; + +public static class Constants +{ + public static class Role + { + public const string Admin = "admin"; + public const string User = "user"; + } + + public static class StandardScopes + { + public const string Roles = "roles"; + public const string FlightApi = "flight-api"; + public const string PassengerApi = "passenger-api"; + public const string BookingApi = "booking-api"; + } +} diff --git a/src/Services/Identity/src/Identity/Identity/Dtos/RegisterNewUserResponseDto.cs b/src/Services/Identity/src/Identity/Identity/Dtos/RegisterNewUserResponseDto.cs new file mode 100644 index 0000000..00b87b7 --- /dev/null +++ b/src/Services/Identity/src/Identity/Identity/Dtos/RegisterNewUserResponseDto.cs @@ -0,0 +1,13 @@ +using System; + +namespace Identity.Identity.Dtos; + +public record RegisterNewUserResponseDto +{ + public long Id { get; init; } + public string FirstName { get; init; } + public string LastName { get; init; } + public string Username { get; init; } + + public string PassportNumber { get; set; } +} diff --git a/src/Services/Identity/src/Identity/Identity/Exceptions/RegisterIdentityUserException.cs b/src/Services/Identity/src/Identity/Identity/Exceptions/RegisterIdentityUserException.cs new file mode 100644 index 0000000..60155e0 --- /dev/null +++ b/src/Services/Identity/src/Identity/Identity/Exceptions/RegisterIdentityUserException.cs @@ -0,0 +1,10 @@ +using BuildingBlocks.Exception; + +namespace Identity.Identity.Exceptions; + +public class RegisterIdentityUserException : AppException +{ + public RegisterIdentityUserException(string error) : base(error) + { + } +} \ No newline at end of file diff --git a/src/Services/Identity/src/Identity/Identity/Features/RegisterNewUser/RegisterNewUserCommand.cs b/src/Services/Identity/src/Identity/Identity/Features/RegisterNewUser/RegisterNewUserCommand.cs new file mode 100644 index 0000000..9b159d5 --- /dev/null +++ b/src/Services/Identity/src/Identity/Identity/Features/RegisterNewUser/RegisterNewUserCommand.cs @@ -0,0 +1,6 @@ +using Identity.Identity.Dtos; +using MediatR; + +namespace Identity.Identity.Features.RegisterNewUser; + +public record RegisterNewUserCommand(string FirstName, string LastName, string Username, string Email, string Password, string ConfirmPassword, string PassportNumber) : IRequest; diff --git a/src/Services/Identity/src/Identity/Identity/Features/RegisterNewUser/RegisterNewUserCommandHandler.cs b/src/Services/Identity/src/Identity/Identity/Features/RegisterNewUser/RegisterNewUserCommandHandler.cs new file mode 100644 index 0000000..f394bc2 --- /dev/null +++ b/src/Services/Identity/src/Identity/Identity/Features/RegisterNewUser/RegisterNewUserCommandHandler.cs @@ -0,0 +1,63 @@ +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Ardalis.GuardClauses; +using BuildingBlocks.Contracts.EventBus.Messages; +using BuildingBlocks.Domain; +using Identity.Identity.Dtos; +using Identity.Identity.Exceptions; +using Identity.Identity.Models; +using MediatR; +using Microsoft.AspNetCore.Identity; + +namespace Identity.Identity.Features.RegisterNewUser; + +public class RegisterNewUserCommandHandler : IRequestHandler +{ + private readonly IBusPublisher _busPublisher; + private readonly UserManager _userManager; + + public RegisterNewUserCommandHandler(UserManager userManager, + IBusPublisher busPublisher) + { + _userManager = userManager; + _busPublisher = busPublisher; + } + + public async Task Handle(RegisterNewUserCommand command, + CancellationToken cancellationToken) + { + Guard.Against.Null(command, nameof(command)); + + var applicationUser = new ApplicationUser + { + FirstName = command.FirstName, + LastName = command.LastName, + UserName = command.Username, + Email = command.Email, + PasswordHash = command.Password, + PassPortNumber = command.PassportNumber + }; + + var identityResult = await _userManager.CreateAsync(applicationUser, command.Password); + var roleResult = await _userManager.AddToRoleAsync(applicationUser, Constants.Constants.Role.User); + + if (identityResult.Succeeded == false) + throw new RegisterIdentityUserException(string.Join(',', identityResult.Errors.Select(e => e.Description))); + + if (roleResult.Succeeded == false) + throw new RegisterIdentityUserException(string.Join(',', roleResult.Errors.Select(e => e.Description))); + + await _busPublisher.SendAsync(new UserCreated(applicationUser.Id, applicationUser.FirstName + " " + applicationUser.LastName, + applicationUser.PassPortNumber), cancellationToken); + + return new RegisterNewUserResponseDto + { + Id = applicationUser.Id, + FirstName = applicationUser.FirstName, + LastName = applicationUser.LastName, + Username = applicationUser.UserName, + PassportNumber = applicationUser.PassPortNumber + }; + } +} diff --git a/src/Services/Identity/src/Identity/Identity/Features/RegisterNewUser/RegisterNewUserEndpoint.cs b/src/Services/Identity/src/Identity/Identity/Features/RegisterNewUser/RegisterNewUserEndpoint.cs new file mode 100644 index 0000000..0d1251d --- /dev/null +++ b/src/Services/Identity/src/Identity/Identity/Features/RegisterNewUser/RegisterNewUserEndpoint.cs @@ -0,0 +1,33 @@ +using System.Threading; +using System.Threading.Tasks; +using MediatR; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Swashbuckle.AspNetCore.Annotations; + +namespace Identity.Identity.Features.RegisterNewUser; + +[Route("identity/register-user")] +[ApiController] +public class LoginEndpoint : ControllerBase +{ + private readonly IMediator _mediator; + + public LoginEndpoint(IMediator mediator) + { + _mediator = mediator; + } + + // [Authorize] + [HttpPost] + [ProducesResponseType(StatusCodes.Status201Created)] + [ProducesResponseType(StatusCodes.Status400BadRequest)] + [SwaggerOperation(Summary = "Register new user", Description = "Register new user")] + public async Task RegisterNewUser([FromBody] RegisterNewUserCommand command, + CancellationToken cancellationToken) + { + var result = await _mediator.Send(command, cancellationToken); + + return Ok(result); + } +} diff --git a/src/Services/Identity/src/Identity/Identity/Features/RegisterNewUser/RegisterNewUserValidator.cs b/src/Services/Identity/src/Identity/Identity/Features/RegisterNewUser/RegisterNewUserValidator.cs new file mode 100644 index 0000000..7e239f2 --- /dev/null +++ b/src/Services/Identity/src/Identity/Identity/Features/RegisterNewUser/RegisterNewUserValidator.cs @@ -0,0 +1,28 @@ +using FluentValidation; + +namespace Identity.Identity.Features.RegisterNewUser; + +public class RegisterNewUserValidator : AbstractValidator +{ + public RegisterNewUserValidator() + { + CascadeMode = CascadeMode.Stop; + + RuleFor(x => x.Password).NotEmpty().WithMessage("Please enter the password"); + RuleFor(x => x.ConfirmPassword).NotEmpty().WithMessage("Please enter the confirmation password"); + + RuleFor(x => x).Custom((x, context) => + { + if (x.Password != x.ConfirmPassword) + { + context.AddFailure(nameof(x.Password), "Passwords should match"); + } + }); + + RuleFor(x => x.Username).NotEmpty().WithMessage("Please enter the username"); + RuleFor(x => x.FirstName).NotEmpty().WithMessage("Please enter the first name"); + RuleFor(x => x.LastName).NotEmpty().WithMessage("Please enter the last name"); + RuleFor(x => x.Email).NotEmpty().WithMessage("Please enter the last email") + .EmailAddress().WithMessage("A valid email is required"); + } +} diff --git a/src/Services/Identity/src/Identity/Identity/Models/ApplicationUser.cs b/src/Services/Identity/src/Identity/Identity/Models/ApplicationUser.cs new file mode 100644 index 0000000..55fbe95 --- /dev/null +++ b/src/Services/Identity/src/Identity/Identity/Models/ApplicationUser.cs @@ -0,0 +1,10 @@ +using Microsoft.AspNetCore.Identity; + +namespace Identity.Identity.Models; + +public class ApplicationUser : IdentityUser +{ + public string FirstName { get; set; } + public string LastName { get; set; } + public string PassPortNumber { get; set; } +} diff --git a/src/Services/Identity/src/Identity/IdentityRoot.cs b/src/Services/Identity/src/Identity/IdentityRoot.cs new file mode 100644 index 0000000..063c16b --- /dev/null +++ b/src/Services/Identity/src/Identity/IdentityRoot.cs @@ -0,0 +1,6 @@ +namespace Identity; + +public class IdentityRoot +{ + +} \ No newline at end of file diff --git a/src/Services/Identity/src/Identity/UserValidator.cs b/src/Services/Identity/src/Identity/UserValidator.cs new file mode 100644 index 0000000..37b1132 --- /dev/null +++ b/src/Services/Identity/src/Identity/UserValidator.cs @@ -0,0 +1,54 @@ +using System.Security.Claims; +using System.Threading.Tasks; +using Duende.IdentityServer.Models; +using Duende.IdentityServer.Validation; +using Identity.Identity.Models; +using Microsoft.AspNetCore.Identity; + +namespace Identity; + +public class UserValidator : IResourceOwnerPasswordValidator +{ + private readonly SignInManager _signInManager; + private readonly UserManager _userManager; + + public UserValidator(SignInManager signInManager, + UserManager userManager) + { + _signInManager = signInManager; + _userManager = userManager; + } + + public async Task ValidateAsync(ResourceOwnerPasswordValidationContext context) + { + var user = await _userManager.FindByNameAsync(context.UserName); + + var signIn = await _signInManager.PasswordSignInAsync( + user, + context.Password, + isPersistent: true, + lockoutOnFailure: true); + + if (signIn.Succeeded) + { + var userId = user!.Id.ToString(); + + // context set to success + context.Result = new GrantValidationResult( + subject: userId, + authenticationMethod: "custom", + claims: new Claim[] + { + new Claim(ClaimTypes.NameIdentifier, userId), + new Claim(ClaimTypes.Name, user.UserName) + } + ); + + return; + } + + // context set to Failure + context.Result = new GrantValidationResult( + TokenRequestErrors.UnauthorizedClient, "Invalid Credentials"); + } +} diff --git a/src/Services/Passenger/.dockerignore b/src/Services/Passenger/.dockerignore new file mode 100644 index 0000000..820e869 --- /dev/null +++ b/src/Services/Passenger/.dockerignore @@ -0,0 +1,25 @@ +**/.classpath +**/.dockerignore +**/.env +**/.git +**/.gitignore +**/.project +**/.settings +**/.toolstarget +**/.vs +**/.vscode +**/*.*proj.user +**/*.dbmdl +**/*.jfm +**/azds.yaml +**/bin/ +**/charts +**/docker-compose* +**/Dockerfile* +**/node_modules +**/npm-debug.log +**/obj/ +**/secrets.dev.yaml +**/values.dev.yaml +LICENSE +README.md \ No newline at end of file diff --git a/src/Services/Passenger/Dockerfile b/src/Services/Passenger/Dockerfile new file mode 100644 index 0000000..2964b2b --- /dev/null +++ b/src/Services/Passenger/Dockerfile @@ -0,0 +1,41 @@ +FROM mcr.microsoft.com/dotnet/sdk:6.0 AS builder +WORKDIR /app + +# Setup working directory for the project +WORKDIR /app +COPY ./src/BuildingBlocks/BuildingBlocks.csproj ./BuildingBlocks/ +COPY ./src/Services/Passenger/src/Passenger/Passenger.csproj ./Services/Passenger/src/Passenger/ +COPY ./src/Services/Passenger/src/Passenger.Api/Passenger.Api.csproj ./Services/Passenger/src/Passenger.Api/ + + +# Restore nuget packages +RUN dotnet restore ./Services/Passenger/src/Passenger.Api/Passenger.Api.csproj + +# Copy project files +COPY ./src/BuildingBlocks ./BuildingBlocks/ +COPY ./src/Services/Passenger/src/Passenger/ ./Services/Passenger/src/Passenger/ +COPY ./src/Services/Passenger/src/Passenger.Api/ ./Services/Passenger/src/Passenger.Api/ + +# Build project with Release configuration +# and no restore, as we did it already + +RUN ls +RUN dotnet build -c Release --no-restore ./Services/Passenger/src/Passenger.Api/Passenger.Api.csproj + +WORKDIR /app/Services/Passenger/src/Passenger.Api + +# 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 /app/Services/Passenger/src/Passenger.Api/out . + +ENV ASPNETCORE_URLS https://*:5012, http://*:6012 +ENV ASPNETCORE_ENVIRONMENT docker + +ENTRYPOINT ["dotnet", "Passenger.Api.dll"] + diff --git a/src/Services/Passenger/src/Passenger.Api/Passenger.Api.csproj b/src/Services/Passenger/src/Passenger.Api/Passenger.Api.csproj new file mode 100644 index 0000000..d30af3b --- /dev/null +++ b/src/Services/Passenger/src/Passenger.Api/Passenger.Api.csproj @@ -0,0 +1,12 @@ + + + + net6.0 + enable + + + + + + + diff --git a/src/Services/Passenger/src/Passenger.Api/Program.cs b/src/Services/Passenger/src/Passenger.Api/Program.cs new file mode 100644 index 0000000..c36bb70 --- /dev/null +++ b/src/Services/Passenger/src/Passenger.Api/Program.cs @@ -0,0 +1,81 @@ +using BuildingBlocks.Domain; +using BuildingBlocks.EFCore; +using BuildingBlocks.Exception; +using BuildingBlocks.IdsGenerator; +using BuildingBlocks.Jwt; +using BuildingBlocks.Logging; +using BuildingBlocks.Mapster; +using BuildingBlocks.MassTransit; +using BuildingBlocks.OpenTelemetry; +using BuildingBlocks.Swagger; +using BuildingBlocks.Utils; +using BuildingBlocks.Web; +using Figgle; +using FluentValidation; +using Hellang.Middleware.ProblemDetails; +using Microsoft.AspNetCore.Mvc.ApiExplorer; +using Passenger; +using Passenger.Data; +using Passenger.Extensions; +using Prometheus; +using Serilog; + +var builder = WebApplication.CreateBuilder(args); +var configuration = builder.Configuration; + +var appOptions = builder.Services.GetOptions("AppOptions"); +Console.WriteLine(FiggleFonts.Standard.Render(appOptions.Name)); + +builder.Services.AddCustomDbContext(configuration, typeof(PassengerRoot).Assembly); +builder.AddCustomSerilog(); +builder.Services.AddJwt(); +builder.Services.AddControllers(); +builder.Services.AddCustomSwagger(builder.Configuration, typeof(PassengerRoot).Assembly); +builder.Services.AddCustomVersioning(); +builder.Services.AddCustomMediatR(); +builder.Services.AddValidatorsFromAssembly(typeof(PassengerRoot).Assembly); +builder.Services.AddCustomProblemDetails(); +builder.Services.AddCustomMapster(typeof(PassengerRoot).Assembly); +builder.Services.AddHttpContextAccessor(); + +builder.Services.AddTransient(); +builder.Services.AddTransient(); + +builder.Services.AddCustomMassTransit(typeof(PassengerRoot).Assembly); +builder.Services.AddCustomOpenTelemetry(); +builder.Services.AddGrpc(options => +{ + options.Interceptors.Add(); +}); +builder.Services.AddMagicOnion(); + +SnowFlakIdGenerator.Configure(2); + +var app = builder.Build(); + +if (app.Environment.IsDevelopment()) +{ + var provider = app.Services.GetService(); + app.UseCustomSwagger(provider); +} + +app.UseSerilogRequestLogging(); +app.UseMigrations(); +app.UseCorrelationId(); +app.UseRouting(); +app.UseHttpMetrics(); +app.UseProblemDetails(); +app.UseHttpsRedirection(); +app.UseAuthentication(); +app.UseAuthorization(); + +app.UseEndpoints(endpoints => +{ + endpoints.MapControllers(); + endpoints.MapMetrics(); + endpoints.MapMagicOnionService(); +}); + +app.MapGet("/", x => x.Response.WriteAsync(appOptions.Name)); + +app.Run(); diff --git a/src/Services/Passenger/src/Passenger.Api/Properties/launchSettings.json b/src/Services/Passenger/src/Passenger.Api/Properties/launchSettings.json new file mode 100644 index 0000000..5609b39 --- /dev/null +++ b/src/Services/Passenger/src/Passenger.Api/Properties/launchSettings.json @@ -0,0 +1,23 @@ +{ + "$schema": "https://json.schemastore.org/launchsettings.json", + "iisSettings": { + "windowsAuthentication": false, + "anonymousAuthentication": true, + "iisExpress": { + "applicationUrl": "http://localhost:32326", + "sslPort": 44374 + } + }, + "profiles": { + "Passenger.Api": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "launchUrl": "swagger", + "applicationUrl": "https://localhost:5012;http://localhost:6012", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} diff --git a/src/Services/Passenger/src/Passenger.Api/appsettings.Development.json b/src/Services/Passenger/src/Passenger.Api/appsettings.Development.json new file mode 100644 index 0000000..0db3279 --- /dev/null +++ b/src/Services/Passenger/src/Passenger.Api/appsettings.Development.json @@ -0,0 +1,3 @@ +{ + +} diff --git a/src/Services/Passenger/src/Passenger.Api/appsettings.docker.json b/src/Services/Passenger/src/Passenger.Api/appsettings.docker.json new file mode 100644 index 0000000..94eac04 --- /dev/null +++ b/src/Services/Passenger/src/Passenger.Api/appsettings.docker.json @@ -0,0 +1,17 @@ +{ + "App": "Passenger-Service", + "ConnectionStrings": { + "PassengerConnection": "Server=db;Database=PassengerDB;User ID=sa;Password=@Aa123456" + }, + "Jwt": { + "Authority": "https://localhost:5005", + "Audience": "passenger-api" + }, + "RabbitMq": { + "HostName": "rabbitmq", + "ExchangeName": "passenger", + "UserName": "guest", + "Password": "guest" + }, + "AllowedHosts": "*" +} diff --git a/src/Services/Passenger/src/Passenger.Api/appsettings.json b/src/Services/Passenger/src/Passenger.Api/appsettings.json new file mode 100644 index 0000000..d34c659 --- /dev/null +++ b/src/Services/Passenger/src/Passenger.Api/appsettings.json @@ -0,0 +1,28 @@ +{ + "AppOptions": { + "Name": "Passenger-Service" + }, + "ConnectionStrings": { + "DefaultConnection": "Server=.\\sqlexpress;Database=PassengerDB;Trusted_Connection=True;MultipleActiveResultSets=true" + }, + "Jwt": { + "Authority": "https://localhost:5005", + "Audience": "passenger-api" + }, + "RabbitMq": { + "HostName": "localhost", + "ExchangeName": "passenger", + "UserName": "guest", + "Password": "guest" + }, + "Serilog": { + "MinimumLevel": { + "Default": "Information", + "Override": { + "Microsoft": "Warning", + "Microsoft.Hosting.Lifetime": "Information" + } + } + }, + "AllowedHosts": "*" +} diff --git a/src/Services/Passenger/src/Passenger/Data/Configurations/PassengerConfiguration.cs b/src/Services/Passenger/src/Passenger/Data/Configurations/PassengerConfiguration.cs new file mode 100644 index 0000000..5899f26 --- /dev/null +++ b/src/Services/Passenger/src/Passenger/Data/Configurations/PassengerConfiguration.cs @@ -0,0 +1,15 @@ +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; + +namespace Passenger.Data.Configurations; + +public class PassengerConfiguration: IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder builder) + { + builder.ToTable("Passenger", "dbo"); + + builder.HasKey(r => r.Id); + builder.Property(r => r.Id).ValueGeneratedNever(); + } +} \ No newline at end of file diff --git a/src/Services/Passenger/src/Passenger/Data/DesignTimeDbContextFactory.cs b/src/Services/Passenger/src/Passenger/Data/DesignTimeDbContextFactory.cs new file mode 100644 index 0000000..92da9b2 --- /dev/null +++ b/src/Services/Passenger/src/Passenger/Data/DesignTimeDbContextFactory.cs @@ -0,0 +1,16 @@ +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Design; + +namespace Passenger.Data; + +public class DesignTimeDbContextFactory: IDesignTimeDbContextFactory +{ + public PassengerDbContext CreateDbContext(string[] args) + { + var builder = new DbContextOptionsBuilder(); + + builder.UseSqlServer( + "Data Source=.\\sqlexpress;Initial Catalog=PassengerDB;Persist Security Info=False;Integrated Security=SSPI"); + return new PassengerDbContext(builder.Options, null); + } +} diff --git a/src/Services/Passenger/src/Passenger/Data/Migrations/20220309230648_initial.Designer.cs b/src/Services/Passenger/src/Passenger/Data/Migrations/20220309230648_initial.Designer.cs new file mode 100644 index 0000000..49f2c4f --- /dev/null +++ b/src/Services/Passenger/src/Passenger/Data/Migrations/20220309230648_initial.Designer.cs @@ -0,0 +1,98 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Passenger.Data; + +#nullable disable + +namespace Passenger.Data.Migrations +{ + [DbContext(typeof(PassengerDbContext))] + [Migration("20220309230648_initial")] + partial class initial + { + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "6.0.1") + .HasAnnotation("Relational:MaxIdentifierLength", 128); + + SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder, 1L, 1); + + modelBuilder.Entity("BuildingBlocks.Outbox.OutboxMessage", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("CorrelationId") + .HasColumnType("uniqueidentifier"); + + b.Property("Data") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("EventType") + .IsRequired() + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("OccurredOn") + .HasColumnType("datetime2"); + + b.Property("ProcessedOn") + .HasColumnType("datetime2"); + + b.Property("Type") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("OutboxMessages", (string)null); + }); + + modelBuilder.Entity("Passenger.Passenger.Models.Passenger", b => + { + b.Property("Id") + .HasColumnType("bigint"); + + b.Property("Age") + .HasColumnType("int"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("LastModified") + .HasColumnType("datetime2"); + + b.Property("ModifiedBy") + .HasColumnType("int"); + + b.Property("Name") + .HasColumnType("nvarchar(max)"); + + b.Property("PassengerType") + .HasColumnType("int"); + + b.Property("PassportNumber") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("Passenger", "dbo"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/Services/Passenger/src/Passenger/Data/Migrations/20220309230648_initial.cs b/src/Services/Passenger/src/Passenger/Data/Migrations/20220309230648_initial.cs new file mode 100644 index 0000000..9485856 --- /dev/null +++ b/src/Services/Passenger/src/Passenger/Data/Migrations/20220309230648_initial.cs @@ -0,0 +1,63 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Passenger.Data.Migrations +{ + public partial class initial : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.EnsureSchema( + name: "dbo"); + + migrationBuilder.CreateTable( + name: "OutboxMessages", + columns: table => new + { + Id = table.Column(type: "uniqueidentifier", nullable: false), + Name = table.Column(type: "nvarchar(max)", nullable: false), + OccurredOn = table.Column(type: "datetime2", nullable: false), + Type = table.Column(type: "nvarchar(max)", nullable: false), + Data = table.Column(type: "nvarchar(max)", nullable: false), + ProcessedOn = table.Column(type: "datetime2", nullable: true), + EventType = table.Column(type: "varchar(50)", unicode: false, maxLength: 50, nullable: false), + CorrelationId = table.Column(type: "uniqueidentifier", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_OutboxMessages", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "Passenger", + schema: "dbo", + columns: table => new + { + Id = table.Column(type: "bigint", nullable: false), + PassportNumber = table.Column(type: "nvarchar(max)", nullable: true), + Name = table.Column(type: "nvarchar(max)", nullable: true), + PassengerType = table.Column(type: "int", nullable: false), + Age = table.Column(type: "int", nullable: false), + LastModified = table.Column(type: "datetime2", nullable: false), + IsDeleted = table.Column(type: "bit", nullable: false), + ModifiedBy = table.Column(type: "int", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_Passenger", x => x.Id); + }); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "OutboxMessages"); + + migrationBuilder.DropTable( + name: "Passenger", + schema: "dbo"); + } + } +} diff --git a/src/Services/Passenger/src/Passenger/Data/Migrations/20220416172933_Audit.Designer.cs b/src/Services/Passenger/src/Passenger/Data/Migrations/20220416172933_Audit.Designer.cs new file mode 100644 index 0000000..7398d42 --- /dev/null +++ b/src/Services/Passenger/src/Passenger/Data/Migrations/20220416172933_Audit.Designer.cs @@ -0,0 +1,107 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Passenger.Data; + +#nullable disable + +namespace Passenger.Data.Migrations +{ + [DbContext(typeof(PassengerDbContext))] + [Migration("20220416172933_Audit")] + partial class Audit + { + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "6.0.1") + .HasAnnotation("Relational:MaxIdentifierLength", 128); + + SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder, 1L, 1); + + modelBuilder.Entity("BuildingBlocks.Outbox.OutboxMessage", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("CorrelationId") + .HasColumnType("uniqueidentifier"); + + b.Property("Data") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("EventType") + .IsRequired() + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("OccurredOn") + .HasColumnType("datetime2"); + + b.Property("ProcessedOn") + .HasColumnType("datetime2"); + + b.Property("Type") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("OutboxMessages", (string)null); + }); + + modelBuilder.Entity("Passenger.Passengers.Models.Passenger", b => + { + b.Property("Id") + .HasColumnType("bigint"); + + b.Property("Age") + .HasColumnType("int"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("int"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("LastModified") + .HasColumnType("datetime2"); + + b.Property("LastModifiedBy") + .HasColumnType("int"); + + b.Property("Name") + .HasColumnType("nvarchar(max)"); + + b.Property("PassengerType") + .HasColumnType("int"); + + b.Property("PassportNumber") + .HasColumnType("nvarchar(max)"); + + b.Property("Version") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.ToTable("Passenger", "dbo"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/Services/Passenger/src/Passenger/Data/Migrations/20220416172933_Audit.cs b/src/Services/Passenger/src/Passenger/Data/Migrations/20220416172933_Audit.cs new file mode 100644 index 0000000..7cd1aaa --- /dev/null +++ b/src/Services/Passenger/src/Passenger/Data/Migrations/20220416172933_Audit.cs @@ -0,0 +1,85 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Passenger.Data.Migrations +{ + public partial class Audit : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.RenameColumn( + name: "ModifiedBy", + schema: "dbo", + table: "Passenger", + newName: "LastModifiedBy"); + + migrationBuilder.AlterColumn( + name: "LastModified", + schema: "dbo", + table: "Passenger", + type: "datetime2", + nullable: true, + oldClrType: typeof(DateTime), + oldType: "datetime2"); + + migrationBuilder.AddColumn( + name: "CreatedAt", + schema: "dbo", + table: "Passenger", + type: "datetime2", + nullable: true); + + migrationBuilder.AddColumn( + name: "CreatedBy", + schema: "dbo", + table: "Passenger", + type: "int", + nullable: true); + + migrationBuilder.AddColumn( + name: "Version", + schema: "dbo", + table: "Passenger", + type: "bigint", + nullable: false, + defaultValue: 0L); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "CreatedAt", + schema: "dbo", + table: "Passenger"); + + migrationBuilder.DropColumn( + name: "CreatedBy", + schema: "dbo", + table: "Passenger"); + + migrationBuilder.DropColumn( + name: "Version", + schema: "dbo", + table: "Passenger"); + + migrationBuilder.RenameColumn( + name: "LastModifiedBy", + schema: "dbo", + table: "Passenger", + newName: "ModifiedBy"); + + migrationBuilder.AlterColumn( + name: "LastModified", + schema: "dbo", + table: "Passenger", + type: "datetime2", + nullable: false, + defaultValue: new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), + oldClrType: typeof(DateTime), + oldType: "datetime2", + oldNullable: true); + } + } +} diff --git a/src/Services/Passenger/src/Passenger/Data/Migrations/20220422175724_Update-EventId.Designer.cs b/src/Services/Passenger/src/Passenger/Data/Migrations/20220422175724_Update-EventId.Designer.cs new file mode 100644 index 0000000..0628c6a --- /dev/null +++ b/src/Services/Passenger/src/Passenger/Data/Migrations/20220422175724_Update-EventId.Designer.cs @@ -0,0 +1,139 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Passenger.Data; + +#nullable disable + +namespace Passenger.Data.Migrations +{ + [DbContext(typeof(PassengerDbContext))] + [Migration("20220422175724_Update-EventId")] + partial class UpdateEventId + { + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "6.0.1") + .HasAnnotation("Relational:MaxIdentifierLength", 128); + + SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder, 1L, 1); + + modelBuilder.Entity("BuildingBlocks.InternalProcessor.InternalMessage", b => + { + b.Property("EventId") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("CommandType") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("CorrelationId") + .HasColumnType("uniqueidentifier"); + + b.Property("Data") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("OccurredOn") + .HasColumnType("datetime2"); + + b.Property("ProcessedOn") + .HasColumnType("datetime2"); + + b.HasKey("EventId"); + + b.ToTable("InternalMessages", (string)null); + }); + + modelBuilder.Entity("BuildingBlocks.Outbox.OutboxMessage", b => + { + b.Property("EventId") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("CorrelationId") + .HasColumnType("uniqueidentifier"); + + b.Property("Data") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("EventType") + .IsRequired() + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("OccurredOn") + .HasColumnType("datetime2"); + + b.Property("ProcessedOn") + .HasColumnType("datetime2"); + + b.Property("Type") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("EventId"); + + b.ToTable("OutboxMessages", (string)null); + }); + + modelBuilder.Entity("Passenger.Passengers.Models.Passenger", b => + { + b.Property("Id") + .HasColumnType("bigint"); + + b.Property("Age") + .HasColumnType("int"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("bigint"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("LastModified") + .HasColumnType("datetime2"); + + b.Property("LastModifiedBy") + .HasColumnType("bigint"); + + b.Property("Name") + .HasColumnType("nvarchar(max)"); + + b.Property("PassengerType") + .HasColumnType("int"); + + b.Property("PassportNumber") + .HasColumnType("nvarchar(max)"); + + b.Property("Version") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.ToTable("Passenger", "dbo"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/Services/Passenger/src/Passenger/Data/Migrations/20220422175724_Update-EventId.cs b/src/Services/Passenger/src/Passenger/Data/Migrations/20220422175724_Update-EventId.cs new file mode 100644 index 0000000..39709fb --- /dev/null +++ b/src/Services/Passenger/src/Passenger/Data/Migrations/20220422175724_Update-EventId.cs @@ -0,0 +1,86 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Passenger.Data.Migrations +{ + public partial class UpdateEventId : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.RenameColumn( + name: "Id", + table: "OutboxMessages", + newName: "EventId"); + + migrationBuilder.AlterColumn( + name: "LastModifiedBy", + schema: "dbo", + table: "Passenger", + type: "bigint", + nullable: true, + oldClrType: typeof(int), + oldType: "int", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "CreatedBy", + schema: "dbo", + table: "Passenger", + type: "bigint", + nullable: true, + oldClrType: typeof(int), + oldType: "int", + oldNullable: true); + + migrationBuilder.CreateTable( + name: "InternalMessages", + columns: table => new + { + EventId = table.Column(type: "uniqueidentifier", nullable: false), + Name = table.Column(type: "nvarchar(max)", nullable: false), + OccurredOn = table.Column(type: "datetime2", nullable: false), + CommandType = table.Column(type: "nvarchar(max)", nullable: false), + Data = table.Column(type: "nvarchar(max)", nullable: false), + ProcessedOn = table.Column(type: "datetime2", nullable: true), + CorrelationId = table.Column(type: "uniqueidentifier", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_InternalMessages", x => x.EventId); + }); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "InternalMessages"); + + migrationBuilder.RenameColumn( + name: "EventId", + table: "OutboxMessages", + newName: "Id"); + + migrationBuilder.AlterColumn( + name: "LastModifiedBy", + schema: "dbo", + table: "Passenger", + type: "int", + nullable: true, + oldClrType: typeof(long), + oldType: "bigint", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "CreatedBy", + schema: "dbo", + table: "Passenger", + type: "int", + nullable: true, + oldClrType: typeof(long), + oldType: "bigint", + oldNullable: true); + } + } +} diff --git a/src/Services/Passenger/src/Passenger/Data/Migrations/PassengerDbContextModelSnapshot.cs b/src/Services/Passenger/src/Passenger/Data/Migrations/PassengerDbContextModelSnapshot.cs new file mode 100644 index 0000000..fb7dd22 --- /dev/null +++ b/src/Services/Passenger/src/Passenger/Data/Migrations/PassengerDbContextModelSnapshot.cs @@ -0,0 +1,137 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Passenger.Data; + +#nullable disable + +namespace Passenger.Data.Migrations +{ + [DbContext(typeof(PassengerDbContext))] + partial class PassengerDbContextModelSnapshot : ModelSnapshot + { + protected override void BuildModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "6.0.1") + .HasAnnotation("Relational:MaxIdentifierLength", 128); + + SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder, 1L, 1); + + modelBuilder.Entity("BuildingBlocks.InternalProcessor.InternalMessage", b => + { + b.Property("EventId") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("CommandType") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("CorrelationId") + .HasColumnType("uniqueidentifier"); + + b.Property("Data") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("OccurredOn") + .HasColumnType("datetime2"); + + b.Property("ProcessedOn") + .HasColumnType("datetime2"); + + b.HasKey("EventId"); + + b.ToTable("InternalMessages", (string)null); + }); + + modelBuilder.Entity("BuildingBlocks.Outbox.OutboxMessage", b => + { + b.Property("EventId") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("CorrelationId") + .HasColumnType("uniqueidentifier"); + + b.Property("Data") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("EventType") + .IsRequired() + .HasMaxLength(50) + .IsUnicode(false) + .HasColumnType("varchar(50)"); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("OccurredOn") + .HasColumnType("datetime2"); + + b.Property("ProcessedOn") + .HasColumnType("datetime2"); + + b.Property("Type") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("EventId"); + + b.ToTable("OutboxMessages", (string)null); + }); + + modelBuilder.Entity("Passenger.Passengers.Models.Passenger", b => + { + b.Property("Id") + .HasColumnType("bigint"); + + b.Property("Age") + .HasColumnType("int"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("bigint"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("LastModified") + .HasColumnType("datetime2"); + + b.Property("LastModifiedBy") + .HasColumnType("bigint"); + + b.Property("Name") + .HasColumnType("nvarchar(max)"); + + b.Property("PassengerType") + .HasColumnType("int"); + + b.Property("PassportNumber") + .HasColumnType("nvarchar(max)"); + + b.Property("Version") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.ToTable("Passenger", "dbo"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/Services/Passenger/src/Passenger/Data/PassengerDbContext.cs b/src/Services/Passenger/src/Passenger/Data/PassengerDbContext.cs new file mode 100644 index 0000000..fc202b0 --- /dev/null +++ b/src/Services/Passenger/src/Passenger/Data/PassengerDbContext.cs @@ -0,0 +1,21 @@ +using System.Reflection; +using BuildingBlocks.EFCore; +using Microsoft.AspNetCore.Http; +using Microsoft.EntityFrameworkCore; + +namespace Passenger.Data; + +public sealed class PassengerDbContext : AppDbContextBase +{ + public PassengerDbContext(DbContextOptions options, IHttpContextAccessor httpContextAccessor) : base(options, httpContextAccessor) + { + } + + public DbSet Passengers => Set(); + + protected override void OnModelCreating(ModelBuilder builder) + { + builder.ApplyConfigurationsFromAssembly(Assembly.GetExecutingAssembly()); + base.OnModelCreating(builder); + } +} diff --git a/src/Services/Passenger/src/Passenger/Data/readme.md b/src/Services/Passenger/src/Passenger/Data/readme.md new file mode 100644 index 0000000..97deb62 --- /dev/null +++ b/src/Services/Passenger/src/Passenger/Data/readme.md @@ -0,0 +1,2 @@ +dotnet ef migrations add initial --context PassengerDbContext -o "Data\Migrations" +dotnet ef database update --context PassengerDbContext diff --git a/src/Services/Passenger/src/Passenger/EventMapper.cs b/src/Services/Passenger/src/Passenger/EventMapper.cs new file mode 100644 index 0000000..11bb213 --- /dev/null +++ b/src/Services/Passenger/src/Passenger/EventMapper.cs @@ -0,0 +1,20 @@ +using BuildingBlocks.Domain; +using BuildingBlocks.Domain.Event; + +namespace Passenger; + +public sealed class EventMapper : IEventMapper +{ + public IEnumerable MapAll(IEnumerable events) + { + return events.Select(Map); + } + + public IIntegrationEvent Map(IDomainEvent @event) + { + return @event switch + { + _ => null + }; + } +} \ No newline at end of file diff --git a/src/Services/Passenger/src/Passenger/Extensions/MediatRExtensions.cs b/src/Services/Passenger/src/Passenger/Extensions/MediatRExtensions.cs new file mode 100644 index 0000000..53d3f31 --- /dev/null +++ b/src/Services/Passenger/src/Passenger/Extensions/MediatRExtensions.cs @@ -0,0 +1,20 @@ +using BuildingBlocks.EFCore; +using BuildingBlocks.Logging; +using BuildingBlocks.Validation; +using MediatR; +using Microsoft.Extensions.DependencyInjection; + +namespace Passenger.Extensions; + +public static class MediatRExtensions +{ + public static IServiceCollection AddCustomMediatR(this IServiceCollection services) + { + services.AddMediatR(typeof(PassengerRoot).Assembly); + services.AddScoped(typeof(IPipelineBehavior<,>), typeof(ValidationBehavior<,>)); + services.AddScoped(typeof(IPipelineBehavior<,>), typeof(LoggingBehavior<,>)); + services.AddScoped(typeof(IPipelineBehavior<,>), typeof(EfTxBehavior<,>)); + + return services; + } +} diff --git a/src/Services/Passenger/src/Passenger/Extensions/MigrationsExtensions.cs b/src/Services/Passenger/src/Passenger/Extensions/MigrationsExtensions.cs new file mode 100644 index 0000000..0a83285 --- /dev/null +++ b/src/Services/Passenger/src/Passenger/Extensions/MigrationsExtensions.cs @@ -0,0 +1,36 @@ +using BuildingBlocks.EFCore; +using Microsoft.AspNetCore.Builder; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.DependencyInjection; +using Passenger.Data; + +namespace Passenger.Extensions; + +public static class MigrationsExtensions +{ + public static IApplicationBuilder UseMigrations(this IApplicationBuilder app) + { + MigrateDatabase(app.ApplicationServices); + SeedData(app.ApplicationServices); + + return app; + } + + private static void MigrateDatabase(IServiceProvider serviceProvider) + { + using var scope = serviceProvider.CreateScope(); + + var context = scope.ServiceProvider.GetRequiredService(); + context.Database.Migrate(); + } + + private static void SeedData(IServiceProvider serviceProvider) + { + using var scope = serviceProvider.CreateScope(); + var seeders = scope.ServiceProvider.GetServices(); + foreach (var seeder in seeders) + { + seeder.SeedAllAsync().GetAwaiter().GetResult(); + } + } +} diff --git a/src/Services/Passenger/src/Passenger/Extensions/ProblemDetailsExtensions.cs b/src/Services/Passenger/src/Passenger/Extensions/ProblemDetailsExtensions.cs new file mode 100644 index 0000000..176f607 --- /dev/null +++ b/src/Services/Passenger/src/Passenger/Extensions/ProblemDetailsExtensions.cs @@ -0,0 +1,84 @@ +using BuildingBlocks.Exception; +using Hellang.Middleware.ProblemDetails; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Newtonsoft.Json; + +namespace Passenger.Extensions; + +public static class ProblemDetailsExtensions +{ + public static IServiceCollection AddCustomProblemDetails(this IServiceCollection services) + { + services.AddProblemDetails(x => + { + // Control when an exception is included + x.IncludeExceptionDetails = (ctx, _) => + { + // Fetch services from HttpContext.RequestServices + var env = ctx.RequestServices.GetRequiredService(); + return env.IsDevelopment() || env.IsStaging(); + }; + x.Map(ex => new ProblemDetails + { + Title = "Application rule broken", + Status = StatusCodes.Status409Conflict, + Detail = ex.Message, + Type = "https://somedomain/application-rule-validation-error" + }); + + // Exception will produce and returns from our FluentValidation RequestValidationBehavior + x.Map(ex => new ProblemDetails + { + Title = "input validation rules broken", + Status = StatusCodes.Status400BadRequest, + Detail = JsonConvert.SerializeObject(ex.ValidationResultModel.Errors), + Type = "https://somedomain/input-validation-rules-error" + }); + x.Map(ex => new ProblemDetails + { + Title = "bad request exception", + Status = StatusCodes.Status400BadRequest, + Detail = ex.Message, + Type = "https://somedomain/bad-request-error" + }); + x.Map(ex => new ProblemDetails + { + Title = "not found exception", + Status = StatusCodes.Status404NotFound, + Detail = ex.Message, + Type = "https://somedomain/not-found-error" + }); + x.Map(ex => new ProblemDetails + { + Title = "api server exception", + Status = StatusCodes.Status500InternalServerError, + Detail = ex.Message, + Type = "https://somedomain/api-server-error" + }); + x.Map(ex => new ProblemDetails + { + Title = "application exception", + Status = StatusCodes.Status500InternalServerError, + Detail = ex.Message, + Type = "https://somedomain/application-error" + }); + x.Map(ex => + { + var pd = new ProblemDetails + { + Status = (int)ex.StatusCode, + Title = "identity exception", + Detail = ex.Message, + Type = "https://somedomain/identity-error" + }; + + return pd; + }); + x.MapToStatusCode(StatusCodes.Status400BadRequest); + }); + return services; + } +} \ No newline at end of file diff --git a/src/Services/Passenger/src/Passenger/GrpcServer/PassengerGrpcService.cs b/src/Services/Passenger/src/Passenger/GrpcServer/PassengerGrpcService.cs new file mode 100644 index 0000000..3edd30f --- /dev/null +++ b/src/Services/Passenger/src/Passenger/GrpcServer/PassengerGrpcService.cs @@ -0,0 +1,24 @@ +using BuildingBlocks.Contracts.Grpc; +using MagicOnion; +using MagicOnion.Server; +using Mapster; +using MediatR; +using Passenger.Passengers.Features.GetPassengerById; + +namespace Passenger.GrpcServer; + +public class PassengerGrpcService : ServiceBase, IPassengerGrpcService +{ + private readonly IMediator _mediator; + + public PassengerGrpcService(IMediator mediator) + { + _mediator = mediator; + } + + public async UnaryResult GetById(long id) + { + var result = await _mediator.Send(new GetPassengerQueryById(id)); + return result.Adapt(); + } +} diff --git a/src/Services/Passenger/src/Passenger/Identity/RegisterNewUser/RegisterNewUserConsumerHandler.cs b/src/Services/Passenger/src/Passenger/Identity/RegisterNewUser/RegisterNewUserConsumerHandler.cs new file mode 100644 index 0000000..c754c1c --- /dev/null +++ b/src/Services/Passenger/src/Passenger/Identity/RegisterNewUser/RegisterNewUserConsumerHandler.cs @@ -0,0 +1,28 @@ +using Ardalis.GuardClauses; +using BuildingBlocks.Contracts.EventBus.Messages; +using BuildingBlocks.IdsGenerator; +using MassTransit; +using Passenger.Data; + +namespace Passenger.Identity.RegisterNewUser; + +public class RegisterNewUserConsumerHandler : IConsumer +{ + private readonly PassengerDbContext _passengerDbContext; + + public RegisterNewUserConsumerHandler(PassengerDbContext passengerDbContext) + { + _passengerDbContext = passengerDbContext; + } + + public async Task Consume(ConsumeContext context) + { + Guard.Against.Null(context.Message, nameof(UserCreated)); + + var passenger = Passengers.Models.Passenger.Create(context.Message.Id, context.Message.Name, context.Message.PassportNumber); + + await _passengerDbContext.AddAsync(passenger); + + await _passengerDbContext.SaveChangesAsync(); + } +} diff --git a/src/Services/Passenger/src/Passenger/Passenger.csproj b/src/Services/Passenger/src/Passenger/Passenger.csproj new file mode 100644 index 0000000..487d10d --- /dev/null +++ b/src/Services/Passenger/src/Passenger/Passenger.csproj @@ -0,0 +1,24 @@ + + + + net6.0 + enable + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + + + + + diff --git a/src/Services/Passenger/src/Passenger/PassengerRoot.cs b/src/Services/Passenger/src/Passenger/PassengerRoot.cs new file mode 100644 index 0000000..003d77e --- /dev/null +++ b/src/Services/Passenger/src/Passenger/PassengerRoot.cs @@ -0,0 +1,6 @@ +namespace Passenger; + +public class PassengerRoot +{ + +} \ No newline at end of file diff --git a/src/Services/Passenger/src/Passenger/Passengers/Dtos/PassengerResponseDto.cs b/src/Services/Passenger/src/Passenger/Passengers/Dtos/PassengerResponseDto.cs new file mode 100644 index 0000000..4c3826c --- /dev/null +++ b/src/Services/Passenger/src/Passenger/Passengers/Dtos/PassengerResponseDto.cs @@ -0,0 +1,12 @@ +using Passenger.Passengers.Models; + +namespace Passenger.Passengers.Dtos; + +public record PassengerResponseDto +{ + public long Id { get; init; } + public string Name { get; init; } + public string PassportNumber { get; init; } + public PassengerType PassengerType { get; init; } + public int Age { get; init; } +} diff --git a/src/Services/Passenger/src/Passenger/Passengers/Events/Domain/PassengerCreatedDomainEvent.cs b/src/Services/Passenger/src/Passenger/Passengers/Events/Domain/PassengerCreatedDomainEvent.cs new file mode 100644 index 0000000..cddd5af --- /dev/null +++ b/src/Services/Passenger/src/Passenger/Passengers/Events/Domain/PassengerCreatedDomainEvent.cs @@ -0,0 +1,5 @@ +using BuildingBlocks.Domain.Event; + +namespace Passenger.Passengers.Events.Domain; + +public record PassengerCreatedDomainEvent(string FlightNumber) : IDomainEvent; diff --git a/src/Services/Passenger/src/Passenger/Passengers/Exceptions/PassengerAlreadyExist.cs b/src/Services/Passenger/src/Passenger/Passengers/Exceptions/PassengerAlreadyExist.cs new file mode 100644 index 0000000..6bed359 --- /dev/null +++ b/src/Services/Passenger/src/Passenger/Passengers/Exceptions/PassengerAlreadyExist.cs @@ -0,0 +1,10 @@ +using BuildingBlocks.Exception; + +namespace Passenger.Passengers.Exceptions; + +public class PassengerNotExist : BadRequestException +{ + public PassengerNotExist(string code = default) : base("Please register before!") + { + } +} diff --git a/src/Services/Passenger/src/Passenger/Passengers/Exceptions/PassengerNotFoundException.cs b/src/Services/Passenger/src/Passenger/Passengers/Exceptions/PassengerNotFoundException.cs new file mode 100644 index 0000000..2c3f95d --- /dev/null +++ b/src/Services/Passenger/src/Passenger/Passengers/Exceptions/PassengerNotFoundException.cs @@ -0,0 +1,10 @@ +using BuildingBlocks.Exception; + +namespace Passenger.Passengers.Exceptions; + +public class PassengerNotFoundException: NotFoundException +{ + public PassengerNotFoundException(string code = default) : base("Passenger not found!") + { + } +} diff --git a/src/Services/Passenger/src/Passenger/Passengers/Features/CompleteRegisterPassenger/CompleteRegisterPassengerCommand.cs b/src/Services/Passenger/src/Passenger/Passengers/Features/CompleteRegisterPassenger/CompleteRegisterPassengerCommand.cs new file mode 100644 index 0000000..556f53a --- /dev/null +++ b/src/Services/Passenger/src/Passenger/Passengers/Features/CompleteRegisterPassenger/CompleteRegisterPassengerCommand.cs @@ -0,0 +1,8 @@ +using MediatR; +using Passenger.Passengers.Dtos; +using Passenger.Passengers.Models; + +namespace Passenger.Passengers.Features.CompleteRegisterPassenger; + +public record CompleteRegisterPassengerCommand + (string PassportNumber, PassengerType PassengerType, int Age) : IRequest; diff --git a/src/Services/Passenger/src/Passenger/Passengers/Features/CompleteRegisterPassenger/CompleteRegisterPassengerCommandHandler.cs b/src/Services/Passenger/src/Passenger/Passengers/Features/CompleteRegisterPassenger/CompleteRegisterPassengerCommandHandler.cs new file mode 100644 index 0000000..0e5284f --- /dev/null +++ b/src/Services/Passenger/src/Passenger/Passengers/Features/CompleteRegisterPassenger/CompleteRegisterPassengerCommandHandler.cs @@ -0,0 +1,39 @@ +using Ardalis.GuardClauses; +using MapsterMapper; +using MediatR; +using Microsoft.EntityFrameworkCore; +using Passenger.Data; +using Passenger.Passengers.Dtos; +using Passenger.Passengers.Exceptions; + +namespace Passenger.Passengers.Features.CompleteRegisterPassenger; + +public class CompleteRegisterPassengerCommandHandler : IRequestHandler +{ + private readonly IMapper _mapper; + private readonly PassengerDbContext _passengerDbContext; + + public CompleteRegisterPassengerCommandHandler(IMapper mapper, PassengerDbContext passengerDbContext) + { + _mapper = mapper; + _passengerDbContext = passengerDbContext; + } + + public async Task Handle(CompleteRegisterPassengerCommand command, CancellationToken cancellationToken) + { + Guard.Against.Null(command, nameof(command)); + + var passenger = await _passengerDbContext.Passengers.AsNoTracking().SingleOrDefaultAsync( + x => x.PassportNumber == command.PassportNumber, + cancellationToken); + + if (passenger is null) + throw new PassengerNotExist(); + + var passengerEntity = passenger.CompleteRegistrationPassenger(passenger.Id, passenger.Name, passenger.PassportNumber, command.PassengerType, command.Age); + + var updatePassenger = _passengerDbContext.Passengers.Update(passengerEntity); + + return _mapper.Map(updatePassenger.Entity); + } +} diff --git a/src/Services/Passenger/src/Passenger/Passengers/Features/CompleteRegisterPassenger/CompleteRegisterPassengerCommandValidator.cs b/src/Services/Passenger/src/Passenger/Passengers/Features/CompleteRegisterPassenger/CompleteRegisterPassengerCommandValidator.cs new file mode 100644 index 0000000..f296b10 --- /dev/null +++ b/src/Services/Passenger/src/Passenger/Passengers/Features/CompleteRegisterPassenger/CompleteRegisterPassengerCommandValidator.cs @@ -0,0 +1,21 @@ +using FluentValidation; +using Passenger.Passengers.Models; + +namespace Passenger.Passengers.Features.CompleteRegisterPassenger; + +public class CompleteRegisterPassengerCommandValidator : AbstractValidator +{ + public CompleteRegisterPassengerCommandValidator() + { + CascadeMode = CascadeMode.Stop; + + RuleFor(x => x.PassportNumber).NotNull().WithMessage("The PassportNumber is required!"); + RuleFor(x => x.Age).GreaterThan(0).WithMessage("The Age must be greater than 0!"); + RuleFor(x => x.PassengerType).Must(p => p.GetType().IsEnum && + p == PassengerType.Baby || + p == PassengerType.Female || + p == PassengerType.Male || + p == PassengerType.Unknown) + .WithMessage("PassengerType must be Male, Female, Baby or Unknown"); + } +} diff --git a/src/Services/Passenger/src/Passenger/Passengers/Features/CompleteRegisterPassenger/CompleteRegisterPassengerEndpoint.cs b/src/Services/Passenger/src/Passenger/Passengers/Features/CompleteRegisterPassenger/CompleteRegisterPassengerEndpoint.cs new file mode 100644 index 0000000..75fc722 --- /dev/null +++ b/src/Services/Passenger/src/Passenger/Passengers/Features/CompleteRegisterPassenger/CompleteRegisterPassengerEndpoint.cs @@ -0,0 +1,24 @@ +using BuildingBlocks.Web; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Swashbuckle.AspNetCore.Annotations; + +namespace Passenger.Passengers.Features.CompleteRegisterPassenger; + +[Route(BaseApiPath + "/passenger/complete-registration")] +public class CompleteRegisterPassengerEndpoint : BaseController +{ + [Authorize] + [HttpPost] + [ProducesResponseType(StatusCodes.Status201Created)] + [ProducesResponseType(StatusCodes.Status400BadRequest)] + [SwaggerOperation(Summary = "Complete Register Passenger", Description = "Complete Register Passenger")] + public async Task CompleteRegisterPassenger([FromBody] CompleteRegisterPassengerCommand command, + CancellationToken cancellationToken) + { + var result = await Mediator.Send(command, cancellationToken); + + return Ok(result); + } +} diff --git a/src/Services/Passenger/src/Passenger/Passengers/Features/GetPassengerById/GetPassengerByIdEndpoint.cs b/src/Services/Passenger/src/Passenger/Passengers/Features/GetPassengerById/GetPassengerByIdEndpoint.cs new file mode 100644 index 0000000..c813a32 --- /dev/null +++ b/src/Services/Passenger/src/Passenger/Passengers/Features/GetPassengerById/GetPassengerByIdEndpoint.cs @@ -0,0 +1,23 @@ +using BuildingBlocks.Web; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Swashbuckle.AspNetCore.Annotations; + +namespace Passenger.Passengers.Features.GetPassengerById; + +[Route(BaseApiPath + "/passenger")] +public class GetPassengerByIdEndpoint : BaseController +{ + [HttpGet("{id}")] + [Authorize] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status400BadRequest)] + [SwaggerOperation(Summary = "Get passenger by id", Description = "Get passenger by id")] + public async Task GetById([FromRoute] GetPassengerQueryById query, CancellationToken cancellationToken) + { + var result = await Mediator.Send(query, cancellationToken); + + return Ok(result); + } +} diff --git a/src/Services/Passenger/src/Passenger/Passengers/Features/GetPassengerById/GetPassengerQueryById.cs b/src/Services/Passenger/src/Passenger/Passengers/Features/GetPassengerById/GetPassengerQueryById.cs new file mode 100644 index 0000000..ac5611a --- /dev/null +++ b/src/Services/Passenger/src/Passenger/Passengers/Features/GetPassengerById/GetPassengerQueryById.cs @@ -0,0 +1,6 @@ +using MediatR; +using Passenger.Passengers.Dtos; + +namespace Passenger.Passengers.Features.GetPassengerById; + +public record GetPassengerQueryById(long Id) : IRequest; \ No newline at end of file diff --git a/src/Services/Passenger/src/Passenger/Passengers/Features/GetPassengerById/GetPassengerQueryByIdHandler.cs b/src/Services/Passenger/src/Passenger/Passengers/Features/GetPassengerById/GetPassengerQueryByIdHandler.cs new file mode 100644 index 0000000..7432a12 --- /dev/null +++ b/src/Services/Passenger/src/Passenger/Passengers/Features/GetPassengerById/GetPassengerQueryByIdHandler.cs @@ -0,0 +1,34 @@ +using Ardalis.GuardClauses; +using MapsterMapper; +using MediatR; +using Microsoft.EntityFrameworkCore; +using Passenger.Data; +using Passenger.Passengers.Dtos; +using Passenger.Passengers.Exceptions; + +namespace Passenger.Passengers.Features.GetPassengerById; + +public class GetPassengerQueryByIdHandler : IRequestHandler +{ + private readonly PassengerDbContext _passengerDbContext; + private readonly IMapper _mapper; + + public GetPassengerQueryByIdHandler(IMapper mapper, PassengerDbContext passengerDbContext) + { + _mapper = mapper; + _passengerDbContext = passengerDbContext; + } + + public async Task Handle(GetPassengerQueryById query, CancellationToken cancellationToken) + { + Guard.Against.Null(query, nameof(query)); + + var passenger = + await _passengerDbContext.Passengers.SingleOrDefaultAsync(x => x.Id == query.Id, cancellationToken); + + if (passenger is null) + throw new PassengerNotFoundException(); + + return _mapper.Map(passenger!); + } +} diff --git a/src/Services/Passenger/src/Passenger/Passengers/Features/GetPassengerById/GetPassengerQueryByIdValidator.cs b/src/Services/Passenger/src/Passenger/Passengers/Features/GetPassengerById/GetPassengerQueryByIdValidator.cs new file mode 100644 index 0000000..a3dd372 --- /dev/null +++ b/src/Services/Passenger/src/Passenger/Passengers/Features/GetPassengerById/GetPassengerQueryByIdValidator.cs @@ -0,0 +1,13 @@ +using FluentValidation; + +namespace Passenger.Passengers.Features.GetPassengerById; + +public class GetPassengerQueryByIdValidator: AbstractValidator +{ + public GetPassengerQueryByIdValidator() + { + CascadeMode = CascadeMode.Stop; + + RuleFor(x => x.Id).NotNull().WithMessage("Id is required!"); + } +} \ No newline at end of file diff --git a/src/Services/Passenger/src/Passenger/Passengers/Features/ReservationMappings.cs b/src/Services/Passenger/src/Passenger/Passengers/Features/ReservationMappings.cs new file mode 100644 index 0000000..83a60e4 --- /dev/null +++ b/src/Services/Passenger/src/Passenger/Passengers/Features/ReservationMappings.cs @@ -0,0 +1,12 @@ +using AutoMapper; +using Passenger.Passengers.Dtos; + +namespace Passenger.Passengers.Features; + +public class ReservationMappings: Profile +{ + public ReservationMappings() + { + CreateMap(); + } +} diff --git a/src/Services/Passenger/src/Passenger/Passengers/Models/Passenger.cs b/src/Services/Passenger/src/Passenger/Passengers/Models/Passenger.cs new file mode 100644 index 0000000..879b606 --- /dev/null +++ b/src/Services/Passenger/src/Passenger/Passengers/Models/Passenger.cs @@ -0,0 +1,32 @@ +using BuildingBlocks.Domain.Model; + +namespace Passenger.Passengers.Models; + +public class Passenger : Aggregate +{ + public Passenger CompleteRegistrationPassenger(long id, string name, string passportNumber, PassengerType passengerType, int age) + { + var passenger = new Passenger + { + Name = name, + PassportNumber = passportNumber, + PassengerType = passengerType, + Age = age, + Id = id + }; + return passenger; + } + + + public static Passenger Create(long id, string name, string passportNumber, bool isDeleted = false) + { + var passenger = new Passenger { Id = id, Name = name, PassportNumber = passportNumber, IsDeleted = isDeleted }; + return passenger; + } + + + public string PassportNumber { get; private set; } + public string Name { get; private set; } + public PassengerType PassengerType { get; private set; } + public int Age { get; private set; } +} diff --git a/src/Services/Passenger/src/Passenger/Passengers/Models/PassengerType.cs b/src/Services/Passenger/src/Passenger/Passengers/Models/PassengerType.cs new file mode 100644 index 0000000..52ad478 --- /dev/null +++ b/src/Services/Passenger/src/Passenger/Passengers/Models/PassengerType.cs @@ -0,0 +1,9 @@ +namespace Passenger.Passengers.Models; + +public enum PassengerType +{ + Male, + Female, + Baby, + Unknown +} \ No newline at end of file