Compare commits

...

42 Commits
v2.2.0 ... main

Author SHA1 Message Date
Meysam Hadeli
d99d1a9a9a
Merge pull request #374 from meysamhadeli/refactor/refactor-docker-files
refactor: refactor dockerfiles
2026-02-26 23:50:46 +03:30
Meysam Hadeli
6e1fe61a65 refactor: refactor dockerfiles 2026-02-26 23:49:12 +03:30
Meysam Hadeli
a882b3cfe2
Merge pull request #373 from meysamhadeli/fix/fix-ci
fix: fix ci failed
2026-02-24 22:56:10 +03:30
Meysam Hadeli
14cc9e7c96 fix: fix ci failed 2026-02-24 22:54:44 +03:30
Meysam Hadeli
0f66c14299 fix: fix ci failed for tests 2026-02-21 22:00:57 +03:30
Meysam Hadeli
7f9cf8b922 fix: fix ci failed for tests 2026-02-20 23:18:29 +03:30
Meysam Hadeli
23d4babd52 fix: fix ci failed for tests 2026-02-20 20:57:26 +03:30
Meysam Hadeli
8d4819624e fix: fix ci failed for tests 2026-02-20 02:47:07 +03:30
Meysam Hadeli
043c20002c
Merge pull request #372 from meysamhadeli/fix/fix-ci-field-for-tests
fix: fix ci failed for tests
2026-02-20 02:11:41 +03:30
Meysam Hadeli
94a22dfc23 fix: fix ci failed for tests 2026-02-20 02:10:54 +03:30
Meysam Hadeli
3ce312891a Merge branch 'fix/fix-ci-failed' 2026-02-20 00:27:53 +03:30
Meysam Hadeli
a62177a6c4 fix: fix ci failed 2026-02-20 00:25:48 +03:30
Meysam Hadeli
b67d000580
Merge pull request #371 from meysamhadeli/fix/fix-ci-failed
fix: fix ci failed
2026-02-19 23:32:27 +03:30
Meysam Hadeli
2a5909bdbd fix: fix ci failed 2026-02-19 23:30:10 +03:30
Meysam Hadeli
786fbb121f
Merge pull request #370 from meysamhadeli/fix/fix-ci-failed
fix: fix ci failed
2026-02-19 23:19:09 +03:30
Meysam Hadeli
b9b3c26edc fix: fix ci failed 2026-02-19 23:17:14 +03:30
Meysam Hadeli
9dcd6625b2
Merge pull request #369 from meysamhadeli/fix/fix-ci-failed
Fix/fix ci failed
2026-02-19 22:03:04 +03:30
Meysam Hadeli
43666a7dd3 fix: fix ci failed 2026-02-19 22:01:04 +03:30
Meysam Hadeli
23b14eabe7 fix: fix ci failed 2026-02-19 21:59:25 +03:30
Meysam Hadeli
9164c770b5
Merge pull request #368 from meysamhadeli/fix/fix-ci-failed
Fix/fix ci failed
2026-02-19 21:08:53 +03:30
Meysam Hadeli
3bdcc6341f fix: fix ci failed 2026-02-19 21:06:59 +03:30
Meysam Hadeli
20e49770b2 fix: fix ci failed 2026-02-19 21:05:18 +03:30
Meysam Hadeli
e259b64476
Merge pull request #367 from meysamhadeli/chore-update-dockerfiles-to-dotnet-10
Chore update dockerfiles to dotnet 10
2026-02-16 21:13:10 +03:30
Meysam Hadeli
7476502e50 fix: fix formatting 2026-02-16 21:09:00 +03:30
Meysam Hadeli
64dfee4224 chore: update Dockerfiles to .net 10 2026-02-16 21:06:37 +03:30
Meysam Hadeli
bd94742d18
Merge pull request #366 from meysamhadeli/chore/update-config-file
chore: update .config file
2026-02-13 17:49:13 +03:30
Meysam Hadeli
d5e9f75dfc chore: update .config file 2026-02-13 17:29:58 +03:30
Meysam Hadeli
eef8aead7e
Merge pull request #365 from meysamhadeli/feat/update-packages-to-dotnet-10
Feat/update packages to dotnet 10
2026-02-13 14:52:12 +03:30
Meysam Hadeli
38c339c1aa chore: update ci build version 2026-02-13 00:49:49 +03:30
Meysam Hadeli
20bd2ac0bc docs: update documentation 2026-02-13 00:47:15 +03:30
Meysam Hadeli
3b86cf7917 feat: update packages to .net 10 2026-02-13 00:39:59 +03:30
Meysam Hadeli
d1dbe6209c feat: update packages to .net 10 2026-02-13 00:38:09 +03:30
Meysam Hadeli
475fa90e1c
Merge pull request #364 from meysamhadeli/chore/update-aspire-namespaces
chore: update aspire namespaces
2025-10-10 14:19:28 +03:30
Meysam Hadeli
0dd7941136 chore: update aspire namespaces 2025-10-10 14:07:55 +03:30
Meysam Hadeli
29a1e47a2d
Merge pull request #363 from meysamhadeli/chore/remove-additional-files
chore: remove additional files
2025-10-09 17:20:48 +03:30
Meysam Hadeli
68f768d687 chore: remove additional files 2025-10-09 17:18:05 +03:30
Meysam Hadeli
e2fbd27222
docs: update documentation 2025-10-09 16:15:00 +03:30
Meysam Hadeli
d6e313a560
Merge pull request #360 from meysamhadeli/chore/update-aspire-structure
chore/update aspire structure
2025-07-30 20:14:38 +03:30
Meysam Hadeli
57000c5802 chore: update aspire structure 2025-07-30 19:38:31 +03:30
Meysam Hadeli
b3ce2889b2 chore: update aspire structure 2025-07-30 19:37:07 +03:30
Meysam Hadeli
a3c6f670e1
Merge pull request #359 from meysamhadeli/feat/add-docker-compose-deployment-to-aspire-host
feat: add docker compose deployment to aspire host
2025-07-29 23:58:52 +03:30
Meysam Hadeli
3bd8cb1db3 feat: add docker compose deployment to aspire host 2025-07-29 23:13:26 +03:30
68 changed files with 1066 additions and 1248 deletions

3
.aspire/settings.json Normal file
View File

@ -0,0 +1,3 @@
{
"appHostPath": "../src/Aspire/src/AppHost/AppHost.csproj"
}

View File

@ -3,18 +3,28 @@
"isRoot": true, "isRoot": true,
"tools": { "tools": {
"dotnet-outdated-tool": { "dotnet-outdated-tool": {
"version": "4.6.4", "version": "4.6.9",
"commands": [ "commands": [
"dotnet-outdated" "dotnet-outdated"
], ]
"rollForward": false
}, },
"dotnet-ef": { "dotnet-ef": {
"version": "9.0.0", "version": "10.0.3",
"commands": [ "commands": [
"dotnet-ef" "dotnet-ef"
], ]
"rollForward": false },
"aspire.cli": {
"version": "13.1.1",
"commands": [
"aspire"
]
},
"csharpier": {
"version": "0.30.6",
"commands": [
"dotnet-csharpier"
]
} }
} }
} }

View File

@ -455,6 +455,8 @@ dotnet_diagnostic.CA1707.severity = None
dotnet_diagnostic.CA1716.severity = Suggestion dotnet_diagnostic.CA1716.severity = Suggestion
# CA1032: Implement standard exception constructors # CA1032: Implement standard exception constructors
dotnet_diagnostic.CA1032.severity = Suggestion dotnet_diagnostic.CA1032.severity = Suggestion
# AV1500: A method should not exceed a predefined number (60-100 lines) of lines
dotnet_diagnostic.AV1500.severity = none
################################################################################## ##################################################################################
# https://learn.microsoft.com/en-us/dotnet/fundamentals/code-analysis/style-rules/ # https://learn.microsoft.com/en-us/dotnet/fundamentals/code-analysis/style-rules/

View File

@ -34,7 +34,7 @@ runs:
- name: Setup .NET - name: Setup .NET
uses: actions/setup-dotnet@v4 uses: actions/setup-dotnet@v4
with: with:
dotnet-version: '9.x.x' dotnet-version: '10.x.x'
# https://learn.microsoft.com/en-us/dotnet/core/tools/global-tools # https://learn.microsoft.com/en-us/dotnet/core/tools/global-tools
- name: Restore .NET Tools - name: Restore .NET Tools

1
.gitpod.Dockerfile vendored
View File

@ -1 +0,0 @@
FROM gitpod/workspace-dotnet:latest

View File

@ -1,37 +0,0 @@
# https://github.com/gitpod-samples/template-dotnet-core-cli-csharp
# https://www.gitpod.io/docs/introduction/languages/dotnet
# https://github.com/gitpod-samples/template-docker-compose
# https://www.gitpod.io/docs/references/gitpod-yml
# https://www.gitpod.io/docs/configure
# https://www.gitpod.io/docs/configure/workspaces/ports
image:
file: .gitpod.Dockerfile
vscode:
extensions:
- muhammad-sammy.csharp
- editorconfig.editorconfig
- vivaxy.vscode-conventional-commits
- humao.rest-client
- ms-azuretools.vscode-docker
- donjayamanne.githistory
- pkief.material-icon-theme
- emmanuelbeziat.vscode-great-icons
# https://www.gitpod.io/docs/configure/workspaces/tasks#execution-order
# https://www.gitpod.io/docs/configure/projects/prebuilds
tasks:
- name: Init Docker-Compose
# https://www.gitpod.io/docs/configure/projects/prebuilds
# We load docker on pre-build for increasing speed
init: |
docker-compose pull
docker-compose -f ./deployments/docker-compose/infrastracture.yaml up -d
- name: Setup kubectl
command: bash $GITPOD_REPO_ROOT/scripts/setup_kubectl_gitpod.sh
- name: Restore & Build
init: |
dotnet dev-certs https
dotnet restore
dotnet build

View File

@ -1,34 +1,35 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<Project> <Project>
<PropertyGroup> <PropertyGroup>
<TargetFramework>net9.0</TargetFramework> <TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings> <ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable> <Nullable>enable</Nullable>
</PropertyGroup> </PropertyGroup>
<PropertyGroup>
<RepositoryRoot>$(MSBuildThisFileDirectory)</RepositoryRoot>
</PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="StyleCop.Analyzers" PrivateAssets="all" Version="1.1.118"> <PackageReference Include="StyleCop.Analyzers" PrivateAssets="all" Version="1.1.118">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference> </PackageReference>
<PackageReference Include="Meziantou.Analyzer" PrivateAssets="all" Version="2.0.182"> <PackageReference Include="Meziantou.Analyzer" PrivateAssets="all" Version="2.0.299">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference> </PackageReference>
<PackageReference Include="Roslynator.Analyzers" PrivateAssets="all" Version="4.12.9"> <PackageReference Include="Roslynator.Analyzers" PrivateAssets="all" Version="4.15.0">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference> </PackageReference>
<PackageReference Include="Roslynator.CodeAnalysis.Analyzers" PrivateAssets="all" Version="4.12.9"> <PackageReference Include="Roslynator.CodeAnalysis.Analyzers" PrivateAssets="all" Version="4.15.0">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference> </PackageReference>
<PackageReference Include="Roslynator.Formatting.Analyzers" PrivateAssets="all" Version="4.12.9"> <PackageReference Include="Roslynator.Formatting.Analyzers" PrivateAssets="all" Version="4.15.0">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference> </PackageReference>
<PackageReference Include="Microsoft.VisualStudio.Threading.Analyzers" PrivateAssets="all" Version="17.12.19"> <PackageReference Include="Microsoft.VisualStudio.Threading.Analyzers" PrivateAssets="all" Version="17.14.15">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference> </PackageReference>
<PackageReference Include="AsyncAwaitBestPractices" PrivateAssets="all" Version="9.0.0"> <PackageReference Include="AsyncAwaitBestPractices" PrivateAssets="all" Version="10.0.0">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="SerilogAnalyzer" PrivateAssets="all" Version="0.15.0">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference> </PackageReference>
<PackageReference Include="CSharpGuidelinesAnalyzer" PrivateAssets="all" Version="3.8.5"> <PackageReference Include="CSharpGuidelinesAnalyzer" PrivateAssets="all" Version="3.8.5">

View File

@ -6,15 +6,12 @@
</div> </div>
</div> </div>
> 🚀 **A practical microservices with the latest technologies and architectures like Vertical Slice Architecture, Event Sourcing, CQRS, DDD, gRpc, MongoDB, RabbitMq, Masstransit, and Aspire in .Net 9.** > 🚀 **A practical microservices with the latest technologies and architectures like Vertical Slice Architecture, Event Sourcing, CQRS, DDD, gRpc, MongoDB, RabbitMq, Masstransit, and Aspire in .Net 10.**
## You can find other version of this project here: ## You can find other version of this project here:
- [Booking with Modular Monolith Architecture](https://github.com/meysamhadeli/booking-modular-monolith) - [Booking with Modular Monolith Architecture](https://github.com/meysamhadeli/booking-modular-monolith)
- [Booking with Monolith Architecture](https://github.com/meysamhadeli/booking-monolith) - [Booking with Monolith Architecture](https://github.com/meysamhadeli/booking-monolith)
<div>
<a href="https://gitpod.io/#https://github.com/meysamhadeli/booking-microservices"><img alt="Open in Gitpod" src="https://gitpod.io/button/open-in-gitpod.svg"/></a>
</div>
<div> <div>
<a href='https://codespaces.new/meysamhadeli/booking-microservices?quickstart=1'><img alt='Open in GitHub Codespaces' src='https://github.com/codespaces/badge.svg'></a> <a href='https://codespaces.new/meysamhadeli/booking-microservices?quickstart=1'><img alt='Open in GitHub Codespaces' src='https://github.com/codespaces/badge.svg'></a>
</div> </div>
@ -77,10 +74,10 @@
## Technologies - Libraries ## Technologies - Libraries
- ✔️ **[`.NET 9`](https://github.com/dotnet/aspnetcore)** - .NET Framework and .NET Core, including ASP.NET and ASP.NET Core. - ✔️ **[`.NET 10`](https://github.com/dotnet/aspnetcore)** - .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. - ✔️ **[`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. - ✔️ **[`EF Core`](https://github.com/dotnet/efcore)** - Modern object-database mapper for .NET. It supports LINQ queries, change tracking, updates, and schema migrations.
- ✔️ **[`AspNetCore OpenApi`](https://learn.microsoft.com/en-us/aspnet/core/fundamentals/openapi/aspnetcore-openapi?view=aspnetcore-9.0&tabs=visual-studio#configure-openapi-document-generation)** - Provides built-in support for OpenAPI document generation in ASP.NET Core. - ✔️ **[`AspNetCore OpenApi`](https://learn.microsoft.com/en-us/aspnet/core/fundamentals/openapi/aspnetcore-openapi)** - Provides built-in support for OpenAPI document generation in ASP.NET Core.
- ✔️ **[`Masstransit`](https://github.com/MassTransit/MassTransit)** - Distributed Application Framework for .NET. - ✔️ **[`Masstransit`](https://github.com/MassTransit/MassTransit)** - Distributed Application Framework for .NET.
- ✔️ **[`MediatR`](https://github.com/jbogard/MediatR)** - Simple, unambitious mediator implementation in .NET. - ✔️ **[`MediatR`](https://github.com/jbogard/MediatR)** - Simple, unambitious mediator implementation in .NET.
- ✔️ **[`FluentValidation`](https://github.com/FluentValidation/FluentValidation)** - Popular .NET validation library for building strongly-typed validation rules. - ✔️ **[`FluentValidation`](https://github.com/FluentValidation/FluentValidation)** - Popular .NET validation library for building strongly-typed validation rules.
@ -217,13 +214,13 @@ dotnet dev-certs https --trust
### Aspire ### Aspire
To run the application using the `ASPIRE App Host`, execute the following command from the solution root: To run the application using the `Aspire App Host`, execute the following command from the solution root:
```bash ```bash
dotnet run --project ./src/Aspire/src/AppHost aspire run
``` ```
> Note:The `ASPIRE dashboard` will be available at `http://localhost:18888` > Note:The `Aspire dashboard` will be available at `http://localhost:18888`
> ### Docker Compose > ### Docker Compose

View File

@ -75,6 +75,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{C4287034-683
EndProject EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AppHost", "src\Aspire\src\AppHost\AppHost.csproj", "{490BCB11-314C-473C-9B85-A32164783507}" Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AppHost", "src\Aspire\src\AppHost\AppHost.csproj", "{490BCB11-314C-473C-9B85-A32164783507}"
EndProject EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ServiceDefaults", "src\Aspire\src\ServiceDefaults\ServiceDefaults.csproj", "{5B7BF918-E47F-4932-B5C5-E8C2C35890E4}"
EndProject
Global Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU Debug|Any CPU = Debug|Any CPU
@ -289,6 +291,18 @@ Global
{490BCB11-314C-473C-9B85-A32164783507}.Release|x64.Build.0 = Release|Any CPU {490BCB11-314C-473C-9B85-A32164783507}.Release|x64.Build.0 = Release|Any CPU
{490BCB11-314C-473C-9B85-A32164783507}.Release|x86.ActiveCfg = Release|Any CPU {490BCB11-314C-473C-9B85-A32164783507}.Release|x86.ActiveCfg = Release|Any CPU
{490BCB11-314C-473C-9B85-A32164783507}.Release|x86.Build.0 = Release|Any CPU {490BCB11-314C-473C-9B85-A32164783507}.Release|x86.Build.0 = Release|Any CPU
{5B7BF918-E47F-4932-B5C5-E8C2C35890E4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{5B7BF918-E47F-4932-B5C5-E8C2C35890E4}.Debug|Any CPU.Build.0 = Debug|Any CPU
{5B7BF918-E47F-4932-B5C5-E8C2C35890E4}.Debug|x64.ActiveCfg = Debug|Any CPU
{5B7BF918-E47F-4932-B5C5-E8C2C35890E4}.Debug|x64.Build.0 = Debug|Any CPU
{5B7BF918-E47F-4932-B5C5-E8C2C35890E4}.Debug|x86.ActiveCfg = Debug|Any CPU
{5B7BF918-E47F-4932-B5C5-E8C2C35890E4}.Debug|x86.Build.0 = Debug|Any CPU
{5B7BF918-E47F-4932-B5C5-E8C2C35890E4}.Release|Any CPU.ActiveCfg = Release|Any CPU
{5B7BF918-E47F-4932-B5C5-E8C2C35890E4}.Release|Any CPU.Build.0 = Release|Any CPU
{5B7BF918-E47F-4932-B5C5-E8C2C35890E4}.Release|x64.ActiveCfg = Release|Any CPU
{5B7BF918-E47F-4932-B5C5-E8C2C35890E4}.Release|x64.Build.0 = Release|Any CPU
{5B7BF918-E47F-4932-B5C5-E8C2C35890E4}.Release|x86.ActiveCfg = Release|Any CPU
{5B7BF918-E47F-4932-B5C5-E8C2C35890E4}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection EndGlobalSection
GlobalSection(SolutionProperties) = preSolution GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE HideSolutionNode = FALSE
@ -329,5 +343,6 @@ Global
{D1B6353A-63F5-4DD9-90E6-42B2CFDF1DEA} = {CD4A4407-C3B0-422D-BB8C-2A810CED9938} {D1B6353A-63F5-4DD9-90E6-42B2CFDF1DEA} = {CD4A4407-C3B0-422D-BB8C-2A810CED9938}
{C4287034-6833-4505-A6EB-704A86392ECB} = {D1B6353A-63F5-4DD9-90E6-42B2CFDF1DEA} {C4287034-6833-4505-A6EB-704A86392ECB} = {D1B6353A-63F5-4DD9-90E6-42B2CFDF1DEA}
{490BCB11-314C-473C-9B85-A32164783507} = {C4287034-6833-4505-A6EB-704A86392ECB} {490BCB11-314C-473C-9B85-A32164783507} = {C4287034-6833-4505-A6EB-704A86392ECB}
{5B7BF918-E47F-4932-B5C5-E8C2C35890E4} = {C4287034-6833-4505-A6EB-704A86392ECB}
EndGlobalSection EndGlobalSection
EndGlobal EndGlobal

View File

@ -355,7 +355,7 @@ services:
args: args:
Version: "1" Version: "1"
context: ../../ context: ../../
dockerfile: src/ApiGateway/dev.Dockerfile dockerfile: src/ApiGateway/Dockerfile
container_name: api-gateway container_name: api-gateway
ports: ports:
- "5001:80" - "5001:80"
@ -382,7 +382,7 @@ services:
args: args:
Version: "1" Version: "1"
context: ../../ context: ../../
dockerfile: src/Services/Flight/dev.Dockerfile dockerfile: src/Services/Flight/Dockerfile
container_name: flight container_name: flight
ports: ports:
- 5004:80 - 5004:80
@ -408,7 +408,7 @@ services:
args: args:
Version: "1" Version: "1"
context: ../../ context: ../../
dockerfile: src/Services/Identity/dev.Dockerfile dockerfile: src/Services/Identity/Dockerfile
container_name: identity container_name: identity
ports: ports:
- 6005:80 - 6005:80
@ -435,7 +435,7 @@ services:
args: args:
Version: "1" Version: "1"
context: ../../ context: ../../
dockerfile: src/Services/Passenger/dev.Dockerfile dockerfile: src/Services/Passenger/Dockerfile
container_name: passenger container_name: passenger
ports: ports:
- 6012:80 - 6012:80
@ -462,7 +462,7 @@ services:
args: args:
Version: "1" Version: "1"
context: ../../ context: ../../
dockerfile: src/Services/Booking/dev.Dockerfile dockerfile: src/Services/Booking/Dockerfile
container_name: booking container_name: booking
ports: ports:
- 6010:80 - 6010:80

View File

@ -1,6 +1,6 @@
{ {
"sdk": { "sdk": {
"version": "9.0.100", "version": "10.0.103",
"rollForward": "latestFeature" "rollForward": "latestFeature"
} }
} }

View File

@ -7,8 +7,8 @@
"main": "index.js", "main": "index.js",
"scripts": { "scripts": {
"prepare": "husky && dotnet tool restore", "prepare": "husky && dotnet tool restore",
"format": "dotnet format booking-microservices.sln --severity error --verbosity detailed", "format": "dotnet tool run dotnet-csharpier booking-microservices.sln",
"ci-format": "dotnet format booking-microservices.sln --verify-no-changes --severity error --verbosity detailed", "ci-format": "dotnet tool run dotnet-csharpier booking-microservices.sln --check",
"upgrade-packages": "dotnet outdated --upgrade" "upgrade-packages": "dotnet outdated --upgrade"
}, },
"devDependencies": { "devDependencies": {

View File

@ -1,45 +1,38 @@
FROM mcr.microsoft.com/dotnet/sdk:9.0 AS builder # ---------- Build Stage ----------
WORKDIR / FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build
WORKDIR /src
COPY ./.editorconfig ./ # Copy solution-level files
COPY ./global.json ./ COPY .editorconfig .
COPY ./Directory.Build.props ./ COPY global.json .
COPY Directory.Build.props .
# Setup working directory for the project # Copy project files first (better Docker caching)
COPY ./src/BuildingBlocks/BuildingBlocks.csproj ./building-blocks/ COPY src/BuildingBlocks/BuildingBlocks.csproj src/BuildingBlocks/
COPY ./src/ApiGateway/src/ApiGateway.csproj ./src/ApiGateway/src/ COPY src/ApiGateway/src/ApiGateway.csproj src/ApiGateway/src/
COPY src/Aspire/src/ServiceDefaults/ServiceDefaults.csproj src/Aspire/src/ServiceDefaults/
# Restore dependencies
RUN dotnet restore src/ApiGateway/src/ApiGateway.csproj
# Restore nuget packages # Copy the rest of the source code
RUN dotnet restore ./src/ApiGateway/src/ApiGateway.csproj COPY src ./src
# Copy project files # Publish (build included)
COPY ./src/BuildingBlocks ./src/BuildingBlocks/ RUN dotnet publish src/ApiGateway/src/ApiGateway.csproj \
COPY ./src/ApiGateway/src ./src/ApiGateway/src/ -c Release \
-o /app/publish \
--no-restore
# Build project with Release configuration # ---------- Runtime Stage ----------
# and no restore, as we did it already FROM mcr.microsoft.com/dotnet/aspnet:10.0 AS runtime
WORKDIR /app
RUN ls COPY --from=build /app/publish .
RUN dotnet build -c Release --no-restore ./src/ApiGateway/src/ApiGateway.csproj
WORKDIR /src/ApiGateway/src ENV ASPNETCORE_URLS=http://+:80
ENV ASPNETCORE_ENVIRONMENT=docker
# 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:9.0
# Setup working directory for the project
WORKDIR /
COPY --from=builder /src/ApiGateway/src/out .
ENV ASPNETCORE_URLS https://*:443, http://*:80
ENV ASPNETCORE_ENVIRONMENT docker
EXPOSE 80 EXPOSE 80
EXPOSE 443
ENTRYPOINT ["dotnet", "ApiGateway.dll"] ENTRYPOINT ["dotnet", "ApiGateway.dll"]

View File

@ -1,48 +0,0 @@
FROM mcr.microsoft.com/dotnet/sdk:9.0 AS builder
WORKDIR /
COPY ./.editorconfig ./
COPY ./global.json ./
COPY ./Directory.Build.props ./
# Setup working directory for the project
COPY ./src/BuildingBlocks/BuildingBlocks.csproj ./building-blocks/
COPY ./src/ApiGateway/src/ApiGateway.csproj ./src/ApiGateway/src/
# Restore nuget packages
RUN --mount=type=cache,id=gateway_nuget,target=/root/.nuget/packages \
dotnet restore ./src/ApiGateway/src/ApiGateway.csproj
# Copy project files
COPY ./src/BuildingBlocks ./src/BuildingBlocks/
COPY ./src/ApiGateway/src ./src/ApiGateway/src/
# Build project with Release configuration
# and no restore, as we did it already
RUN ls
RUN --mount=type=cache,id=gateway_nuget,target=/root/.nuget/packages \
dotnet build -c Release --no-restore ./src/ApiGateway/src/ApiGateway.csproj
WORKDIR /src/ApiGateway/src
# Publish project to output folder
# and no build, as we did it already
RUN --mount=type=cache,id=gateway_nuget,target=/root/.nuget/packages \
dotnet publish -c Release --no-build -o out
FROM mcr.microsoft.com/dotnet/aspnet:9.0
# Setup working directory for the project
WORKDIR /
COPY --from=builder /src/ApiGateway/src/out .
ENV ASPNETCORE_URLS https://*:443, http://*:80
ENV ASPNETCORE_ENVIRONMENT docker
EXPOSE 80
EXPOSE 443
ENTRYPOINT ["dotnet", "ApiGateway.dll"]

View File

@ -1,5 +1,6 @@
using BuildingBlocks.Web; using BuildingBlocks.Web;
using Figgle; using Figgle;
using Figgle.Fonts;
var builder = WebApplication.CreateBuilder(args); var builder = WebApplication.CreateBuilder(args);
var env = builder.Environment; var env = builder.Environment;
@ -7,7 +8,6 @@ var appOptions = builder.Services.GetOptions<AppOptions>("AppOptions");
Console.WriteLine(FiggleFonts.Standard.Render(appOptions.Name)); Console.WriteLine(FiggleFonts.Standard.Render(appOptions.Name));
builder.Services.AddControllers(); builder.Services.AddControllers();
builder.Services.AddHttpContextAccessor(); builder.Services.AddHttpContextAccessor();

View File

@ -1,10 +1,10 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<Sdk Name="Aspire.AppHost.SDK" Version="9.3.1"/> <Sdk Name="Aspire.AppHost.SDK" Version="13.1.1"/>
<PropertyGroup> <PropertyGroup>
<OutputType>Exe</OutputType> <OutputType>Exe</OutputType>
<TargetFramework>net9.0</TargetFramework> <TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings> <ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable> <Nullable>enable</Nullable>
<IsAspireHost>true</IsAspireHost> <IsAspireHost>true</IsAspireHost>
@ -12,12 +12,13 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Aspire.Hosting.AppHost" Version="9.3.1" /> <PackageReference Include="Aspire.Hosting.AppHost" Version="13.1.1" />
<PackageReference Include="Aspire.Hosting.MongoDB" Version="9.3.1" /> <PackageReference Include="Aspire.Hosting.Docker" Version="13.1.1-preview.1.26105.8" />
<PackageReference Include="Aspire.Hosting.PostgreSQL" Version="9.3.1" /> <PackageReference Include="Aspire.Hosting.MongoDB" Version="13.1.1" />
<PackageReference Include="Aspire.Hosting.RabbitMQ" Version="9.3.1" /> <PackageReference Include="Aspire.Hosting.PostgreSQL" Version="13.1.1" />
<PackageReference Include="Aspire.Hosting.Redis" Version="9.3.1" /> <PackageReference Include="Aspire.Hosting.RabbitMQ" Version="13.1.1" />
<PackageReference Include="CommunityToolkit.Aspire.Hosting.EventStore" Version="9.6.0" /> <PackageReference Include="Aspire.Hosting.Redis" Version="13.1.1" />
<PackageReference Include="CommunityToolkit.Aspire.Hosting.EventStore" Version="9.9.0" />
<PackageReference Include="Elastic.Aspire.Hosting.Elasticsearch" Version="9.3.0" /> <PackageReference Include="Elastic.Aspire.Hosting.Elasticsearch" Version="9.3.0" />
</ItemGroup> </ItemGroup>

View File

@ -2,39 +2,82 @@ using System.Net.Sockets;
var builder = DistributedApplication.CreateBuilder(args); var builder = DistributedApplication.CreateBuilder(args);
// 1. Database Services builder.AddDockerComposeEnvironment("docker-compose");
var username = builder.AddParameter("username", "postgres", secret: true);
var password = builder.AddParameter("password", "postgres", secret: true);
var postgres = builder.AddPostgres("postgres", username, password) // 1. Database Services
var pgUsername = builder.AddParameter("pg-username", "postgres", secret: true);
var pgPassword = builder.AddParameter("pg-password", "postgres", secret: true);
var postgres = builder.AddPostgres("postgres", pgUsername, pgPassword)
.WithImage("postgres:latest") .WithImage("postgres:latest")
.WithEndpoint(port: 5432, targetPort: 5432, name: "postgres") .WithEndpoint(
"tcp",
e =>
{
e.Port = 5432;
e.TargetPort = 5432;
e.IsProxied = true;
e.IsExternal = false;
})
.WithArgs( .WithArgs(
"-c", "wal_level=logical", "-c",
"-c", "max_prepared_transactions=10" "wal_level=logical",
) "-c",
.WithDataVolume("postgres-data") "max_prepared_transactions=10");
.WithLifetime(ContainerLifetime.Persistent);
if (builder.ExecutionContext.IsPublishMode)
{
postgres.WithDataVolume("postgres-data")
.WithLifetime(ContainerLifetime.Persistent);
}
var flightDb = postgres.AddDatabase("flight"); var flightDb = postgres.AddDatabase("flight");
var passengerDb = postgres.AddDatabase("passenger"); var passengerDb = postgres.AddDatabase("passenger");
var identityDb = postgres.AddDatabase("identity"); var identityDb = postgres.AddDatabase("identity");
var persistMessageDb = postgres.AddDatabase("persist-message"); var persistMessageDb = postgres.AddDatabase("persist-message");
var mongoUsername = builder.AddParameter("mongo-username", "root"); var mongoUsername = builder.AddParameter("mongo-username", "root", secret: true);
var mongoPassword = builder.AddParameter("mongo-password", "secret", secret: true); var mongoPassword = builder.AddParameter("mongo-password", "secret", secret: true);
var mongo = builder.AddMongoDB("mongo", userName: mongoUsername, password: mongoPassword) var mongo = builder.AddMongoDB("mongo", userName: mongoUsername, password: mongoPassword)
.WithImage("mongo:latest") .WithImage("mongo")
.WithEndpoint(port: 27017, targetPort: 27017, name: "mongo") .WithImageTag("latest")
.WithDataVolume("mongo-data") .WithEndpoint(
.WithLifetime(ContainerLifetime.Persistent); "tcp",
e =>
{
e.Port = 27017;
e.TargetPort = 27017;
e.IsProxied = true;
e.IsExternal = false;
});
if (builder.ExecutionContext.IsPublishMode)
{
mongo.WithDataVolume("mongo-data")
.WithLifetime(ContainerLifetime.Persistent);
}
var redis = builder.AddRedis("redis") var redis = builder.AddRedis("redis")
.WithImage("redis:latest") .WithImage("redis:latest")
.WithEndpoint(port: 6379, targetPort: 6379, name: "redis") .WithEndpoint(
.WithDataVolume("redis-data") "tcp",
.WithLifetime(ContainerLifetime.Persistent); e =>
{
e.Port = 6379;
e.TargetPort = 6379;
e.IsProxied = true;
e.IsExternal = false;
});
if (builder.ExecutionContext.IsPublishMode)
{
redis.WithDataVolume("redis-data")
.WithLifetime(ContainerLifetime.Persistent);
}
var eventstore = builder.AddEventStore("eventstore") var eventstore = builder.AddEventStore("eventstore")
.WithImage("eventstore/eventstore") .WithImage("eventstore/eventstore")
@ -43,41 +86,103 @@ var eventstore = builder.AddEventStore("eventstore")
.WithEnvironment("EVENTSTORE_START_STANDARD_PROJECTIONS", "True") .WithEnvironment("EVENTSTORE_START_STANDARD_PROJECTIONS", "True")
.WithEnvironment("EVENTSTORE_INSECURE", "True") .WithEnvironment("EVENTSTORE_INSECURE", "True")
.WithEnvironment("EVENTSTORE_ENABLE_ATOM_PUB_OVER_HTTP", "True") .WithEnvironment("EVENTSTORE_ENABLE_ATOM_PUB_OVER_HTTP", "True")
.WithHttpEndpoint(port: 2113, targetPort: 2113, name: "eventstore-http") .WithEndpoint(
.WithDataVolume("eventstore-data") "http",
.WithLifetime(ContainerLifetime.Persistent); e =>
{
e.TargetPort = 2113;
e.Port = 2113;
e.IsProxied = true;
e.IsExternal = true;
})
.WithEndpoint(
port: 1113,
targetPort: 1113,
name: "tcp",
isProxied: true,
isExternal: false);
if (builder.ExecutionContext.IsPublishMode)
{
eventstore.WithDataVolume("eventstore-data")
.WithLifetime(ContainerLifetime.Persistent);
}
// 2. Messaging Services // 2. Messaging Services
var rabbitmq = builder.AddRabbitMQ("rabbitmq") var rabbitmqUsername = builder.AddParameter("rabbitmq-username", "guest", secret: true);
.WithImage("rabbitmq:management") var rabbitmqPassword = builder.AddParameter("rabbitmq-password", "guest", secret: true);
.WithEndpoint(port: 5672, targetPort: 5672, name: "rabbitmq-amqp")
.WithEndpoint(port: 15672, targetPort: 15672, name: "rabbitmq-management")
.WithLifetime(ContainerLifetime.Persistent);
// 3. Observability Services var rabbitmq = builder.AddRabbitMQ("rabbitmq", rabbitmqUsername, rabbitmqPassword)
.WithManagementPlugin()
.WithEndpoint(
"tcp",
e =>
{
e.TargetPort = 5672;
e.Port = 5672;
e.IsProxied = true;
e.IsExternal = false;
})
.WithEndpoint(
"management",
e =>
{
e.TargetPort = 15672;
e.Port = 15672;
e.IsProxied = true;
e.IsExternal = true;
});
if (builder.ExecutionContext.IsPublishMode)
{
rabbitmq.WithLifetime(ContainerLifetime.Persistent);
}
// // 3. Observability Services
var jaeger = builder.AddContainer("jaeger-all-in-one", "jaegertracing/all-in-one") var jaeger = builder.AddContainer("jaeger-all-in-one", "jaegertracing/all-in-one")
.WithEndpoint(port: 6831, targetPort: 6831, name: "jaeger-udp", protocol: ProtocolType.Udp) .WithEndpoint(
.WithEndpoint(port: 16686, targetPort: 16686, name: "jaeger-ui") port: 6831,
.WithEndpoint(port: 14268, targetPort: 14268, name: "jaeger-api") targetPort: 6831,
.WithEndpoint(port: 14317, targetPort: 4317, name: "jaeger-otlp-grpc") name: "agent",
.WithEndpoint(port: 14318, targetPort: 4318, name: "jaeger-otlp-http") protocol: ProtocolType.Udp,
.WithLifetime(ContainerLifetime.Persistent); isProxied: true,
isExternal: false)
.WithEndpoint(port: 16686, targetPort: 16686, name: "http", isProxied: true, isExternal: true)
.WithEndpoint(port: 14268, targetPort: 14268, name: "collector", isProxied: true, isExternal: false)
.WithEndpoint(port: 14317, targetPort: 4317, name: "otlp-grpc", isProxied: true, isExternal: false)
.WithEndpoint(port: 14318, targetPort: 4318, name: "otlp-http", isProxied: true, isExternal: false);
if (builder.ExecutionContext.IsPublishMode)
{
jaeger.WithLifetime(ContainerLifetime.Persistent);
}
var zipkin = builder.AddContainer("zipkin-all-in-one", "openzipkin/zipkin") var zipkin = builder.AddContainer("zipkin-all-in-one", "openzipkin/zipkin")
.WithEndpoint(port: 9411, targetPort: 9411, name: "zipkin-api") .WithEndpoint(port: 9411, targetPort: 9411, name: "http", isProxied: true, isExternal: true);
.WithLifetime(ContainerLifetime.Persistent);
if (builder.ExecutionContext.IsPublishMode)
{
zipkin.WithLifetime(ContainerLifetime.Persistent);
}
var otelCollector = builder.AddContainer("otel-collector", "otel/opentelemetry-collector-contrib") var otelCollector = builder.AddContainer("otel-collector", "otel/opentelemetry-collector-contrib")
.WithBindMount("../../../../deployments/configs/otel-collector-config.yaml", "/etc/otelcol-contrib/config.yaml", isReadOnly: true) .WithBindMount(
"../../../../deployments/configs/otel-collector-config.yaml",
"/etc/otelcol-contrib/config.yaml",
isReadOnly: true)
.WithArgs("--config=/etc/otelcol-contrib/config.yaml") .WithArgs("--config=/etc/otelcol-contrib/config.yaml")
.WithEndpoint(port: 11888, targetPort: 1888, name: "otel-pprof") .WithEndpoint(port: 11888, targetPort: 1888, name: "otel-pprof", isProxied: true, isExternal: true)
.WithEndpoint(port: 8888, targetPort: 8888, name: "otel-metrics") .WithEndpoint(port: 8888, targetPort: 8888, name: "otel-metrics", isProxied: true, isExternal: true)
.WithEndpoint(port: 8889, targetPort: 8889, name: "otel-exporter-metrics") .WithEndpoint(port: 8889, targetPort: 8889, name: "otel-exporter-metrics", isProxied: true, isExternal: true)
.WithEndpoint(port: 13133, targetPort: 13133, name: "otel-health") .WithEndpoint(port: 13133, targetPort: 13133, name: "otel-health", isProxied: true, isExternal: true)
.WithEndpoint(port: 4317, targetPort: 4317, name: "otel-grpc") .WithEndpoint(port: 4317, targetPort: 4317, name: "otel-grpc", isProxied: true, isExternal: true)
.WithEndpoint(port: 4318, targetPort: 4318, name: "otel-http") .WithEndpoint(port: 4318, targetPort: 4318, name: "otel-http", isProxied: true, isExternal: true)
.WithEndpoint(port: 55679, targetPort: 55679, name: "otel-zpages") .WithEndpoint(port: 55679, targetPort: 55679, name: "otel-zpages", isProxied: true, isExternal: true);
.WithLifetime(ContainerLifetime.Persistent);
if (builder.ExecutionContext.IsPublishMode)
{
otelCollector.WithLifetime(ContainerLifetime.Persistent);
}
var prometheus = builder.AddContainer("prometheus", "prom/prometheus") var prometheus = builder.AddContainer("prometheus", "prom/prometheus")
.WithBindMount("../../../../deployments/configs/prometheus.yaml", "/etc/prometheus/prometheus.yml") .WithBindMount("../../../../deployments/configs/prometheus.yaml", "/etc/prometheus/prometheus.yml")
@ -87,8 +192,12 @@ var prometheus = builder.AddContainer("prometheus", "prom/prometheus")
"--web.console.libraries=/usr/share/prometheus/console_libraries", "--web.console.libraries=/usr/share/prometheus/console_libraries",
"--web.console.templates=/usr/share/prometheus/consoles", "--web.console.templates=/usr/share/prometheus/consoles",
"--web.enable-remote-write-receiver") "--web.enable-remote-write-receiver")
.WithEndpoint(port: 9090, targetPort: 9090, name: "prometheus-web") .WithEndpoint(port: 9090, targetPort: 9090, name: "http", isProxied: true, isExternal: true);
.WithLifetime(ContainerLifetime.Persistent);
if (builder.ExecutionContext.IsPublishMode)
{
prometheus.WithLifetime(ContainerLifetime.Persistent);
}
var grafana = builder.AddContainer("grafana", "grafana/grafana") var grafana = builder.AddContainer("grafana", "grafana/grafana")
.WithEnvironment("GF_INSTALL_PLUGINS", "grafana-clock-panel,grafana-simple-json-datasource") .WithEnvironment("GF_INSTALL_PLUGINS", "grafana-clock-panel,grafana-simple-json-datasource")
@ -97,8 +206,12 @@ var grafana = builder.AddContainer("grafana", "grafana/grafana")
.WithEnvironment("GF_FEATURE_TOGGLES_ENABLE", "traceqlEditor") .WithEnvironment("GF_FEATURE_TOGGLES_ENABLE", "traceqlEditor")
.WithBindMount("../../../../deployments/configs/grafana/provisioning", "/etc/grafana/provisioning") .WithBindMount("../../../../deployments/configs/grafana/provisioning", "/etc/grafana/provisioning")
.WithBindMount("../../../../deployments/configs/grafana/dashboards", "/var/lib/grafana/dashboards") .WithBindMount("../../../../deployments/configs/grafana/dashboards", "/var/lib/grafana/dashboards")
.WithEndpoint(port: 3000, targetPort: 3000, name: "grafana-web") .WithEndpoint(port: 3000, targetPort: 3000, name: "http", isProxied: true, isExternal: true);
.WithLifetime(ContainerLifetime.Persistent);
if (builder.ExecutionContext.IsPublishMode)
{
grafana.WithLifetime(ContainerLifetime.Persistent);
}
var nodeExporter = builder.AddContainer("node-exporter", "prom/node-exporter") var nodeExporter = builder.AddContainer("node-exporter", "prom/node-exporter")
.WithBindMount("/proc", "/host/proc", isReadOnly: true) .WithBindMount("/proc", "/host/proc", isReadOnly: true)
@ -108,22 +221,36 @@ var nodeExporter = builder.AddContainer("node-exporter", "prom/node-exporter")
"--path.procfs=/host/proc", "--path.procfs=/host/proc",
"--path.rootfs=/rootfs", "--path.rootfs=/rootfs",
"--path.sysfs=/host/sys") "--path.sysfs=/host/sys")
.WithEndpoint(port: 9101, targetPort: 9100, name: "node-exporter") .WithEndpoint(port: 9101, targetPort: 9100, name: "http", isProxied: true, isExternal: true);
.WithLifetime(ContainerLifetime.Persistent);
if (builder.ExecutionContext.IsPublishMode)
{
nodeExporter.WithLifetime(ContainerLifetime.Persistent);
}
var tempo = builder.AddContainer("tempo", "grafana/tempo") var tempo = builder.AddContainer("tempo", "grafana/tempo")
.WithBindMount("../../../../deployments/configs/tempo.yaml", "/etc/tempo.yaml", isReadOnly: true) .WithBindMount("../../../../deployments/configs/tempo.yaml", "/etc/tempo.yaml", isReadOnly: true)
.WithArgs("--config.file=/etc/tempo.yaml") .WithArgs("--config.file=/etc/tempo.yaml")
.WithEndpoint(port: 3200, targetPort: 3200, name: "tempo") .WithEndpoint(port: 3200, targetPort: 3200, name: "http", isProxied: true, isExternal: false)
.WithEndpoint(port: 24317, targetPort: 4317, name: "tempo-otlp-grpc") .WithEndpoint(port: 9095, targetPort: 9095, name: "grpc", isProxied: true, isExternal: false)
.WithEndpoint(port: 24318, targetPort: 4318, name: "tempo-otlp-http") .WithEndpoint(port: 4317, targetPort: 4317, name: "otlp-grpc", isProxied: true, isExternal: false)
.WithLifetime(ContainerLifetime.Persistent); .WithEndpoint(port: 4318, targetPort: 4318, name: "otlp-http", isProxied: true, isExternal: false);
if (builder.ExecutionContext.IsPublishMode)
{
tempo.WithLifetime(ContainerLifetime.Persistent);
}
var loki = builder.AddContainer("loki", "grafana/loki") var loki = builder.AddContainer("loki", "grafana/loki")
.WithBindMount("../../../../deployments/configs/loki-config.yaml", "/etc/loki/local-config.yaml", isReadOnly: true) .WithBindMount("../../../../deployments/configs/loki-config.yaml", "/etc/loki/local-config.yaml", isReadOnly: true)
.WithArgs("-config.file=/etc/loki/local-config.yaml") .WithArgs("-config.file=/etc/loki/local-config.yaml")
.WithEndpoint(port: 3100, targetPort: 3100, name: "loki") .WithEndpoint(port: 3100, targetPort: 3100, name: "http", isProxied: true, isExternal: false)
.WithLifetime(ContainerLifetime.Persistent); .WithEndpoint(port: 9096, targetPort: 9096, name: "grpc", isProxied: true, isExternal: false);
if (builder.ExecutionContext.IsPublishMode)
{
loki.WithLifetime(ContainerLifetime.Persistent);
}
var elasticsearch = builder.AddElasticsearch("elasticsearch") var elasticsearch = builder.AddElasticsearch("elasticsearch")
.WithImage("docker.elastic.co/elasticsearch/elasticsearch:8.17.0") .WithImage("docker.elastic.co/elasticsearch/elasticsearch:8.17.0")
@ -139,18 +266,41 @@ var elasticsearch = builder.AddElasticsearch("elasticsearch")
.WithEnvironment("transport.host", "localhost") .WithEnvironment("transport.host", "localhost")
.WithEnvironment("bootstrap.memory_lock", "true") .WithEnvironment("bootstrap.memory_lock", "true")
.WithEnvironment("cluster.routing.allocation.disk.threshold_enabled", "false") .WithEnvironment("cluster.routing.allocation.disk.threshold_enabled", "false")
.WithEndpoint(port: 9200, targetPort: 9200, name: "elasticsearch-http") .WithEndpoint(
.WithEndpoint(port: 9300, targetPort: 9300, name: "elasticsearch-transport") "http",
.WithDataVolume("elastic-data") e =>
.WithLifetime(ContainerLifetime.Persistent); {
e.TargetPort = 9200;
e.Port = 9200;
e.IsProxied = true;
e.IsExternal = false;
})
.WithEndpoint(
"internal",
e =>
{
e.TargetPort = 9300;
e.Port = 9300;
e.IsProxied = true;
e.IsExternal = false;
})
.WithDataVolume("elastic-data");
if (builder.ExecutionContext.IsPublishMode)
{
elasticsearch.WithLifetime(ContainerLifetime.Persistent);
}
var kibana = builder.AddContainer("kibana", "docker.elastic.co/kibana/kibana:8.17.0") var kibana = builder.AddContainer("kibana", "docker.elastic.co/kibana/kibana:8.17.0")
.WithEnvironment("ELASTICSEARCH_HOSTS", "http://elasticsearch:9200") .WithEnvironment("ELASTICSEARCH_HOSTS", "http://elasticsearch:9200")
.WithEndpoint(port: 5601, targetPort: 5601, name: "kibana") .WithEndpoint(port: 5601, targetPort: 5601, name: "http", isProxied: true, isExternal: true)
.WithReference(elasticsearch) .WithReference(elasticsearch)
.WaitFor(elasticsearch) .WaitFor(elasticsearch);
.WithLifetime(ContainerLifetime.Persistent);
if (builder.ExecutionContext.IsPublishMode)
{
kibana.WithLifetime(ContainerLifetime.Persistent);
}
// 5. Application Services // 5. Application Services
var identity = builder.AddProject<Projects.Identity_Api>("identity-service") var identity = builder.AddProject<Projects.Identity_Api>("identity-service")

View File

@ -0,0 +1,41 @@
using BuildingBlocks.HealthCheck;
using BuildingBlocks.OpenTelemetryCollector;
using Microsoft.AspNetCore.Builder;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
namespace ServiceDefaults;
public static class Extensions
{
public static IHostApplicationBuilder AddServiceDefaults(this WebApplicationBuilder builder)
{
builder.Services.AddCustomHealthCheck();
builder.AddCustomObservability();
builder.Services.AddServiceDiscovery();
builder.Services.ConfigureHttpClientDefaults(http =>
{
http.AddStandardResilienceHandler(options =>
{
var timeSpan = TimeSpan.FromMinutes(1);
options.CircuitBreaker.SamplingDuration = timeSpan * 2;
options.TotalRequestTimeout.Timeout = timeSpan * 3;
options.Retry.MaxRetryAttempts = 3;
});
// Turn on service discovery by default
http.AddServiceDiscovery();
});
return builder;
}
public static WebApplication UseServiceDefaults(this WebApplication app)
{
app.UseCustomHealthCheck();
app.UseCustomObservability();
return app;
}
}

View File

@ -0,0 +1,13 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\..\..\BuildingBlocks\BuildingBlocks.csproj" IsAspireProjectResource="false" />
</ItemGroup>
</Project>

View File

@ -3,69 +3,69 @@
<ItemGroup> <ItemGroup>
<PackageReference Include="Ardalis.GuardClauses" Version="5.0.0" /> <PackageReference Include="Ardalis.GuardClauses" Version="5.0.0" />
<PackageReference Include="Asp.Versioning.Abstractions" Version="8.1.0" /> <PackageReference Include="Asp.Versioning.Abstractions" Version="8.1.0" />
<PackageReference Include="Asp.Versioning.Http" Version="8.1.0" /> <PackageReference Include="Asp.Versioning.Http" Version="8.1.1" />
<PackageReference Include="Asp.Versioning.Mvc" Version="8.1.0" /> <PackageReference Include="Asp.Versioning.Mvc" Version="8.1.1" />
<PackageReference Include="Asp.Versioning.Mvc.ApiExplorer" Version="8.1.0" /> <PackageReference Include="Asp.Versioning.Mvc.ApiExplorer" Version="8.1.1" />
<PackageReference Include="Figgle.Fonts" Version="0.6.5" />
<PackageReference Include="Grpc.Core.Testing" Version="2.46.6" /> <PackageReference Include="Grpc.Core.Testing" Version="2.46.6" />
<PackageReference Include="EasyCaching.Core" Version="1.9.2" /> <PackageReference Include="EasyCaching.Core" Version="1.9.2" />
<PackageReference Include="EasyCaching.InMemory" Version="1.9.2" /> <PackageReference Include="EasyCaching.InMemory" Version="1.9.2" />
<PackageReference Include="EasyNetQ.Management.Client" Version="3.0.0" /> <PackageReference Include="EasyNetQ.Management.Client" Version="3.0.1" />
<PackageReference Include="EFCore.NamingConventions" Version="9.0.0" /> <PackageReference Include="EFCore.NamingConventions" Version="10.0.1" />
<PackageReference Include="Figgle" Version="0.5.1" /> <PackageReference Include="Figgle" Version="0.6.5" />
<PackageReference Include="FluentValidation" Version="11.11.0" /> <PackageReference Include="FluentValidation" Version="12.1.1" />
<PackageReference Include="FluentValidation.AspNetCore" Version="11.3.0" /> <PackageReference Include="FluentValidation.AspNetCore" Version="11.3.1" />
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="9.0.0" /> <PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="10.0.3" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="9.0.0" /> <PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="10.0.3" />
<PackageReference Include="Microsoft.Extensions.DependencyModel" Version="9.0.0" /> <PackageReference Include="Microsoft.Extensions.DependencyModel" Version="10.0.3" />
<PackageReference Include="Microsoft.Extensions.Http.Resilience" Version="9.0.0" /> <PackageReference Include="Microsoft.Extensions.Http.Resilience" Version="10.3.0" />
<PackageReference Include="Microsoft.Extensions.ServiceDiscovery" Version="9.3.1" /> <PackageReference Include="Microsoft.Extensions.ServiceDiscovery" Version="10.3.0" />
<PackageReference Include="Npgsql" Version="9.0.1" /> <PackageReference Include="Npgsql" Version="10.0.1" />
<PackageReference Include="NSubstitute" Version="5.3.0" /> <PackageReference Include="NSubstitute" Version="5.3.0" />
<PackageReference Include="Polly" Version="8.5.0" /> <PackageReference Include="Polly" Version="8.6.5" />
<PackageReference Include="Humanizer.Core" Version="2.14.1" /> <PackageReference Include="Humanizer.Core" Version="3.0.1" />
<PackageReference Include="IdGen" Version="3.0.7" /> <PackageReference Include="IdGen" Version="3.0.7" />
<PackageReference Include="Mapster" Version="7.4.0" /> <PackageReference Include="Mapster" Version="7.4.0" />
<PackageReference Include="Mapster.DependencyInjection" Version="1.0.1" /> <PackageReference Include="Mapster.DependencyInjection" Version="1.0.1" />
<PackageReference Include="MediatR" Version="12.4.1" /> <PackageReference Include="MediatR" Version="14.0.0" />
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="9.0.0" /> <PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="10.0.3" />
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="9.0.0" /> <PackageReference Include="Microsoft.EntityFrameworkCore" Version="10.0.3" />
<PackageReference Include="Microsoft.EntityFrameworkCore.InMemory" Version="9.0.0" /> <PackageReference Include="Microsoft.EntityFrameworkCore.InMemory" Version="10.0.3" />
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="9.0.0" /> <PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="10.0.3" />
<PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="9.0.0" /> <PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="10.0.3" />
<PackageReference Include="Microsoft.Extensions.Configuration.Binder" Version="9.0.0" /> <PackageReference Include="Microsoft.Extensions.Configuration.Binder" Version="10.0.3" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="9.0.0" /> <PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="10.0.3" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="9.0.0" /> <PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="10.0.3" />
<PackageReference Include="Microsoft.Extensions.Http.Polly" Version="9.0.0" /> <PackageReference Include="Microsoft.Extensions.Http.Polly" Version="10.0.3" />
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="9.0.0" /> <PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="10.0.3" />
<PackageReference Include="Microsoft.Extensions.Hosting" Version="9.0.0" /> <PackageReference Include="Microsoft.Extensions.Hosting" Version="10.0.3" />
<PackageReference Include="MongoDB.Driver" Version="3.1.0" /> <PackageReference Include="MongoDB.Driver" Version="3.6.0" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" /> <PackageReference Include="Newtonsoft.Json" Version="13.0.4" />
<PackageReference Include="Scalar.AspNetCore" Version="1.2.64" /> <PackageReference Include="Scalar.AspNetCore" Version="2.12.38" />
<PackageReference Include="Scrutor" Version="5.0.2" /> <PackageReference Include="Scrutor" Version="7.0.0" />
<PackageReference Include="Sieve" Version="2.5.5" /> <PackageReference Include="Sieve" Version="2.5.5" />
<PackageReference Include="Swashbuckle.AspNetCore" Version="7.1.0" /> <PackageReference Include="Swashbuckle.AspNetCore" Version="10.1.2" />
<PackageReference Include="Swashbuckle.AspNetCore.SwaggerGen" Version="7.1.0" /> <PackageReference Include="Swashbuckle.AspNetCore.SwaggerGen" Version="10.1.2" />
<PackageReference Include="Swashbuckle.AspNetCore.SwaggerUI" Version="7.1.0" /> <PackageReference Include="Swashbuckle.AspNetCore.SwaggerUI" Version="10.1.2" />
<PackageReference Include="MassTransit" Version="8.3.6" /> <PackageReference Include="MassTransit" Version="8.5.8" />
<PackageReference Include="MassTransit.RabbitMQ" Version="8.3.6" /> <PackageReference Include="MassTransit.RabbitMQ" Version="8.5.8" />
<PackageReference Include="Duende.IdentityServer" Version="7.0.8" /> <PackageReference Include="Duende.IdentityServer" Version="7.4.5" />
<PackageReference Include="Duende.IdentityServer.AspNetIdentity" Version="7.0.8" /> <PackageReference Include="Duende.IdentityServer.AspNetIdentity" Version="7.4.5" />
<PackageReference Include="Duende.IdentityServer.EntityFramework" Version="7.0.8" /> <PackageReference Include="Duende.IdentityServer.EntityFramework" Version="7.4.5" />
<PackageReference Include="Duende.IdentityServer.EntityFramework.Storage" Version="7.0.8" /> <PackageReference Include="Duende.IdentityServer.EntityFramework.Storage" Version="7.4.5" />
<PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="9.0.0" /> <PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="10.0.3" />
<PackageReference Include="System.Linq.Async" Version="6.0.1" /> <PackageReference Include="System.Linq.Async" Version="7.0.0" />
<PackageReference Include="System.Linq.Async.Queryable" Version="6.0.1" /> <PackageReference Include="System.Linq.Async.Queryable" Version="7.0.0" />
<PackageReference Include="Testcontainers" Version="4.0.0" /> <PackageReference Include="Testcontainers" Version="4.9.0" />
<PackageReference Include="Testcontainers.EventStoreDb" Version="4.0.0" /> <PackageReference Include="Testcontainers.EventStoreDb" Version="4.9.0" />
<PackageReference Include="Testcontainers.MongoDb" Version="4.0.0" /> <PackageReference Include="Testcontainers.MongoDb" Version="4.9.0" />
<PackageReference Include="Testcontainers.PostgreSql" Version="4.0.0" /> <PackageReference Include="Testcontainers.PostgreSql" Version="4.9.0" />
<PackageReference Include="Testcontainers.RabbitMq" Version="4.0.0" /> <PackageReference Include="Testcontainers.RabbitMq" Version="4.9.0" />
<PackageReference Include="Unchase.Swashbuckle.AspNetCore.Extensions" Version="2.7.1" />
<PackageReference Include="xunit.abstractions" Version="2.0.3" /> <PackageReference Include="xunit.abstractions" Version="2.0.3" />
<PackageReference Include="xunit.extensibility.core" Version="2.9.2" /> <PackageReference Include="xunit.extensibility.core" Version="2.9.3" />
<PackageReference Include="Xunit.Extensions.Logging" Version="1.1.0" /> <PackageReference Include="Xunit.Extensions.Logging" Version="1.1.0" />
<PackageReference Include="Yarp.ReverseProxy" Version="2.2.0" /> <PackageReference Include="Yarp.ReverseProxy" Version="2.3.0" />
<PackageReference Include="Microsoft.Extensions.Diagnostics.HealthChecks" Version="9.0.0" /> <PackageReference Include="Microsoft.Extensions.Diagnostics.HealthChecks" Version="10.0.3" />
<PackageReference Include="AspNetCore.HealthChecks.UI" Version="9.0.0" /> <PackageReference Include="AspNetCore.HealthChecks.UI" Version="9.0.0" />
<PackageReference Include="AspNetCore.HealthChecks.UI.Client" Version="9.0.0" /> <PackageReference Include="AspNetCore.HealthChecks.UI.Client" Version="9.0.0" />
<PackageReference Include="AspNetCore.HealthChecks.UI.InMemory.Storage" Version="9.0.0" /> <PackageReference Include="AspNetCore.HealthChecks.UI.InMemory.Storage" Version="9.0.0" />
@ -74,34 +74,34 @@
<PackageReference Include="AspNetCore.HealthChecks.Rabbitmq" Version="9.0.0" /> <PackageReference Include="AspNetCore.HealthChecks.Rabbitmq" Version="9.0.0" />
<PackageReference Include="AspNetCore.HealthChecks.MongoDb" Version="9.0.0" /> <PackageReference Include="AspNetCore.HealthChecks.MongoDb" Version="9.0.0" />
<PackageReference Include="Npgsql.OpenTelemetry" Version="9.0.1" /> <PackageReference Include="Npgsql.OpenTelemetry" Version="10.0.1" />
<PackageReference Include="OpenTelemetry.Exporter.OpenTelemetryProtocol" Version="1.11.1" /> <PackageReference Include="OpenTelemetry.Exporter.OpenTelemetryProtocol" Version="1.15.0" />
<PackageReference Include="OpenTelemetry.Extensions.Hosting" Version="1.11.1"/> <PackageReference Include="OpenTelemetry.Extensions.Hosting" Version="1.15.0" />
<PackageReference Include="OpenTelemetry.Instrumentation.AspNetCore" Version="1.11.0"/> <PackageReference Include="OpenTelemetry.Instrumentation.AspNetCore" Version="1.15.0" />
<PackageReference Include="OpenTelemetry.Instrumentation.GrpcNetClient" Version="1.11.0-beta.1"/> <PackageReference Include="OpenTelemetry.Instrumentation.GrpcNetClient" Version="1.15.0-beta.1" />
<PackageReference Include="OpenTelemetry.Instrumentation.Http" Version="1.11.0"/> <PackageReference Include="OpenTelemetry.Instrumentation.Http" Version="1.15.0" />
<PackageReference Include="OpenTelemetry.Instrumentation.Process" Version="1.11.0-beta.1"/> <PackageReference Include="OpenTelemetry.Instrumentation.Process" Version="1.15.0-beta.1" />
<PackageReference Include="OpenTelemetry.Instrumentation.Runtime" Version="1.11.0"/> <PackageReference Include="OpenTelemetry.Instrumentation.Runtime" Version="1.15.0" />
<PackageReference Include="OpenTelemetry.Exporter.Prometheus.AspNetCore" Version="1.11.0-beta.1"/> <PackageReference Include="OpenTelemetry.Exporter.Prometheus.AspNetCore" Version="1.15.0-beta.1" />
<PackageReference Include="Grafana.OpenTelemetry" Version="1.2.0"/> <PackageReference Include="Grafana.OpenTelemetry" Version="1.5.2" />
<PackageReference Include="OpenTelemetry.Exporter.Console" Version="1.11.1"/> <PackageReference Include="OpenTelemetry.Exporter.Console" Version="1.15.0" />
<PackageReference Include="OpenTelemetry.Exporter.Zipkin" Version="1.11.1"/> <PackageReference Include="OpenTelemetry.Exporter.Zipkin" Version="1.15.0" />
<PackageReference Include="EventStore.Client.Grpc.Streams" Version="23.3.7" /> <PackageReference Include="EventStore.Client.Grpc.Streams" Version="23.3.9" />
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="9.0.1" /> <PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="10.0.0" />
<PackageReference Include="AutoBogus" Version="2.13.1" /> <PackageReference Include="AutoBogus" Version="2.13.1" />
<PackageReference Include="Bogus" Version="35.6.1" /> <PackageReference Include="Bogus" Version="35.6.5" />
<PackageReference Include="FluentAssertions" Version="7.0.0" /> <PackageReference Include="FluentAssertions" Version="8.8.0" />
<PackageReference Include="Respawn" Version="6.2.1" /> <PackageReference Include="Respawn" Version="7.0.0" />
<PackageReference Include="Microsoft.AspNetCore.TestHost" Version="9.0.0" /> <PackageReference Include="Microsoft.AspNetCore.TestHost" Version="10.0.3" />
<PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" Version="9.0.0" /> <PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" Version="10.0.3" />
<PackageReference Include="WebMotions.Fake.Authentication.JwtBearer" Version="8.0.1" /> <PackageReference Include="WebMotions.Fake.Authentication.JwtBearer" Version="10.0.0" />
<PackageReference Include="Google.Protobuf" Version="3.29.1" /> <PackageReference Include="Google.Protobuf" Version="3.33.5" />
<PackageReference Include="Grpc.Net.ClientFactory" Version="2.67.0" /> <PackageReference Include="Grpc.Net.ClientFactory" Version="2.76.0" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="9.0.0"> <PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="10.0.3">
<PrivateAssets>all</PrivateAssets> <PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference> </PackageReference>

View File

@ -1,5 +1,5 @@
using Microsoft.AspNetCore.OpenApi; using Microsoft.AspNetCore.OpenApi;
using Microsoft.OpenApi.Models; using Microsoft.OpenApi;
public class SecuritySchemeDocumentTransformer : IOpenApiDocumentTransformer public class SecuritySchemeDocumentTransformer : IOpenApiDocumentTransformer
{ {
@ -9,12 +9,14 @@ public class SecuritySchemeDocumentTransformer : IOpenApiDocumentTransformer
CancellationToken cancellationToken CancellationToken cancellationToken
) )
{ {
document.Components ??= new(); document.Components ??= new OpenApiComponents();
// Bearer token scheme // Initialize with the correct interface type
document.Components.SecuritySchemes.Add( document.Components.SecuritySchemes ??= new Dictionary<string, IOpenApiSecurityScheme>();
"Bearer",
new OpenApiSecurityScheme var securitySchemes = new Dictionary<string, IOpenApiSecurityScheme>
{
["Bearer"] = new OpenApiSecurityScheme
{ {
Name = "Authorization", Name = "Authorization",
Type = SecuritySchemeType.Http, Type = SecuritySchemeType.Http,
@ -23,21 +25,23 @@ public class SecuritySchemeDocumentTransformer : IOpenApiDocumentTransformer
In = ParameterLocation.Header, In = ParameterLocation.Header,
Description = Description =
"Enter 'Bearer' [space] and your token in the text input below.\n\nExample: 'Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...'", "Enter 'Bearer' [space] and your token in the text input below.\n\nExample: 'Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...'",
} },
); ["ApiKey"] = new OpenApiSecurityScheme
// API Key scheme
document.Components.SecuritySchemes.Add(
"ApiKey",
new OpenApiSecurityScheme
{ {
Name = "X-API-KEY", Name = "X-API-KEY",
Type = SecuritySchemeType.ApiKey, Type = SecuritySchemeType.ApiKey,
In = ParameterLocation.Header, In = ParameterLocation.Header,
Description = Description = "Enter your API key in the text input below.\n\nExample: '12345-abcdef'",
"Enter your API key in the text input below.\n\nExample: '12345-abcdef'", },
};
foreach (var (key, scheme) in securitySchemes)
{
if (!document.Components.SecuritySchemes.ContainsKey(key))
{
document.Components.SecuritySchemes.Add(key, scheme);
} }
); }
return Task.CompletedTask; return Task.CompletedTask;
} }

View File

@ -162,10 +162,7 @@ public static class Extensions
{ {
instrumentationOptions.RecordException = true; instrumentationOptions.RecordException = true;
}) })
.AddEntityFrameworkCoreInstrumentation(instrumentationOptions => .AddEntityFrameworkCoreInstrumentation()
{
instrumentationOptions.SetDbStatementForText = true;
})
.AddSource(DiagnosticHeaders.DefaultListenerName) .AddSource(DiagnosticHeaders.DefaultListenerName)
.AddNpgsql() .AddNpgsql()
// `AddSource` for adding custom activity sources // `AddSource` for adding custom activity sources

View File

@ -1,6 +1,5 @@
using System.Net; using System.Net;
using System.Security.Claims; using System.Security.Claims;
using Ardalis.GuardClauses;
using BuildingBlocks.Core.Event; using BuildingBlocks.Core.Event;
using BuildingBlocks.Core.Model; using BuildingBlocks.Core.Model;
using BuildingBlocks.EFCore; using BuildingBlocks.EFCore;
@ -13,7 +12,6 @@ using Grpc.Net.Client;
using MassTransit; using MassTransit;
using MassTransit.Testing; using MassTransit.Testing;
using MediatR; using MediatR;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc.Testing; using Microsoft.AspNetCore.Mvc.Testing;
@ -38,11 +36,11 @@ using Testcontainers.PostgreSql;
using Testcontainers.RabbitMq; using Testcontainers.RabbitMq;
public class TestFixture<TEntryPoint> : IAsyncLifetime public class TestFixture<TEntryPoint> : IAsyncLifetime
where TEntryPoint : class where TEntryPoint : class
{ {
private readonly WebApplicationFactory<TEntryPoint> _factory; private readonly WebApplicationFactory<TEntryPoint> _factory;
private int Timeout => 120; // Second private int Timeout => 120; // Second
private ITestHarness TestHarness => ServiceProvider?.GetTestHarness(); public ITestHarness TestHarness => ServiceProvider?.GetTestHarness();
private Action<IServiceCollection> TestRegistrationServices { get; set; } private Action<IServiceCollection> TestRegistrationServices { get; set; }
private PostgreSqlContainer PostgresTestcontainer; private PostgreSqlContainer PostgresTestcontainer;
private PostgreSqlContainer PostgresPersistTestContainer; private PostgreSqlContainer PostgresPersistTestContainer;
@ -59,11 +57,11 @@ where TEntryPoint : class
get get
{ {
var claims = new Dictionary<string, object> var claims = new Dictionary<string, object>
{ {
{ ClaimTypes.Name, "test@sample.com" }, { ClaimTypes.Name, "test@sample.com" },
{ ClaimTypes.Role, "admin" }, { ClaimTypes.Role, "admin" },
{ "scope", "flight-api" } { "scope", "flight-api" },
}; };
var httpClient = _factory.CreateClient(); var httpClient = _factory.CreateClient();
httpClient.SetFakeBearerToken(claims); // Uses FakeJwtBearer httpClient.SetFakeBearerToken(claims); // Uses FakeJwtBearer
@ -72,9 +70,7 @@ where TEntryPoint : class
} }
public GrpcChannel Channel => public GrpcChannel Channel =>
GrpcChannel.ForAddress( GrpcChannel.ForAddress(HttpClient.BaseAddress!, new GrpcChannelOptions { HttpClient = HttpClient });
HttpClient.BaseAddress!,
new GrpcChannelOptions { HttpClient = HttpClient });
public IServiceProvider ServiceProvider => _factory?.Services; public IServiceProvider ServiceProvider => _factory?.Services;
public IConfiguration Configuration => _factory?.Services.GetRequiredService<IConfiguration>(); public IConfiguration Configuration => _factory?.Services.GetRequiredService<IConfiguration>();
@ -82,54 +78,55 @@ where TEntryPoint : class
protected TestFixture() protected TestFixture()
{ {
_factory = new WebApplicationFactory<TEntryPoint>() _factory = new WebApplicationFactory<TEntryPoint>().WithWebHostBuilder(builder =>
.WithWebHostBuilder( {
builder => builder.ConfigureAppConfiguration(AddCustomAppSettings);
builder.UseEnvironment("test");
builder.ConfigureServices(services =>
{
TestRegistrationServices?.Invoke(services);
services.ReplaceSingleton(AddHttpContextAccessorMock);
services.AddSingleton<PersistMessageBackgroundService>();
services.RemoveHostedService<PersistMessageBackgroundService>();
// Register all ITestDataSeeder implementations dynamically
services.Scan(scan =>
scan.FromApplicationDependencies() // Scan the current app and its dependencies
.AddClasses(classes => classes.AssignableTo<ITestDataSeeder>()) // Find classes that implement ITestDataSeeder
.AsImplementedInterfaces()
.WithScopedLifetime()
);
// Add Fake JWT Authentication - we can use SetAdminUser method to set authenticate user to existing HttContextAccessor
// https://github.com/webmotions/fake-authentication-jwtbearer
// https://github.com/webmotions/fake-authentication-jwtbearer/issues/14
services
.AddAuthentication(options =>
{
options.DefaultAuthenticateScheme = FakeJwtBearerDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = FakeJwtBearerDefaults.AuthenticationScheme;
})
.AddFakeJwtBearer();
// Mock Authorization Policies
services.AddAuthorization(options =>
{ {
builder.ConfigureAppConfiguration(AddCustomAppSettings); options.AddPolicy(
nameof(ApiScope),
builder.UseEnvironment("test"); policy =>
builder.ConfigureServices(
services =>
{ {
TestRegistrationServices?.Invoke(services); policy.AddAuthenticationSchemes(FakeJwtBearerDefaults.AuthenticationScheme);
services.ReplaceSingleton(AddHttpContextAccessorMock); policy.RequireAuthenticatedUser();
policy.RequireClaim("scope", "flight-api"); // Test-specific scope
services.AddSingleton<PersistMessageBackgroundService>(); }
services.RemoveHostedService<PersistMessageBackgroundService>(); );
// Register all ITestDataSeeder implementations dynamically
services.Scan(scan => scan
.FromApplicationDependencies() // Scan the current app and its dependencies
.AddClasses(classes => classes.AssignableTo<ITestDataSeeder>()) // Find classes that implement ITestDataSeeder
.AsImplementedInterfaces()
.WithScopedLifetime());
// Add Fake JWT Authentication - we can use SetAdminUser method to set authenticate user to existing HttContextAccessor
// https://github.com/webmotions/fake-authentication-jwtbearer
// https://github.com/webmotions/fake-authentication-jwtbearer/issues/14
services.AddAuthentication(
options =>
{
options.DefaultAuthenticateScheme = FakeJwtBearerDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = FakeJwtBearerDefaults.AuthenticationScheme;
})
.AddFakeJwtBearer();
// Mock Authorization Policies
services.AddAuthorization(options =>
{
options.AddPolicy(nameof(ApiScope), policy =>
{
policy.AddAuthenticationSchemes(FakeJwtBearerDefaults.AuthenticationScheme);
policy.RequireAuthenticatedUser();
policy.RequireClaim("scope", "flight-api"); // Test-specific scope
});
});
});
}); });
});
});
} }
public async Task InitializeAsync() public async Task InitializeAsync()
@ -157,10 +154,10 @@ where TEntryPoint : class
return null; return null;
var loggerFactory = LoggerFactory.Create(builder => var loggerFactory = LoggerFactory.Create(builder =>
{ {
builder.AddXunit(output); builder.AddXunit(output);
builder.SetMinimumLevel(LogLevel.Debug); builder.SetMinimumLevel(LogLevel.Debug);
}); });
return loggerFactory.CreateLogger("TestLogger"); return loggerFactory.CreateLogger("TestLogger");
} }
@ -179,132 +176,93 @@ where TEntryPoint : class
return result; return result;
} }
public Task<TResponse> SendAsync<TResponse>(IRequest<TResponse> request) public Task<TResponse> SendAsync<TResponse>(IRequest<TResponse> request)
{ {
return ExecuteScopeAsync( return ExecuteScopeAsync(sp =>
sp => {
{ var mediator = sp.GetRequiredService<IMediator>();
var mediator = sp.GetRequiredService<IMediator>();
return mediator.Send(request); return mediator.Send(request);
}); });
} }
public Task SendAsync(IRequest request) public Task SendAsync(IRequest request)
{ {
return ExecuteScopeAsync( return ExecuteScopeAsync(sp =>
sp => {
{ var mediator = sp.GetRequiredService<IMediator>();
var mediator = sp.GetRequiredService<IMediator>(); return mediator.Send(request);
return mediator.Send(request); });
});
} }
public async Task Publish<TMessage>( public async Task Publish<TMessage>(TMessage message, CancellationToken cancellationToken = default)
TMessage message, where TMessage : class, IEvent
CancellationToken cancellationToken = default
)
where TMessage : class, IEvent
{ {
// Use harness bus to ensure publish happens only after the bus is started.
await TestHarness.Bus.Publish(message, cancellationToken); await TestHarness.Bus.Publish(message, cancellationToken);
} }
public async Task<bool> WaitForPublishing<TMessage>( public async Task<bool> WaitForPublishing<TMessage>(CancellationToken cancellationToken = default)
CancellationToken cancellationToken = default where TMessage : class, IEvent
)
where TMessage : class, IEvent
{ {
var result = await WaitUntilConditionMet( var result = await WaitUntilConditionMet(
async () => async () =>
{ {
var published = var published = await TestHarness.Published.Any<TMessage>(cancellationToken);
await TestHarness.Published.Any<TMessage>(cancellationToken);
return published; return published;
}); },
cancellationToken: cancellationToken
);
return result; return result;
} }
public async Task<bool> WaitForConsuming<TMessage>( public Task<bool> WaitUntilAsync(
Func<Task<bool>> condition,
TimeSpan? timeout = null,
TimeSpan? pollInterval = null,
CancellationToken cancellationToken = default CancellationToken cancellationToken = default
) )
where TMessage : class, IEvent
{ {
var result = await WaitUntilConditionMet( var effectiveTimeout = timeout ?? TimeSpan.FromSeconds(Timeout);
async () => var effectivePollInterval = pollInterval ?? TimeSpan.FromMilliseconds(200);
{
var consumed =
await TestHarness.Consumed.Any<TMessage>(cancellationToken);
return consumed; return WaitUntilConditionMet(
}); conditionToMet: async () =>
{
return result; cancellationToken.ThrowIfCancellationRequested();
} return await condition();
},
public async Task<bool> ShouldProcessedPersistInternalCommand<TInternalCommand>( timeoutSecond: (int)Math.Ceiling(effectiveTimeout.TotalSeconds),
CancellationToken cancellationToken = default pollInterval: effectivePollInterval,
) cancellationToken: cancellationToken
where TInternalCommand : class, IInternalCommand );
{
var result = await WaitUntilConditionMet(
async () =>
{
return await ExecuteScopeAsync(
async sp =>
{
var persistMessageProcessor =
sp.GetService<IPersistMessageProcessor>();
Guard.Against.Null(
persistMessageProcessor,
nameof(persistMessageProcessor));
var filter =
await persistMessageProcessor.GetByFilterAsync(
x =>
x.DeliveryType ==
MessageDeliveryType.Internal &&
typeof(TInternalCommand).ToString() ==
x.DataType);
var res = filter.Any(
x => x.MessageStatus == MessageStatus.Processed);
return res;
});
});
return result;
} }
// Ref: https://tech.energyhelpline.com/in-memory-testing-with-masstransit/ // Ref: https://tech.energyhelpline.com/in-memory-testing-with-masstransit/
private async Task<bool> WaitUntilConditionMet( private async Task<bool> WaitUntilConditionMet(
Func<Task<bool>> conditionToMet, Func<Task<bool>> conditionToMet,
int? timeoutSecond = null int? timeoutSecond = null,
TimeSpan? pollInterval = null,
CancellationToken cancellationToken = default
) )
{ {
var time = timeoutSecond ?? Timeout; var time = timeoutSecond ?? Timeout;
var delay = pollInterval ?? TimeSpan.FromMilliseconds(100);
var startTime = DateTime.Now; var startTime = DateTime.UtcNow;
var timeoutExpired = false; while (DateTime.UtcNow - startTime <= TimeSpan.FromSeconds(time))
var meet = await conditionToMet.Invoke();
while (!meet)
{ {
if (timeoutExpired) cancellationToken.ThrowIfCancellationRequested();
{
return false;
}
await Task.Delay(100); if (await conditionToMet.Invoke())
meet = await conditionToMet.Invoke(); return true;
timeoutExpired = DateTime.Now - startTime > TimeSpan.FromSeconds(time);
await Task.Delay(delay, cancellationToken);
} }
return true; return false;
} }
private async Task StartTestContainerAsync() private async Task StartTestContainerAsync()
@ -337,39 +295,25 @@ where TEntryPoint : class
configuration.AddInMemoryCollection( configuration.AddInMemoryCollection(
new KeyValuePair<string, string>[] new KeyValuePair<string, string>[]
{ {
new( new("PostgresOptions:ConnectionString", PostgresTestcontainer.GetConnectionString()),
"PostgresOptions:ConnectionString", new("PostgresOptions:ConnectionString:Flight", PostgresTestcontainer.GetConnectionString()),
PostgresTestcontainer.GetConnectionString()), new("PostgresOptions:ConnectionString:Identity", PostgresTestcontainer.GetConnectionString()),
new( new("PostgresOptions:ConnectionString:Passenger", PostgresTestcontainer.GetConnectionString()),
"PostgresOptions:ConnectionString:Flight", new("PersistMessageOptions:ConnectionString", PostgresPersistTestContainer.GetConnectionString()),
PostgresTestcontainer.GetConnectionString()), new("RabbitMqOptions:HostName", "127.0.0.1"),
new( new("RabbitMqOptions:UserName", TestContainers.RabbitMqContainerConfiguration.UserName),
"PostgresOptions:ConnectionString:Identity", new("RabbitMqOptions:Password", TestContainers.RabbitMqContainerConfiguration.Password),
PostgresTestcontainer.GetConnectionString()),
new(
"PostgresOptions:ConnectionString:Passenger",
PostgresTestcontainer.GetConnectionString()),
new(
"PersistMessageOptions:ConnectionString",
PostgresPersistTestContainer.GetConnectionString()),
new("RabbitMqOptions:HostName", RabbitMqTestContainer.Hostname),
new(
"RabbitMqOptions:UserName",
TestContainers.RabbitMqContainerConfiguration.UserName),
new(
"RabbitMqOptions:Password",
TestContainers.RabbitMqContainerConfiguration.Password),
new( new(
"RabbitMqOptions:Port", "RabbitMqOptions:Port",
RabbitMqTestContainer.GetMappedPublicPort( RabbitMqTestContainer
TestContainers.RabbitMqContainerConfiguration.Port) .GetMappedPublicPort(TestContainers.RabbitMqContainerConfiguration.Port)
.ToString(NumberFormatInfo.InvariantInfo)), .ToString(NumberFormatInfo.InvariantInfo)
),
new("MongoOptions:ConnectionString", MongoDbTestContainer.GetConnectionString()), new("MongoOptions:ConnectionString", MongoDbTestContainer.GetConnectionString()),
new("MongoOptions:DatabaseName", TestContainers.MongoContainerConfiguration.Name), new("MongoOptions:DatabaseName", TestContainers.MongoContainerConfiguration.Name),
new( new("EventStoreOptions:ConnectionString", EventStoreDbTestContainer.GetConnectionString()),
"EventStoreOptions:ConnectionString", }
EventStoreDbTestContainer.GetConnectionString()) );
});
} }
private IHttpContextAccessor AddHttpContextAccessorMock(IServiceProvider serviceProvider) private IHttpContextAccessor AddHttpContextAccessorMock(IServiceProvider serviceProvider)
@ -377,8 +321,7 @@ where TEntryPoint : class
var httpContextAccessorMock = Substitute.For<IHttpContextAccessor>(); var httpContextAccessorMock = Substitute.For<IHttpContextAccessor>();
using var scope = serviceProvider.CreateScope(); using var scope = serviceProvider.CreateScope();
httpContextAccessorMock.HttpContext = new DefaultHttpContext httpContextAccessorMock.HttpContext = new DefaultHttpContext { RequestServices = scope.ServiceProvider };
{ RequestServices = scope.ServiceProvider };
httpContextAccessorMock.HttpContext.Request.Host = new HostString("localhost", 6012); httpContextAccessorMock.HttpContext.Request.Host = new HostString("localhost", 6012);
httpContextAccessorMock.HttpContext.Request.Scheme = "http"; httpContextAccessorMock.HttpContext.Request.Scheme = "http";
@ -388,100 +331,90 @@ where TEntryPoint : class
} }
public class TestWriteFixture<TEntryPoint, TWContext> : TestFixture<TEntryPoint> public class TestWriteFixture<TEntryPoint, TWContext> : TestFixture<TEntryPoint>
where TEntryPoint : class where TEntryPoint : class
where TWContext : DbContext where TWContext : DbContext
{ {
public Task ExecuteDbContextAsync(Func<TWContext, Task> action) public Task ExecuteDbContextAsync(Func<TWContext, Task> action)
{ {
return ExecuteScopeAsync(sp => action(sp.GetService<TWContext>())); return ExecuteScopeAsync(sp => action(sp.GetRequiredService<TWContext>()));
} }
public Task ExecuteDbContextAsync(Func<TWContext, ValueTask> action) public Task ExecuteDbContextAsync(Func<TWContext, ValueTask> action)
{ {
return ExecuteScopeAsync(sp => action(sp.GetService<TWContext>()).AsTask()); return ExecuteScopeAsync(sp => action(sp.GetRequiredService<TWContext>()).AsTask());
} }
public Task ExecuteDbContextAsync(Func<TWContext, IMediator, Task> action) public Task ExecuteDbContextAsync(Func<TWContext, IMediator, Task> action)
{ {
return ExecuteScopeAsync( return ExecuteScopeAsync(sp => action(sp.GetRequiredService<TWContext>(), sp.GetRequiredService<IMediator>()));
sp => action(sp.GetService<TWContext>(), sp.GetService<IMediator>()));
} }
public Task<T> ExecuteDbContextAsync<T>(Func<TWContext, Task<T>> action) public Task<T> ExecuteDbContextAsync<T>(Func<TWContext, Task<T>> action)
{ {
return ExecuteScopeAsync(sp => action(sp.GetService<TWContext>())); return ExecuteScopeAsync(sp => action(sp.GetRequiredService<TWContext>()));
} }
public Task<T> ExecuteDbContextAsync<T>(Func<TWContext, ValueTask<T>> action) public Task<T> ExecuteDbContextAsync<T>(Func<TWContext, ValueTask<T>> action)
{ {
return ExecuteScopeAsync(sp => action(sp.GetService<TWContext>()).AsTask()); return ExecuteScopeAsync(sp => action(sp.GetRequiredService<TWContext>()).AsTask());
} }
public Task<T> ExecuteDbContextAsync<T>(Func<TWContext, IMediator, Task<T>> action) public Task<T> ExecuteDbContextAsync<T>(Func<TWContext, IMediator, Task<T>> action)
{ {
return ExecuteScopeAsync( return ExecuteScopeAsync(sp => action(sp.GetRequiredService<TWContext>(), sp.GetRequiredService<IMediator>()));
sp => action(sp.GetService<TWContext>(), sp.GetService<IMediator>()));
} }
public Task InsertAsync<T>(params T[] entities) public Task InsertAsync<T>(params T[] entities)
where T : class where T : class
{ {
return ExecuteDbContextAsync( return ExecuteDbContextAsync(db =>
db => {
foreach (var entity in entities)
{ {
foreach (var entity in entities) db.Set<T>().Add(entity);
{ }
db.Set<T>().Add(entity);
}
return db.SaveChangesAsync(); return db.SaveChangesAsync();
}); });
} }
public async Task InsertAsync<TEntity>(TEntity entity) public async Task InsertAsync<TEntity>(TEntity entity)
where TEntity : class where TEntity : class
{ {
await ExecuteDbContextAsync( await ExecuteDbContextAsync(db =>
db => {
{ db.Set<TEntity>().Add(entity);
db.Set<TEntity>().Add(entity);
return db.SaveChangesAsync(); return db.SaveChangesAsync();
}); });
} }
public Task InsertAsync<TEntity, TEntity2>(TEntity entity, TEntity2 entity2) public Task InsertAsync<TEntity, TEntity2>(TEntity entity, TEntity2 entity2)
where TEntity : class where TEntity : class
where TEntity2 : class where TEntity2 : class
{ {
return ExecuteDbContextAsync( return ExecuteDbContextAsync(db =>
db => {
{ db.Set<TEntity>().Add(entity);
db.Set<TEntity>().Add(entity); db.Set<TEntity2>().Add(entity2);
db.Set<TEntity2>().Add(entity2);
return db.SaveChangesAsync(); return db.SaveChangesAsync();
}); });
} }
public Task InsertAsync<TEntity, TEntity2, TEntity3>( public Task InsertAsync<TEntity, TEntity2, TEntity3>(TEntity entity, TEntity2 entity2, TEntity3 entity3)
TEntity entity, where TEntity : class
TEntity2 entity2, where TEntity2 : class
TEntity3 entity3 where TEntity3 : class
)
where TEntity : class
where TEntity2 : class
where TEntity3 : class
{ {
return ExecuteDbContextAsync( return ExecuteDbContextAsync(db =>
db => {
{ db.Set<TEntity>().Add(entity);
db.Set<TEntity>().Add(entity); db.Set<TEntity2>().Add(entity2);
db.Set<TEntity2>().Add(entity2); db.Set<TEntity3>().Add(entity3);
db.Set<TEntity3>().Add(entity3);
return db.SaveChangesAsync(); return db.SaveChangesAsync();
}); });
} }
public Task InsertAsync<TEntity, TEntity2, TEntity3, TEntity4>( public Task InsertAsync<TEntity, TEntity2, TEntity3, TEntity4>(
@ -490,39 +423,38 @@ where TWContext : DbContext
TEntity3 entity3, TEntity3 entity3,
TEntity4 entity4 TEntity4 entity4
) )
where TEntity : class where TEntity : class
where TEntity2 : class where TEntity2 : class
where TEntity3 : class where TEntity3 : class
where TEntity4 : class where TEntity4 : class
{ {
return ExecuteDbContextAsync( return ExecuteDbContextAsync(db =>
db => {
{ db.Set<TEntity>().Add(entity);
db.Set<TEntity>().Add(entity); db.Set<TEntity2>().Add(entity2);
db.Set<TEntity2>().Add(entity2); db.Set<TEntity3>().Add(entity3);
db.Set<TEntity3>().Add(entity3); db.Set<TEntity4>().Add(entity4);
db.Set<TEntity4>().Add(entity4);
return db.SaveChangesAsync(); return db.SaveChangesAsync();
}); });
} }
public Task<T> FindAsync<T, TKey>(TKey id) public Task<T> FindAsync<T, TKey>(TKey id)
where T : class, IEntity where T : class, IEntity
{ {
return ExecuteDbContextAsync(db => db.Set<T>().FindAsync(id).AsTask()); return ExecuteDbContextAsync(db => db.Set<T>().FindAsync(id).AsTask());
} }
public Task<T> FirstOrDefaultAsync<T>() public Task<T> FirstOrDefaultAsync<T>()
where T : class, IEntity where T : class, IEntity
{ {
return ExecuteDbContextAsync(db => db.Set<T>().FirstOrDefaultAsync()); return ExecuteDbContextAsync(db => db.Set<T>().FirstOrDefaultAsync());
} }
} }
public class TestReadFixture<TEntryPoint, TRContext> : TestFixture<TEntryPoint> public class TestReadFixture<TEntryPoint, TRContext> : TestFixture<TEntryPoint>
where TEntryPoint : class where TEntryPoint : class
where TRContext : MongoDbContext where TRContext : MongoDbContext
{ {
public Task ExecuteReadContextAsync(Func<TRContext, Task> action) public Task ExecuteReadContextAsync(Func<TRContext, Task> action)
{ {
@ -535,21 +467,19 @@ where TRContext : MongoDbContext
} }
public async Task InsertMongoDbContextAsync<T>(string collectionName, params T[] entities) public async Task InsertMongoDbContextAsync<T>(string collectionName, params T[] entities)
where T : class where T : class
{ {
await ExecuteReadContextAsync( await ExecuteReadContextAsync(async db =>
async db => {
{ await db.GetCollection<T>(collectionName).InsertManyAsync(entities.ToList());
await db.GetCollection<T>(collectionName).InsertManyAsync(entities.ToList()); });
});
} }
} }
public class TestFixture<TEntryPoint, TWContext, TRContext> public class TestFixture<TEntryPoint, TWContext, TRContext> : TestWriteFixture<TEntryPoint, TWContext>
: TestWriteFixture<TEntryPoint, TWContext> where TEntryPoint : class
where TEntryPoint : class where TWContext : DbContext
where TWContext : DbContext where TRContext : MongoDbContext
where TRContext : MongoDbContext
{ {
public Task ExecuteReadContextAsync(Func<TRContext, Task> action) public Task ExecuteReadContextAsync(Func<TRContext, Task> action)
{ {
@ -562,38 +492,38 @@ where TRContext : MongoDbContext
} }
public async Task InsertMongoDbContextAsync<T>(string collectionName, params T[] entities) public async Task InsertMongoDbContextAsync<T>(string collectionName, params T[] entities)
where T : class where T : class
{ {
await ExecuteReadContextAsync( await ExecuteReadContextAsync(async db =>
async db => {
{ await db.GetCollection<T>(collectionName).InsertManyAsync(entities.ToList());
await db.GetCollection<T>(collectionName).InsertManyAsync(entities.ToList()); });
});
} }
} }
public class TestFixtureCore<TEntryPoint> : IAsyncLifetime public class TestFixtureCore<TEntryPoint> : IAsyncLifetime
where TEntryPoint : class where TEntryPoint : class
{ {
private Respawner _reSpawnerDefaultDb; private Respawner _reSpawnerDefaultDb;
private Respawner _reSpawnerPersistDb; private Respawner _reSpawnerPersistDb;
private NpgsqlConnection DefaultDbConnection { get; set; } private NpgsqlConnection DefaultDbConnection { get; set; }
private NpgsqlConnection PersistDbConnection { get; set; } private NpgsqlConnection PersistDbConnection { get; set; }
private Type _dbContextType;
public TestFixtureCore( public TestFixtureCore(
TestFixture<TEntryPoint> integrationTestFixture, TestFixture<TEntryPoint> integrationTestFixture,
ITestOutputHelper outputHelper ITestOutputHelper outputHelper,
Type dbContextType = null
) )
{ {
Fixture = integrationTestFixture; Fixture = integrationTestFixture;
integrationTestFixture.RegisterServices(RegisterTestsServices); integrationTestFixture.RegisterServices(RegisterTestsServices);
integrationTestFixture.Logger = integrationTestFixture.CreateLogger(outputHelper); integrationTestFixture.Logger = integrationTestFixture.CreateLogger(outputHelper);
_dbContextType = dbContextType;
} }
public TestFixture<TEntryPoint> Fixture { get; } public TestFixture<TEntryPoint> Fixture { get; }
public async Task InitializeAsync() public async Task InitializeAsync()
{ {
await InitPostgresAsync(); await InitPostgresAsync();
@ -613,25 +543,37 @@ where TEntryPoint : class
if (!string.IsNullOrEmpty(persistOptions?.ConnectionString)) if (!string.IsNullOrEmpty(persistOptions?.ConnectionString))
{ {
await Fixture.PersistMessageBackgroundService.StartAsync(
Fixture.CancellationTokenSource.Token);
PersistDbConnection = new NpgsqlConnection(persistOptions.ConnectionString); PersistDbConnection = new NpgsqlConnection(persistOptions.ConnectionString);
await PersistDbConnection.OpenAsync(); await PersistDbConnection.OpenAsync();
using var scope = Fixture.ServiceProvider.CreateScope();
var dbContext = scope.ServiceProvider.GetRequiredService<PersistMessageDbContext>();
await dbContext.Database.EnsureCreatedAsync();
await Fixture.PersistMessageBackgroundService.StartAsync(Fixture.CancellationTokenSource.Token);
_reSpawnerPersistDb = await Respawner.CreateAsync( _reSpawnerPersistDb = await Respawner.CreateAsync(
PersistDbConnection, PersistDbConnection,
new RespawnerOptions { DbAdapter = DbAdapter.Postgres }); new RespawnerOptions { DbAdapter = DbAdapter.Postgres }
);
} }
if (!string.IsNullOrEmpty(postgresOptions?.ConnectionString)) if (!string.IsNullOrEmpty(postgresOptions?.ConnectionString) && _dbContextType != null)
{ {
DefaultDbConnection = new NpgsqlConnection(postgresOptions.ConnectionString); DefaultDbConnection = new NpgsqlConnection(postgresOptions.ConnectionString);
await DefaultDbConnection.OpenAsync(); await DefaultDbConnection.OpenAsync();
using var scope = Fixture.ServiceProvider.CreateScope();
if (scope.ServiceProvider.GetRequiredService(_dbContextType) is DbContext dbContext)
{
await dbContext.Database.EnsureCreatedAsync();
}
_reSpawnerDefaultDb = await Respawner.CreateAsync( _reSpawnerDefaultDb = await Respawner.CreateAsync(
DefaultDbConnection, DefaultDbConnection,
new RespawnerOptions { DbAdapter = DbAdapter.Postgres }); new RespawnerOptions { DbAdapter = DbAdapter.Postgres, TablesToIgnore = ["__EFMigrationsHistory"] }
);
await SeedDataAsync(); await SeedDataAsync();
} }
@ -643,8 +585,7 @@ where TEntryPoint : class
{ {
await _reSpawnerPersistDb.ResetAsync(PersistDbConnection); await _reSpawnerPersistDb.ResetAsync(PersistDbConnection);
await Fixture.PersistMessageBackgroundService.StopAsync( await Fixture.PersistMessageBackgroundService.StopAsync(Fixture.CancellationTokenSource.Token);
Fixture.CancellationTokenSource.Token);
} }
if (DefaultDbConnection is not null) if (DefaultDbConnection is not null)
@ -659,31 +600,33 @@ where TEntryPoint : class
var dbClient = new MongoClient(Fixture.MongoDbTestContainer?.GetConnectionString()); var dbClient = new MongoClient(Fixture.MongoDbTestContainer?.GetConnectionString());
var collections = await dbClient var collections = await dbClient
.GetDatabase(TestContainers.MongoContainerConfiguration.Name) .GetDatabase(TestContainers.MongoContainerConfiguration.Name)
.ListCollectionsAsync(cancellationToken: cancellationToken); .ListCollectionsAsync(cancellationToken: cancellationToken);
foreach (var collection in collections.ToList()) foreach (var collection in collections.ToList())
{ {
await dbClient.GetDatabase(TestContainers.MongoContainerConfiguration.Name) await dbClient
.GetDatabase(TestContainers.MongoContainerConfiguration.Name)
.DropCollectionAsync(collection["name"].AsString, cancellationToken); .DropCollectionAsync(collection["name"].AsString, cancellationToken);
} }
} }
private async Task ResetRabbitMqAsync(CancellationToken cancellationToken = default) private async Task ResetRabbitMqAsync(CancellationToken cancellationToken = default)
{ {
var port = Fixture.RabbitMqTestContainer?.GetMappedPublicPort( var port =
TestContainers.RabbitMqContainerConfiguration Fixture.RabbitMqTestContainer?.GetMappedPublicPort(TestContainers.RabbitMqContainerConfiguration.ApiPort)
.ApiPort) ?? ?? TestContainers.RabbitMqContainerConfiguration.ApiPort;
TestContainers.RabbitMqContainerConfiguration.ApiPort;
var managementClient = new ManagementClient(Fixture.RabbitMqTestContainer?.Hostname, var managementClient = new ManagementClient(
Fixture.RabbitMqTestContainer?.Hostname,
TestContainers.RabbitMqContainerConfiguration?.UserName, TestContainers.RabbitMqContainerConfiguration?.UserName,
TestContainers.RabbitMqContainerConfiguration?.Password, port); TestContainers.RabbitMqContainerConfiguration?.Password,
port
);
var bd = await managementClient.GetBindingsAsync(cancellationToken); var bd = await managementClient.GetBindingsAsync(cancellationToken);
var bindings = bd.Where( var bindings = bd.Where(x => !string.IsNullOrEmpty(x.Source) && !string.IsNullOrEmpty(x.Destination));
x => !string.IsNullOrEmpty(x.Source) && !string.IsNullOrEmpty(x.Destination));
foreach (var binding in bindings) foreach (var binding in bindings)
{ {
@ -698,9 +641,7 @@ where TEntryPoint : class
} }
} }
protected virtual void RegisterTestsServices(IServiceCollection services) protected virtual void RegisterTestsServices(IServiceCollection services) { }
{
}
private async Task SeedDataAsync() private async Task SeedDataAsync()
{ {
@ -712,14 +653,15 @@ where TEntryPoint : class
} }
public abstract class TestReadBase<TEntryPoint, TRContext> : TestFixtureCore<TEntryPoint> public abstract class TestReadBase<TEntryPoint, TRContext> : TestFixtureCore<TEntryPoint>
// ,IClassFixture<IntegrationTestFactory<TEntryPoint, TWContext>> // ,IClassFixture<IntegrationTestFactory<TEntryPoint, TWContext>>
where TEntryPoint : class where TEntryPoint : class
where TRContext : MongoDbContext where TRContext : MongoDbContext
{ {
protected TestReadBase( protected TestReadBase(
TestReadFixture<TEntryPoint, TRContext> integrationTestFixture, TestReadFixture<TEntryPoint, TRContext> integrationTestFixture,
ITestOutputHelper outputHelper = null ITestOutputHelper outputHelper = null
) : base(integrationTestFixture, outputHelper) )
: base(integrationTestFixture, outputHelper)
{ {
Fixture = integrationTestFixture; Fixture = integrationTestFixture;
} }
@ -728,14 +670,15 @@ where TRContext : MongoDbContext
} }
public abstract class TestWriteBase<TEntryPoint, TWContext> : TestFixtureCore<TEntryPoint> public abstract class TestWriteBase<TEntryPoint, TWContext> : TestFixtureCore<TEntryPoint>
//,IClassFixture<IntegrationTestFactory<TEntryPoint, TWContext>> //,IClassFixture<IntegrationTestFactory<TEntryPoint, TWContext>>
where TEntryPoint : class where TEntryPoint : class
where TWContext : DbContext where TWContext : DbContext
{ {
protected TestWriteBase( protected TestWriteBase(
TestWriteFixture<TEntryPoint, TWContext> integrationTestFixture, TestWriteFixture<TEntryPoint, TWContext> integrationTestFixture,
ITestOutputHelper outputHelper = null ITestOutputHelper outputHelper = null
) : base(integrationTestFixture, outputHelper) )
: base(integrationTestFixture, outputHelper, typeof(TWContext))
{ {
Fixture = integrationTestFixture; Fixture = integrationTestFixture;
} }
@ -744,16 +687,16 @@ where TWContext : DbContext
} }
public abstract class TestBase<TEntryPoint, TWContext, TRContext> : TestFixtureCore<TEntryPoint> public abstract class TestBase<TEntryPoint, TWContext, TRContext> : TestFixtureCore<TEntryPoint>
//,IClassFixture<IntegrationTestFactory<TEntryPoint, TWContext, TRContext>> //,IClassFixture<IntegrationTestFactory<TEntryPoint, TWContext, TRContext>>
where TEntryPoint : class where TEntryPoint : class
where TWContext : DbContext where TWContext : DbContext
where TRContext : MongoDbContext where TRContext : MongoDbContext
{ {
protected TestBase( protected TestBase(
TestFixture<TEntryPoint, TWContext, TRContext> integrationTestFixture, TestFixture<TEntryPoint, TWContext, TRContext> integrationTestFixture,
ITestOutputHelper outputHelper = null ITestOutputHelper outputHelper = null
) : )
base(integrationTestFixture, outputHelper) : base(integrationTestFixture, outputHelper, typeof(TWContext))
{ {
Fixture = integrationTestFixture; Fixture = integrationTestFixture;
} }

View File

@ -1,3 +1,5 @@
using DotNet.Testcontainers.Builders;
namespace BuildingBlocks.TestBase; namespace BuildingBlocks.TestBase;
using Testcontainers.EventStoreDb; using Testcontainers.EventStoreDb;
@ -18,15 +20,19 @@ public static class TestContainers
{ {
var configuration = ConfigurationHelper.GetConfiguration(); var configuration = ConfigurationHelper.GetConfiguration();
RabbitMqContainerConfiguration = RabbitMqContainerConfiguration = configuration.GetOptions<RabbitMqContainerOptions>(
configuration.GetOptions<RabbitMqContainerOptions>(nameof(RabbitMqContainerOptions)); nameof(RabbitMqContainerOptions)
PostgresContainerConfiguration = );
configuration.GetOptions<PostgresContainerOptions>(nameof(PostgresContainerOptions)); PostgresContainerConfiguration = configuration.GetOptions<PostgresContainerOptions>(
PostgresPersistContainerConfiguration = nameof(PostgresContainerOptions)
configuration.GetOptions<PostgresPersistContainerOptions>(nameof(PostgresPersistContainerOptions)); );
PostgresPersistContainerConfiguration = configuration.GetOptions<PostgresPersistContainerOptions>(
nameof(PostgresPersistContainerOptions)
);
MongoContainerConfiguration = configuration.GetOptions<MongoContainerOptions>(nameof(MongoContainerOptions)); MongoContainerConfiguration = configuration.GetOptions<MongoContainerOptions>(nameof(MongoContainerOptions));
EventStoreContainerConfiguration = EventStoreContainerConfiguration = configuration.GetOptions<EventStoreContainerOptions>(
configuration.GetOptions<EventStoreContainerOptions>(nameof(EventStoreContainerOptions)); nameof(EventStoreContainerOptions)
);
} }
public static PostgreSqlContainer PostgresTestContainer() public static PostgreSqlContainer PostgresTestContainer()
@ -98,8 +104,7 @@ public static class TestContainers
public static EventStoreDbContainer EventStoreTestContainer() public static EventStoreDbContainer EventStoreTestContainer()
{ {
var baseBuilder = new EventStoreDbBuilder() var baseBuilder = new EventStoreDbBuilder().WithLabel("Key", "Value");
.WithLabel("Key", "Value");
var builder = baseBuilder var builder = baseBuilder
.WithImage(EventStoreContainerConfiguration.ImageName) .WithImage(EventStoreContainerConfiguration.ImageName)

View File

@ -1,47 +1,39 @@
FROM mcr.microsoft.com/dotnet/sdk:9.0 AS builder # ---------- Build Stage ----------
WORKDIR / FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build
WORKDIR /src
COPY ./.editorconfig ./ # Copy solution-level files
COPY ./global.json ./ COPY .editorconfig .
COPY ./Directory.Build.props ./ COPY global.json .
COPY Directory.Build.props .
# Setup working directory for the project # Copy project files first (for Docker layer caching)
COPY ./src/BuildingBlocks/BuildingBlocks.csproj ./src/BuildingBlocks/ COPY src/BuildingBlocks/BuildingBlocks.csproj src/BuildingBlocks/
COPY ./src/Services/Booking/src/Booking/Booking.csproj ./src/Services/Booking/src/Booking/ COPY src/Services/Booking/src/Booking/Booking.csproj src/Services/Booking/src/Booking/
COPY ./src/Services/Booking/src/Booking.Api/Booking.Api.csproj ./src/Services/Booking/src/Booking.Api/ COPY src/Services/Booking/src/Booking.Api/Booking.Api.csproj src/Services/Booking/src/Booking.Api/
COPY src/Aspire/src/ServiceDefaults/ServiceDefaults.csproj src/Aspire/src/ServiceDefaults/
# Restore dependencies
RUN dotnet restore src/Services/Booking/src/Booking.Api/Booking.Api.csproj
# Restore nuget packages # Copy the rest of the source
RUN dotnet restore ./src/Services/Booking/src/Booking.Api/Booking.Api.csproj COPY src ./src
# Copy project files # Publish (build included)
COPY ./src/BuildingBlocks ./src/BuildingBlocks/ RUN dotnet publish src/Services/Booking/src/Booking.Api/Booking.Api.csproj \
COPY ./src/Services/Booking/src/Booking/ ./src/Services/Booking/src/Booking/ -c Release \
COPY ./src/Services/Booking/src/Booking.Api/ ./src/Services/Booking/src/Booking.Api/ -o /app/publish \
--no-restore
# Build project with Release configuration # ---------- Runtime Stage ----------
# and no restore, as we did it already FROM mcr.microsoft.com/dotnet/aspnet:10.0 AS runtime
WORKDIR /app
RUN ls COPY --from=build /app/publish .
RUN dotnet build -c Release --no-restore ./src/Services/Booking/src/Booking.Api/Booking.Api.csproj
WORKDIR /src/Services/Booking/src/Booking.Api ENV ASPNETCORE_URLS=http://+:80
ENV ASPNETCORE_ENVIRONMENT=docker
# 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:9.0
# Setup working directory for the project
WORKDIR /
COPY --from=builder /src/Services/Booking/src/Booking.Api/out .
ENV ASPNETCORE_URLS https://*:443, http://*:80
ENV ASPNETCORE_ENVIRONMENT docker
EXPOSE 80 EXPOSE 80
EXPOSE 443
ENTRYPOINT ["dotnet", "Booking.Api.dll"] ENTRYPOINT ["dotnet", "Booking.Api.dll"]

View File

@ -1,50 +0,0 @@
FROM mcr.microsoft.com/dotnet/sdk:9.0 AS builder
WORKDIR /
COPY ./.editorconfig ./
COPY ./global.json ./
COPY ./Directory.Build.props ./
# Setup working directory for the project
COPY ./src/BuildingBlocks/BuildingBlocks.csproj ./src/BuildingBlocks/
COPY ./src/Services/Booking/src/Booking/Booking.csproj ./src/Services/Booking/src/Booking/
COPY ./src/Services/Booking/src/Booking.Api/Booking.Api.csproj ./src/Services/Booking/src/Booking.Api/
# Restore nuget packages
RUN --mount=type=cache,id=booking_nuget,target=/root/.nuget/packages \
dotnet restore ./src/Services/Booking/src/Booking.Api/Booking.Api.csproj
# Copy project files
COPY ./src/BuildingBlocks ./src/BuildingBlocks/
COPY ./src/Services/Booking/src/Booking/ ./src/Services/Booking/src/Booking/
COPY ./src/Services/Booking/src/Booking.Api/ ./src/Services/Booking/src/Booking.Api/
# Build project with Release configuration
# and no restore, as we did it already
RUN ls
RUN --mount=type=cache,id=booking_nuget,target=/root/.nuget/packages\
dotnet build -c Release --no-restore ./src/Services/Booking/src/Booking.Api/Booking.Api.csproj
WORKDIR /src/Services/Booking/src/Booking.Api
# Publish project to output folder
# and no build, as we did it already
RUN --mount=type=cache,id=booking_nuget,target=/root/.nuget/packages\
dotnet publish -c Release --no-build -o out
FROM mcr.microsoft.com/dotnet/aspnet:9.0
# Setup working directory for the project
WORKDIR /
COPY --from=builder /src/Services/Booking/src/Booking.Api/out .
ENV ASPNETCORE_URLS https://*:443, http://*:80
ENV ASPNETCORE_ENVIRONMENT docker
EXPOSE 80
EXPOSE 443
ENTRYPOINT ["dotnet", "Booking.Api.dll"]

View File

@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<ItemGroup> <ItemGroup>
<PackageReference Include="Grpc.Tools" Version="2.68.1"> <PackageReference Include="Grpc.Tools" Version="2.78.0">
<PrivateAssets>all</PrivateAssets> <PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference> </PackageReference>
@ -14,6 +14,7 @@
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<ProjectReference Include="..\..\..\..\Aspire\src\ServiceDefaults\ServiceDefaults.csproj" />
<ProjectReference Include="..\..\..\..\BuildingBlocks\BuildingBlocks.csproj" /> <ProjectReference Include="..\..\..\..\BuildingBlocks\BuildingBlocks.csproj" />
</ItemGroup> </ItemGroup>

View File

@ -13,9 +13,12 @@ public static class GrpcClientExtensions
public static IServiceCollection AddGrpcClients(this IServiceCollection services) public static IServiceCollection AddGrpcClients(this IServiceCollection services)
{ {
var grpcOptions = services.GetOptions<GrpcOptions>("Grpc"); var grpcOptions = services.GetOptions<GrpcOptions>("Grpc");
var resilienceOptions = services.GetOptions<HttpStandardResilienceOptions>(nameof(HttpStandardResilienceOptions)); var resilienceOptions = services.GetOptions<HttpStandardResilienceOptions>(
nameof(HttpStandardResilienceOptions)
);
services.AddGrpcClient<FlightGrpcService.FlightGrpcServiceClient>(o => services
.AddGrpcClient<FlightGrpcService.FlightGrpcServiceClient>(o =>
{ {
o.Address = new Uri(grpcOptions.FlightAddress); o.Address = new Uri(grpcOptions.FlightAddress);
}) })
@ -25,53 +28,36 @@ public static class GrpcClientExtensions
{ {
var timeSpan = TimeSpan.FromMinutes(1); var timeSpan = TimeSpan.FromMinutes(1);
options.AddRetry( options.AddRetry(new HttpRetryStrategyOptions { MaxRetryAttempts = 3 });
new HttpRetryStrategyOptions
{
MaxRetryAttempts = 3,
});
options.AddCircuitBreaker( options.AddCircuitBreaker(
new HttpCircuitBreakerStrategyOptions new HttpCircuitBreakerStrategyOptions { SamplingDuration = timeSpan * 2 }
{ );
SamplingDuration = timeSpan * 2,
});
options.AddTimeout( options.AddTimeout(new HttpTimeoutStrategyOptions { Timeout = timeSpan * 3 });
new HttpTimeoutStrategyOptions }
{ );
Timeout = timeSpan * 3,
});
});
services.AddGrpcClient<PassengerGrpcService.PassengerGrpcServiceClient>(o => services
{ .AddGrpcClient<PassengerGrpcService.PassengerGrpcServiceClient>(o =>
o.Address = new Uri(grpcOptions.PassengerAddress);
})
.AddResilienceHandler(
"grpc-passenger-resilience",
options =>
{ {
var timeSpan = TimeSpan.FromMinutes(1); o.Address = new Uri(grpcOptions.PassengerAddress);
})
.AddResilienceHandler(
"grpc-passenger-resilience",
options =>
{
var timeSpan = TimeSpan.FromMinutes(1);
options.AddRetry( options.AddRetry(new HttpRetryStrategyOptions { MaxRetryAttempts = 3 });
new HttpRetryStrategyOptions
{
MaxRetryAttempts = 3,
});
options.AddCircuitBreaker( options.AddCircuitBreaker(
new HttpCircuitBreakerStrategyOptions new HttpCircuitBreakerStrategyOptions { SamplingDuration = timeSpan * 2 }
{ );
SamplingDuration = timeSpan * 2,
});
options.AddTimeout( options.AddTimeout(new HttpTimeoutStrategyOptions { Timeout = timeSpan * 3 });
new HttpTimeoutStrategyOptions }
{ );
Timeout = timeSpan * 3,
});
});
return services; return services;
} }

View File

@ -12,12 +12,14 @@ using BuildingBlocks.PersistMessageProcessor;
using BuildingBlocks.ProblemDetails; using BuildingBlocks.ProblemDetails;
using BuildingBlocks.Web; using BuildingBlocks.Web;
using Figgle; using Figgle;
using Figgle.Fonts;
using FluentValidation; using FluentValidation;
using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Hosting;
using ServiceDefaults;
namespace Booking.Extensions.Infrastructure; namespace Booking.Extensions.Infrastructure;
@ -28,25 +30,7 @@ public static class InfrastructureExtensions
var configuration = builder.Configuration; var configuration = builder.Configuration;
var env = builder.Environment; var env = builder.Environment;
builder.Services.AddCustomHealthCheck(); builder.AddServiceDefaults();
builder.AddCustomObservability();
builder.Services.AddServiceDiscovery();
builder.Services.ConfigureHttpClientDefaults(http =>
{
http.AddStandardResilienceHandler(options =>
{
var timeSpan = TimeSpan.FromMinutes(1);
options.CircuitBreaker.SamplingDuration = timeSpan * 2;
options.TotalRequestTimeout.Timeout = timeSpan * 3;
options.Retry.MaxRetryAttempts = 3;
});
// Turn on service discovery by default
http.AddServiceDiscovery();
});
builder.Services.AddScoped<ICurrentUserProvider, CurrentUserProvider>(); builder.Services.AddScoped<ICurrentUserProvider, CurrentUserProvider>();
builder.Services.AddScoped<IEventMapper, BookingEventMapper>(); builder.Services.AddScoped<IEventMapper, BookingEventMapper>();
@ -94,8 +78,7 @@ public static class InfrastructureExtensions
app.UseAuthentication(); app.UseAuthentication();
app.UseAuthorization(); app.UseAuthorization();
app.UseCustomHealthCheck(); app.UseServiceDefaults();
app.UseCustomObservability();
app.UseCustomProblemDetails(); app.UseCustomProblemDetails();
app.UseCorrelationId(); app.UseCorrelationId();

View File

@ -19,10 +19,8 @@ namespace Integration.Test.Booking.Features
{ {
public class CreateBookingTests : BookingIntegrationTestBase public class CreateBookingTests : BookingIntegrationTestBase
{ {
public CreateBookingTests(TestReadFixture<Program, BookingReadDbContext> integrationTestFixture) : base( public CreateBookingTests(TestReadFixture<Program, BookingReadDbContext> integrationTestFixture)
integrationTestFixture) : base(integrationTestFixture) { }
{
}
protected override void RegisterTestsServices(IServiceCollection services) protected override void RegisterTestsServices(IServiceCollection services)
{ {
@ -40,46 +38,81 @@ namespace Integration.Test.Booking.Features
var response = await Fixture.SendAsync(command); var response = await Fixture.SendAsync(command);
// Assert // Assert
response?.Id.Should().BeGreaterOrEqualTo(0); response?.Id.Should().BeGreaterThanOrEqualTo(0);
(await Fixture.WaitForPublishing<BookingCreated>()).Should().Be(true); (await Fixture.WaitForPublishing<BookingCreated>()).Should().Be(true);
} }
private void MockPassengerGrpcServices(IServiceCollection services) private void MockPassengerGrpcServices(IServiceCollection services)
{ {
services.Replace(ServiceDescriptor.Singleton(x => services.Replace(
{ ServiceDescriptor.Singleton(x =>
var mockPassenger = Substitute.For<PassengerGrpcService.PassengerGrpcServiceClient>(); {
var mockPassenger = Substitute.For<PassengerGrpcService.PassengerGrpcServiceClient>();
mockPassenger.GetByIdAsync(Arg.Any<Passenger.GetByIdRequest>()) mockPassenger
.Returns(TestCalls.AsyncUnaryCall(Task.FromResult(FakePassengerResponse.Generate()), .GetByIdAsync(Arg.Any<Passenger.GetByIdRequest>())
Task.FromResult(new Metadata()), () => Status.DefaultSuccess, () => new Metadata(), () => { })); .Returns(
TestCalls.AsyncUnaryCall(
Task.FromResult(FakePassengerResponse.Generate()),
Task.FromResult(new Metadata()),
() => Status.DefaultSuccess,
() => new Metadata(),
() => { }
)
);
return mockPassenger; return mockPassenger;
})); })
);
} }
private void MockFlightGrpcServices(IServiceCollection services) private void MockFlightGrpcServices(IServiceCollection services)
{ {
services.Replace(ServiceDescriptor.Singleton(x => services.Replace(
{ ServiceDescriptor.Singleton(x =>
var mockFlight = Substitute.For<FlightGrpcService.FlightGrpcServiceClient>(); {
var mockFlight = Substitute.For<FlightGrpcService.FlightGrpcServiceClient>();
mockFlight.GetByIdAsync(Arg.Any<GetByIdRequest>()) mockFlight
.Returns(TestCalls.AsyncUnaryCall(Task.FromResult(FakeFlightResponse.Generate()), .GetByIdAsync(Arg.Any<GetByIdRequest>())
Task.FromResult(new Metadata()), () => Status.DefaultSuccess, () => new Metadata(), () => { })); .Returns(
TestCalls.AsyncUnaryCall(
Task.FromResult(FakeFlightResponse.Generate()),
Task.FromResult(new Metadata()),
() => Status.DefaultSuccess,
() => new Metadata(),
() => { }
)
);
mockFlight.GetAvailableSeatsAsync(Arg.Any<GetAvailableSeatsRequest>()) mockFlight
.Returns(TestCalls.AsyncUnaryCall(Task.FromResult(FakeGetAvailableSeatsResponse.Generate()), .GetAvailableSeatsAsync(Arg.Any<GetAvailableSeatsRequest>())
Task.FromResult(new Metadata()), () => Status.DefaultSuccess, () => new Metadata(), () => { })); .Returns(
TestCalls.AsyncUnaryCall(
Task.FromResult(FakeGetAvailableSeatsResponse.Generate()),
Task.FromResult(new Metadata()),
() => Status.DefaultSuccess,
() => new Metadata(),
() => { }
)
);
mockFlight.ReserveSeatAsync(Arg.Any<ReserveSeatRequest>()) mockFlight
.Returns(TestCalls.AsyncUnaryCall(Task.FromResult(FakeReserveSeatResponse.Generate()), .ReserveSeatAsync(Arg.Any<ReserveSeatRequest>())
Task.FromResult(new Metadata()), () => Status.DefaultSuccess, () => new Metadata(), () => { })); .Returns(
TestCalls.AsyncUnaryCall(
Task.FromResult(FakeReserveSeatResponse.Generate()),
Task.FromResult(new Metadata()),
() => Status.DefaultSuccess,
() => new Metadata(),
() => { }
)
);
return mockFlight; return mockFlight;
})); })
);
} }
} }
} }

View File

@ -1,4 +1,3 @@
using Booking.Api;
using Booking.Data; using Booking.Data;
using BuildingBlocks.TestBase; using BuildingBlocks.TestBase;
using Xunit; using Xunit;
@ -8,9 +7,8 @@ namespace Integration.Test;
[Collection(IntegrationTestCollection.Name)] [Collection(IntegrationTestCollection.Name)]
public class BookingIntegrationTestBase : TestReadBase<Program, BookingReadDbContext> public class BookingIntegrationTestBase : TestReadBase<Program, BookingReadDbContext>
{ {
public BookingIntegrationTestBase(TestReadFixture<Program, BookingReadDbContext> integrationTestFixture) : base(integrationTestFixture) public BookingIntegrationTestBase(TestReadFixture<Program, BookingReadDbContext> integrationTestFixture)
{ : base(integrationTestFixture) { }
}
} }
[CollectionDefinition(Name)] [CollectionDefinition(Name)]

View File

@ -1,4 +1,5 @@
namespace Integration.Test.Fakes; namespace Integration.Test.Fakes;
using Flight; using Flight;
using MassTransit; using MassTransit;

View File

@ -7,9 +7,9 @@
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.12.0" /> <PackageReference Include="Microsoft.NET.Test.Sdk" Version="18.0.1" />
<PackageReference Include="xunit" Version="2.9.2" /> <PackageReference Include="xunit" Version="2.9.3" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.8.2"> <PackageReference Include="xunit.runner.visualstudio" Version="3.1.5">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets> <PrivateAssets>all</PrivateAssets>
</PackageReference> </PackageReference>

View File

@ -1,48 +1,39 @@
FROM mcr.microsoft.com/dotnet/sdk:9.0 AS builder # ---------- Build Stage ----------
WORKDIR / FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build
WORKDIR /src
COPY ./.editorconfig ./ # Copy solution-level files
COPY ./global.json ./ COPY .editorconfig .
COPY ./Directory.Build.props ./ COPY global.json .
COPY Directory.Build.props .
# Setup working directory for the project # Copy project files first (better layer caching)
COPY ./src/BuildingBlocks/BuildingBlocks.csproj ./src/BuildingBlocks/ COPY src/BuildingBlocks/BuildingBlocks.csproj src/BuildingBlocks/
COPY ./src/Services/Flight/src/Flight/Flight.csproj ./src/Services/Flight/src/Flight/ COPY src/Services/Flight/src/Flight/Flight.csproj src/Services/Flight/src/Flight/
COPY ./src/Services/Flight/src/Flight.Api/Flight.Api.csproj ./src/Services/Flight/src/Flight.Api/ COPY src/Services/Flight/src/Flight.Api/Flight.Api.csproj src/Services/Flight/src/Flight.Api/
COPY src/Aspire/src/ServiceDefaults/ServiceDefaults.csproj src/Aspire/src/ServiceDefaults/
# Restore dependencies
RUN dotnet restore src/Services/Flight/src/Flight.Api/Flight.Api.csproj
# Restore nuget packages # Copy remaining source code
RUN dotnet restore ./src/Services/Flight/src/Flight.Api/Flight.Api.csproj COPY src ./src
# Copy project files # Publish (build included)
COPY ./src/BuildingBlocks ./src/BuildingBlocks/ RUN dotnet publish src/Services/Flight/src/Flight.Api/Flight.Api.csproj \
COPY ./src/Services/Flight/src/Flight/ ./src/Services/Flight/src/Flight/ -c Release \
COPY ./src/Services/Flight/src/Flight.Api/ ./src/Services/Flight/src/Flight.Api/ -o /app/publish \
--no-restore
# Build project with Release configuration # ---------- Runtime Stage ----------
# and no restore, as we did it already FROM mcr.microsoft.com/dotnet/aspnet:10.0 AS runtime
WORKDIR /app
RUN ls COPY --from=build /app/publish .
RUN dotnet build -c Release --no-restore ./src/Services/Flight/src/Flight.Api/Flight.Api.csproj
WORKDIR /src/Services/Flight/src/Flight.Api ENV ASPNETCORE_URLS=http://+:80
ENV ASPNETCORE_ENVIRONMENT=docker
# 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:9.0
# Setup working directory for the project
WORKDIR /
COPY --from=builder /src/Services/Flight/src/Flight.Api/out .
ENV ASPNETCORE_URLS https://*:443, http://*:80
ENV ASPNETCORE_ENVIRONMENT docker
EXPOSE 80 EXPOSE 80
EXPOSE 443
ENTRYPOINT ["dotnet", "Flight.Api.dll"] ENTRYPOINT ["dotnet", "Flight.Api.dll"]

View File

@ -1,51 +0,0 @@
FROM mcr.microsoft.com/dotnet/sdk:9.0 AS builder
WORKDIR /
COPY ./.editorconfig ./
COPY ./global.json ./
COPY ./Directory.Build.props ./
# Setup working directory for the project
COPY ./src/BuildingBlocks/BuildingBlocks.csproj ./src/BuildingBlocks/
COPY ./src/Services/Flight/src/Flight/Flight.csproj ./src/Services/Flight/src/Flight/
COPY ./src/Services/Flight/src/Flight.Api/Flight.Api.csproj ./src/Services/Flight/src/Flight.Api/
# Restore nuget packages
RUN --mount=type=cache,id=flight_nuget,target=/root/.nuget/packages \
dotnet restore ./src/Services/Flight/src/Flight.Api/Flight.Api.csproj
# Copy project files
COPY ./src/BuildingBlocks ./src/BuildingBlocks/
COPY ./src/Services/Flight/src/Flight/ ./src/Services/Flight/src/Flight/
COPY ./src/Services/Flight/src/Flight.Api/ ./src/Services/Flight/src/Flight.Api/
# Build project with Release configuration
# and no restore, as we did it already
RUN ls
RUN --mount=type=cache,id=flight_nuget,target=/root/.nuget/packages \
dotnet build -c Release --no-restore ./src/Services/Flight/src/Flight.Api/Flight.Api.csproj
WORKDIR /src/Services/Flight/src/Flight.Api
# Publish project to output folder
# and no build, as we did it already
RUN --mount=type=cache,id=flight_nuget,target=/root/.nuget/packages \
dotnet publish -c Release --no-build -o out
FROM mcr.microsoft.com/dotnet/aspnet:9.0
# Setup working directory for the project
WORKDIR /
COPY --from=builder /src/Services/Flight/src/Flight.Api/out .
ENV ASPNETCORE_URLS https://*:443, http://*:80
ENV ASPNETCORE_ENVIRONMENT docker
EXPOSE 80
EXPOSE 443
ENTRYPOINT ["dotnet", "Flight.Api.dll"]

View File

@ -12,6 +12,7 @@ using BuildingBlocks.PersistMessageProcessor;
using BuildingBlocks.ProblemDetails; using BuildingBlocks.ProblemDetails;
using BuildingBlocks.Web; using BuildingBlocks.Web;
using Figgle; using Figgle;
using Figgle.Fonts;
using Flight.Data; using Flight.Data;
using Flight.Data.Seed; using Flight.Data.Seed;
using Flight.GrpcServer.Services; using Flight.GrpcServer.Services;
@ -21,6 +22,7 @@ using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Hosting;
using ServiceDefaults;
namespace Flight.Extensions.Infrastructure; namespace Flight.Extensions.Infrastructure;
@ -32,25 +34,7 @@ public static class InfrastructureExtensions
var configuration = builder.Configuration; var configuration = builder.Configuration;
var env = builder.Environment; var env = builder.Environment;
builder.Services.AddCustomHealthCheck(); builder.AddServiceDefaults();
builder.AddCustomObservability();
builder.Services.AddServiceDiscovery();
builder.Services.ConfigureHttpClientDefaults(http =>
{
http.AddStandardResilienceHandler(options =>
{
var timeSpan = TimeSpan.FromMinutes(1);
options.CircuitBreaker.SamplingDuration = timeSpan * 2;
options.TotalRequestTimeout.Timeout = timeSpan * 3;
options.Retry.MaxRetryAttempts = 3;
});
// Turn on service discovery by default
http.AddServiceDiscovery();
});
builder.Services.AddScoped<ICurrentUserProvider, CurrentUserProvider>(); builder.Services.AddScoped<ICurrentUserProvider, CurrentUserProvider>();
builder.Services.AddScoped<IEventMapper, FlightEventMapper>(); builder.Services.AddScoped<IEventMapper, FlightEventMapper>();
@ -100,8 +84,7 @@ public static class InfrastructureExtensions
app.UseAuthentication(); app.UseAuthentication();
app.UseAuthorization(); app.UseAuthorization();
app.UseCustomHealthCheck(); app.UseServiceDefaults();
app.UseCustomObservability();
app.UseCustomProblemDetails(); app.UseCustomProblemDetails();
app.UseCorrelationId(); app.UseCorrelationId();

View File

@ -1,13 +1,13 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<ItemGroup> <ItemGroup>
<PackageReference Include="Grpc.AspNetCore" Version="2.67.0" /> <PackageReference Include="Grpc.AspNetCore" Version="2.76.0" />
<PackageReference Include="Grpc.Net.Client" Version="2.67.0" /> <PackageReference Include="Grpc.Net.Client" Version="2.76.0" />
<PackageReference Include="Grpc.Tools" Version="2.68.1"> <PackageReference Include="Grpc.Tools" Version="2.78.0">
<PrivateAssets>all</PrivateAssets> <PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference> </PackageReference>
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="9.0.0"> <PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="10.0.3">
<PrivateAssets>all</PrivateAssets> <PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference> </PackageReference>
@ -22,6 +22,7 @@
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<ProjectReference Include="..\..\..\..\Aspire\src\ServiceDefaults\ServiceDefaults.csproj" />
<ProjectReference Include="..\..\..\..\BuildingBlocks\BuildingBlocks.csproj" /> <ProjectReference Include="..\..\..\..\BuildingBlocks\BuildingBlocks.csproj" />
</ItemGroup> </ItemGroup>

View File

@ -1,6 +1,7 @@
using System; using System;
namespace Flight.Flights.Dtos; namespace Flight.Flights.Dtos;
public record FlightDto(Guid Id, string FlightNumber, Guid AircraftId, Guid DepartureAirportId, public record FlightDto(Guid Id, string FlightNumber, Guid AircraftId, Guid DepartureAirportId,
DateTime DepartureDate, DateTime ArriveDate, Guid ArriveAirportId, decimal DurationMinutes, DateTime FlightDate, DateTime DepartureDate, DateTime ArriveDate, Guid ArriveAirportId, decimal DurationMinutes, DateTime FlightDate,
Enums.FlightStatus Status, decimal Price); Enums.FlightStatus Status, decimal Price);

View File

@ -1,4 +1,5 @@
namespace Flight.Flights.ValueObjects; namespace Flight.Flights.ValueObjects;
using System; using System;
using Flight.Flights.Exceptions; using Flight.Flights.Exceptions;

View File

@ -1,4 +1,5 @@
namespace Flight.Flights.ValueObjects; namespace Flight.Flights.ValueObjects;
using System; using System;
using Flight.Flights.Exceptions; using Flight.Flights.Exceptions;

View File

@ -1,4 +1,5 @@
namespace Flight.Flights.ValueObjects; namespace Flight.Flights.ValueObjects;
using System; using System;
using Flight.Flights.Exceptions; using Flight.Flights.Exceptions;

View File

@ -7,9 +7,9 @@
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.12.0" /> <PackageReference Include="Microsoft.NET.Test.Sdk" Version="18.0.1" />
<PackageReference Include="xunit" Version="2.9.2" /> <PackageReference Include="xunit" Version="2.9.3" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.8.2"> <PackageReference Include="xunit.runner.visualstudio" Version="3.1.5">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets> <PrivateAssets>all</PrivateAssets>
</PackageReference> </PackageReference>

View File

@ -34,6 +34,6 @@ public class GetAvailableFlightsTests : FlightIntegrationTestBase
// Assert // Assert
response?.Should().NotBeNull(); response?.Should().NotBeNull();
response?.Count.Should().BeGreaterOrEqualTo(2); response?.Count.Should().BeGreaterThanOrEqualTo(2);
} }
} }

View File

@ -7,9 +7,9 @@
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.12.0" /> <PackageReference Include="Microsoft.NET.Test.Sdk" Version="18.0.1" />
<PackageReference Include="xunit" Version="2.9.2" /> <PackageReference Include="xunit" Version="2.9.3" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.8.2"> <PackageReference Include="xunit.runner.visualstudio" Version="3.1.5">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets> <PrivateAssets>all</PrivateAssets>
</PackageReference> </PackageReference>

View File

@ -38,6 +38,6 @@ public class GetAvailableSeatsTests : FlightIntegrationTestBase
// Assert // Assert
response?.Should().NotBeNull(); response?.Should().NotBeNull();
response?.SeatDtos?.Count.Should().BeGreaterOrEqualTo(1); response?.SeatDtos?.Count.Should().BeGreaterThanOrEqualTo(1);
} }
} }

View File

@ -8,6 +8,7 @@ using Integration.Test.Fakes;
using Xunit; using Xunit;
namespace Integration.Test.Seat.Features; namespace Integration.Test.Seat.Features;
public class ReserveSeatTests : FlightIntegrationTestBase public class ReserveSeatTests : FlightIntegrationTestBase
{ {
public ReserveSeatTests( public ReserveSeatTests(

View File

@ -7,9 +7,9 @@
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.12.0" /> <PackageReference Include="Microsoft.NET.Test.Sdk" Version="18.0.1" />
<PackageReference Include="xunit" Version="2.9.2" /> <PackageReference Include="xunit" Version="2.9.3" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.8.2"> <PackageReference Include="xunit.runner.visualstudio" Version="3.1.5">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets> <PrivateAssets>all</PrivateAssets>
</PackageReference> </PackageReference>

View File

@ -1,47 +1,39 @@
FROM mcr.microsoft.com/dotnet/sdk:9.0 AS builder # ---------- Build Stage ----------
FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build
WORKDIR /src
# Setup working directory for the project # Copy solution-level files
WORKDIR / COPY .editorconfig .
COPY global.json .
COPY Directory.Build.props .
COPY ./.editorconfig ./ # Copy project files first (for better Docker layer caching)
COPY ./global.json ./ COPY src/BuildingBlocks/BuildingBlocks.csproj src/BuildingBlocks/
COPY ./Directory.Build.props ./ COPY src/Services/Identity/src/Identity/Identity.csproj src/Services/Identity/src/Identity/
COPY src/Services/Identity/src/Identity.Api/Identity.Api.csproj src/Services/Identity/src/Identity.Api/
COPY src/Aspire/src/ServiceDefaults/ServiceDefaults.csproj src/Aspire/src/ServiceDefaults/
COPY ./src/BuildingBlocks/BuildingBlocks.csproj ./src/BuildingBlocks/ # Restore dependencies
COPY ./src/Services/Identity/src/Identity/Identity.csproj ./src/Services/Identity/src/Identity/ RUN dotnet restore src/Services/Identity/src/Identity.Api/Identity.Api.csproj
COPY ./src/Services/Identity/src/Identity.Api/Identity.Api.csproj ./src/Services/Identity/src/Identity.Api/
# Restore nuget packages # Copy remaining source code
RUN dotnet restore ./src/Services/Identity/src/Identity.Api/Identity.Api.csproj COPY src ./src
# Copy project files # Publish (build included, no separate build step needed)
COPY ./src/BuildingBlocks ./src/BuildingBlocks/ RUN dotnet publish src/Services/Identity/src/Identity.Api/Identity.Api.csproj \
COPY ./src/Services/Identity/src/Identity/ ./src/Services/Identity/src/Identity/ -c Release \
COPY ./src/Services/Identity/src/Identity.Api/ ./src/Services/Identity/src/Identity.Api/ -o /app/publish \
--no-restore
# Build project with Release configuration # ---------- Runtime Stage ----------
# and no restore, as we did it already FROM mcr.microsoft.com/dotnet/aspnet:10.0 AS runtime
WORKDIR /app
RUN ls COPY --from=build /app/publish .
RUN dotnet build -c Release --no-restore ./src/Services/Identity/src/Identity.Api/Identity.Api.csproj
WORKDIR /src/Services/Identity/src/Identity.Api ENV ASPNETCORE_URLS=http://+:80
ENV ASPNETCORE_ENVIRONMENT=docker
# 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:9.0
# Setup working directory for the project
WORKDIR /
COPY --from=builder /src/Services/Identity/src/Identity.Api/out .
ENV ASPNETCORE_URLS https://*:443, http://*:80
ENV ASPNETCORE_ENVIRONMENT docker
EXPOSE 80 EXPOSE 80
EXPOSE 443
ENTRYPOINT ["dotnet", "Identity.Api.dll"] ENTRYPOINT ["dotnet", "Identity.Api.dll"]

View File

@ -1,50 +0,0 @@
FROM mcr.microsoft.com/dotnet/sdk:9.0 AS builder
# Setup working directory for the project
WORKDIR /
COPY ./.editorconfig ./
COPY ./global.json ./
COPY ./Directory.Build.props ./
COPY ./src/BuildingBlocks/BuildingBlocks.csproj ./src/BuildingBlocks/
COPY ./src/Services/Identity/src/Identity/Identity.csproj ./src/Services/Identity/src/Identity/
COPY ./src/Services/Identity/src/Identity.Api/Identity.Api.csproj ./src/Services/Identity/src/Identity.Api/
# Restore nuget packages
RUN --mount=type=cache,id=identity_nuget,target=/root/.nuget/packages \
dotnet restore ./src/Services/Identity/src/Identity.Api/Identity.Api.csproj
# Copy project files
COPY ./src/BuildingBlocks ./src/BuildingBlocks/
COPY ./src/Services/Identity/src/Identity/ ./src/Services/Identity/src/Identity/
COPY ./src/Services/Identity/src/Identity.Api/ ./src/Services/Identity/src/Identity.Api/
# Build project with Release configuration
# and no restore, as we did it already
RUN ls
RUN --mount=type=cache,id=identity_nuget,target=/root/.nuget/packages \
dotnet build -c Release --no-restore ./src/Services/Identity/src/Identity.Api/Identity.Api.csproj
WORKDIR /src/Services/Identity/src/Identity.Api
# Publish project to output folder
# and no build, as we did it already
RUN --mount=type=cache,id=identity_nuget,target=/root/.nuget/packages \
dotnet publish -c Release --no-build -o out
FROM mcr.microsoft.com/dotnet/aspnet:9.0
# Setup working directory for the project
WORKDIR /
COPY --from=builder /src/Services/Identity/src/Identity.Api/out .
ENV ASPNETCORE_URLS https://*:443, http://*:80
ENV ASPNETCORE_ENVIRONMENT docker
EXPOSE 80
EXPOSE 443
ENTRYPOINT ["dotnet", "Identity.Api.dll"]

View File

@ -1,14 +1,13 @@
using BuildingBlocks.Core; using BuildingBlocks.Core;
using BuildingBlocks.EFCore; using BuildingBlocks.EFCore;
using BuildingBlocks.HealthCheck;
using BuildingBlocks.Mapster; using BuildingBlocks.Mapster;
using BuildingBlocks.MassTransit; using BuildingBlocks.MassTransit;
using BuildingBlocks.OpenApi; using BuildingBlocks.OpenApi;
using BuildingBlocks.OpenTelemetryCollector;
using BuildingBlocks.PersistMessageProcessor; using BuildingBlocks.PersistMessageProcessor;
using BuildingBlocks.ProblemDetails; using BuildingBlocks.ProblemDetails;
using BuildingBlocks.Web; using BuildingBlocks.Web;
using Figgle; using Figgle;
using Figgle.Fonts;
using FluentValidation; using FluentValidation;
using Identity.Data; using Identity.Data;
using Identity.Data.Seed; using Identity.Data.Seed;
@ -18,6 +17,7 @@ using Microsoft.AspNetCore.HttpOverrides;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Hosting;
using ServiceDefaults;
namespace Identity.Extensions.Infrastructure; namespace Identity.Extensions.Infrastructure;
@ -28,25 +28,7 @@ public static class InfrastructureExtensions
var configuration = builder.Configuration; var configuration = builder.Configuration;
var env = builder.Environment; var env = builder.Environment;
builder.Services.AddCustomHealthCheck(); builder.AddServiceDefaults();
builder.AddCustomObservability();
builder.Services.AddServiceDiscovery();
builder.Services.ConfigureHttpClientDefaults(http =>
{
http.AddStandardResilienceHandler(options =>
{
var timeSpan = TimeSpan.FromMinutes(1);
options.CircuitBreaker.SamplingDuration = timeSpan * 2;
options.TotalRequestTimeout.Timeout = timeSpan * 3;
options.Retry.MaxRetryAttempts = 3;
});
// Turn on service discovery by default
http.AddServiceDiscovery();
});
builder.Services.AddScoped<ICurrentUserProvider, CurrentUserProvider>(); builder.Services.AddScoped<ICurrentUserProvider, CurrentUserProvider>();
builder.Services.AddScoped<IEventMapper, IdentityEventMapper>(); builder.Services.AddScoped<IEventMapper, IdentityEventMapper>();
@ -94,8 +76,7 @@ public static class InfrastructureExtensions
app.UseAuthentication(); app.UseAuthentication();
app.UseAuthorization(); app.UseAuthorization();
app.UseCustomHealthCheck(); app.UseServiceDefaults();
app.UseCustomObservability();
app.UseForwardedHeaders(); app.UseForwardedHeaders();

View File

@ -1,17 +1,14 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<ItemGroup> <ItemGroup>
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="9.0.0"> <PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="10.0.3">
<PrivateAssets>all</PrivateAssets> <PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference> </PackageReference>
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<Folder Include="Data\Migrations" /> <ProjectReference Include="..\..\..\..\Aspire\src\ServiceDefaults\ServiceDefaults.csproj" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\..\..\BuildingBlocks\BuildingBlocks.csproj" /> <ProjectReference Include="..\..\..\..\BuildingBlocks\BuildingBlocks.csproj" />
</ItemGroup> </ItemGroup>

View File

@ -10,10 +10,8 @@ namespace Integration.Test.Identity.Features;
public class RegisterNewUserTests : IdentityIntegrationTestBase public class RegisterNewUserTests : IdentityIntegrationTestBase
{ {
public RegisterNewUserTests( public RegisterNewUserTests(TestWriteFixture<Program, IdentityContext> integrationTestFactory)
TestWriteFixture<Program, IdentityContext> integrationTestFactory) : base(integrationTestFactory) : base(integrationTestFactory) { }
{
}
[Fact] [Fact]
public async Task should_create_new_user_to_db_and_publish_message_to_broker() public async Task should_create_new_user_to_db_and_publish_message_to_broker()

View File

@ -7,9 +7,9 @@
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.12.0" /> <PackageReference Include="Microsoft.NET.Test.Sdk" Version="18.0.1" />
<PackageReference Include="xunit" Version="2.9.2" /> <PackageReference Include="xunit" Version="2.9.3" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.8.2"> <PackageReference Include="xunit.runner.visualstudio" Version="3.1.5">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets> <PrivateAssets>all</PrivateAssets>
</PackageReference> </PackageReference>

View File

@ -1,47 +1,39 @@
FROM mcr.microsoft.com/dotnet/sdk:9.0 AS builder # ---------- Build Stage ----------
WORKDIR / FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build
WORKDIR /src
COPY ./.editorconfig ./ # Copy solution-level files
COPY ./global.json ./ COPY .editorconfig .
COPY ./Directory.Build.props ./ COPY global.json .
COPY Directory.Build.props .
# Setup working directory for the project # Copy project files first (better Docker layer caching)
COPY ./src/BuildingBlocks/BuildingBlocks.csproj ./src/BuildingBlocks/ COPY src/BuildingBlocks/BuildingBlocks.csproj src/BuildingBlocks/
COPY ./src/Services/Passenger/src/Passenger/Passenger.csproj ./src/Services/Passenger/src/Passenger/ COPY src/Services/Passenger/src/Passenger/Passenger.csproj src/Services/Passenger/src/Passenger/
COPY ./src/Services/Passenger/src/Passenger.Api/Passenger.Api.csproj ./src/Services/Passenger/src/Passenger.Api/ COPY src/Services/Passenger/src/Passenger.Api/Passenger.Api.csproj src/Services/Passenger/src/Passenger.Api/
COPY src/Aspire/src/ServiceDefaults/ServiceDefaults.csproj src/Aspire/src/ServiceDefaults/
# Restore dependencies
RUN dotnet restore src/Services/Passenger/src/Passenger.Api/Passenger.Api.csproj
# Restore nuget packages # Copy remaining source code
RUN dotnet restore ./src/Services/Passenger/src/Passenger.Api/Passenger.Api.csproj COPY src ./src
# Copy project files # Publish (build included)
COPY ./src/BuildingBlocks ./src/BuildingBlocks/ RUN dotnet publish src/Services/Passenger/src/Passenger.Api/Passenger.Api.csproj \
COPY ./src/Services/Passenger/src/Passenger/ ./src/Services/Passenger/src/Passenger/ -c Release \
COPY ./src/Services/Passenger/src/Passenger.Api/ ./src/Services/Passenger/src/Passenger.Api/ -o /app/publish \
--no-restore
# Build project with Release configuration # ---------- Runtime Stage ----------
# and no restore, as we did it already FROM mcr.microsoft.com/dotnet/aspnet:10.0 AS runtime
WORKDIR /app
RUN ls COPY --from=build /app/publish .
RUN dotnet build -c Release --no-restore ./src/Services/Passenger/src/Passenger.Api/Passenger.Api.csproj
WORKDIR /src/Services/Passenger/src/Passenger.Api ENV ASPNETCORE_URLS=http://+:80
ENV ASPNETCORE_ENVIRONMENT=docker
# 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:9.0
# Setup working directory for the project
WORKDIR /
COPY --from=builder /src/Services/Passenger/src/Passenger.Api/out .
ENV ASPNETCORE_URLS https://*:443, http://*:80
ENV ASPNETCORE_ENVIRONMENT docker
EXPOSE 80 EXPOSE 80
EXPOSE 443
ENTRYPOINT ["dotnet", "Passenger.Api.dll"] ENTRYPOINT ["dotnet", "Passenger.Api.dll"]

View File

@ -1,50 +0,0 @@
FROM mcr.microsoft.com/dotnet/sdk:9.0 AS builder
WORKDIR /
COPY ./.editorconfig ./
COPY ./global.json ./
COPY ./Directory.Build.props ./
# Setup working directory for the project
COPY ./src/BuildingBlocks/BuildingBlocks.csproj ./src/BuildingBlocks/
COPY ./src/Services/Passenger/src/Passenger/Passenger.csproj ./src/Services/Passenger/src/Passenger/
COPY ./src/Services/Passenger/src/Passenger.Api/Passenger.Api.csproj ./src/Services/Passenger/src/Passenger.Api/
# Restore nuget packages
RUN --mount=type=cache,id=passenger_nuget,target=/root/.nuget/packages \
dotnet restore ./src/Services/Passenger/src/Passenger.Api/Passenger.Api.csproj
# Copy project files
COPY ./src/BuildingBlocks ./src/BuildingBlocks/
COPY ./src/Services/Passenger/src/Passenger/ ./src/Services/Passenger/src/Passenger/
COPY ./src/Services/Passenger/src/Passenger.Api/ ./src/Services/Passenger/src/Passenger.Api/
# Build project with Release configuration
# and no restore, as we did it already
RUN ls
RUN --mount=type=cache,id=passenger_nuget,target=/root/.nuget/packages \
dotnet build -c Release --no-restore ./src/Services/Passenger/src/Passenger.Api/Passenger.Api.csproj
WORKDIR /src/Services/Passenger/src/Passenger.Api
# Publish project to output folder
# and no build, as we did it already
RUN --mount=type=cache,id=passenger_nuget,target=/root/.nuget/packages \
dotnet publish -c Release --no-build -o out
FROM mcr.microsoft.com/dotnet/aspnet:9.0
# Setup working directory for the project
WORKDIR /
COPY --from=builder /src/Services/Passenger/src/Passenger.Api/out .
ENV ASPNETCORE_URLS https://*:443, http://*:80
ENV ASPNETCORE_ENVIRONMENT docker
EXPOSE 80
EXPOSE 443
ENTRYPOINT ["dotnet", "Passenger.Api.dll"]

View File

@ -1,6 +1,7 @@
using SmartCharging.Infrastructure.Exceptions; using SmartCharging.Infrastructure.Exceptions;
namespace Passenger.Exceptions; namespace Passenger.Exceptions;
using System; using System;
public class InvalidPassengerIdException : DomainException public class InvalidPassengerIdException : DomainException

View File

@ -12,6 +12,7 @@ using BuildingBlocks.PersistMessageProcessor;
using BuildingBlocks.ProblemDetails; using BuildingBlocks.ProblemDetails;
using BuildingBlocks.Web; using BuildingBlocks.Web;
using Figgle; using Figgle;
using Figgle.Fonts;
using FluentValidation; using FluentValidation;
using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http;
@ -20,6 +21,7 @@ using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Hosting;
using Passenger.Data; using Passenger.Data;
using Passenger.GrpcServer.Services; using Passenger.GrpcServer.Services;
using ServiceDefaults;
namespace Passenger.Extensions.Infrastructure; namespace Passenger.Extensions.Infrastructure;
@ -30,26 +32,7 @@ public static class InfrastructureExtensions
var configuration = builder.Configuration; var configuration = builder.Configuration;
var env = builder.Environment; var env = builder.Environment;
builder.Services.AddCustomHealthCheck(); builder.AddServiceDefaults();
builder.AddCustomObservability();
builder.Services.AddServiceDiscovery();
builder.Services.ConfigureHttpClientDefaults(http =>
{
http.AddStandardResilienceHandler(options =>
{
var timeSpan = TimeSpan.FromMinutes(1);
options.CircuitBreaker.SamplingDuration = timeSpan * 2;
options.TotalRequestTimeout.Timeout = timeSpan * 3;
options.Retry.MaxRetryAttempts = 3;
});
// Turn on service discovery by default
http.AddServiceDiscovery();
});
builder.Services.AddScoped<ICurrentUserProvider, CurrentUserProvider>(); builder.Services.AddScoped<ICurrentUserProvider, CurrentUserProvider>();
builder.Services.AddScoped<IEventMapper, PassengerEventMapper>(); builder.Services.AddScoped<IEventMapper, PassengerEventMapper>();
@ -97,8 +80,7 @@ public static class InfrastructureExtensions
app.UseAuthentication(); app.UseAuthentication();
app.UseAuthorization(); app.UseAuthorization();
app.UseCustomHealthCheck(); app.UseServiceDefaults();
app.UseCustomObservability();
app.UseCustomProblemDetails(); app.UseCustomProblemDetails();

View File

@ -1,8 +1,8 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<ItemGroup> <ItemGroup>
<PackageReference Include="Grpc.AspNetCore" Version="2.67.0" /> <PackageReference Include="Grpc.AspNetCore" Version="2.76.0" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="9.0.0"> <PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="10.0.3">
<PrivateAssets>all</PrivateAssets> <PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference> </PackageReference>
@ -17,6 +17,7 @@
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<ProjectReference Include="..\..\..\..\Aspire\src\ServiceDefaults\ServiceDefaults.csproj" />
<ProjectReference Include="..\..\..\..\BuildingBlocks\BuildingBlocks.csproj" /> <ProjectReference Include="..\..\..\..\BuildingBlocks\BuildingBlocks.csproj" />
</ItemGroup> </ItemGroup>

View File

@ -1,2 +1,3 @@
namespace Passenger.Passengers.Dtos; namespace Passenger.Passengers.Dtos;
public record PassengerDto(Guid Id, string Name, string PassportNumber, Enums.PassengerType PassengerType, int Age); public record PassengerDto(Guid Id, string Name, string PassportNumber, Enums.PassengerType PassengerType, int Age);

View File

@ -1,4 +1,5 @@
namespace Passenger.Passengers.Exceptions; namespace Passenger.Passengers.Exceptions;
using BuildingBlocks.Exception; using BuildingBlocks.Exception;
public class InvalidAgeException : BadRequestException public class InvalidAgeException : BadRequestException

View File

@ -1,4 +1,5 @@
namespace Passenger.Passengers.Exceptions; namespace Passenger.Passengers.Exceptions;
using BuildingBlocks.Exception; using BuildingBlocks.Exception;

View File

@ -1,4 +1,5 @@
namespace Passenger.Passengers.Exceptions; namespace Passenger.Passengers.Exceptions;
using BuildingBlocks.Exception; using BuildingBlocks.Exception;

View File

@ -17,17 +17,23 @@ using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Routing; using Microsoft.AspNetCore.Routing;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using Passenger.Passengers.ValueObjects; using ValueObjects;
public record CompleteRegisterPassenger public record CompleteRegisterPassenger(string PassportNumber, Enums.PassengerType PassengerType, int Age)
(string PassportNumber, Enums.PassengerType PassengerType, int Age) : ICommand<CompleteRegisterPassengerResult>, : ICommand<CompleteRegisterPassengerResult>,
IInternalCommand IInternalCommand
{ {
public Guid Id { get; init; } = NewId.NextGuid(); public Guid Id { get; init; } = NewId.NextGuid();
} }
public record PassengerRegistrationCompletedDomainEvent(Guid Id, string Name, string PassportNumber, public record PassengerRegistrationCompletedDomainEvent(
Enums.PassengerType PassengerType, int Age, bool IsDeleted = false) : IDomainEvent; Guid Id,
string Name,
string PassportNumber,
Enums.PassengerType PassengerType,
int Age,
bool IsDeleted = false
) : IDomainEvent;
public record CompleteRegisterPassengerResult(PassengerDto PassengerDto); public record CompleteRegisterPassengerResult(PassengerDto PassengerDto);
@ -39,18 +45,25 @@ public class CompleteRegisterPassengerEndpoint : IMinimalEndpoint
{ {
public IEndpointRouteBuilder MapEndpoint(IEndpointRouteBuilder builder) public IEndpointRouteBuilder MapEndpoint(IEndpointRouteBuilder builder)
{ {
builder.MapPost($"{EndpointConfig.BaseApiPath}/passenger/complete-registration", async ( builder
CompleteRegisterPassengerRequestDto request, IMapper mapper, .MapPost(
IMediator mediator, CancellationToken cancellationToken) => $"{EndpointConfig.BaseApiPath}/passenger/complete-registration",
{ async (
var command = mapper.Map<CompleteRegisterPassenger>(request); CompleteRegisterPassengerRequestDto request,
IMapper mapper,
IMediator mediator,
CancellationToken cancellationToken
) =>
{
var command = mapper.Map<CompleteRegisterPassenger>(request);
var result = await mediator.Send(command, cancellationToken); var result = await mediator.Send(command, cancellationToken);
var response = result.Adapt<CompleteRegisterPassengerResponseDto>(); var response = result.Adapt<CompleteRegisterPassengerResponseDto>();
return Results.Ok(response); return Results.Ok(response);
}) }
)
.RequireAuthorization(nameof(ApiScope)) .RequireAuthorization(nameof(ApiScope))
.WithName("CompleteRegisterPassenger") .WithName("CompleteRegisterPassenger")
.WithApiVersionSet(builder.NewApiVersionSet("Passenger").Build()) .WithApiVersionSet(builder.NewApiVersionSet("Passenger").Build())
@ -71,17 +84,19 @@ public class CompleteRegisterPassengerValidator : AbstractValidator<CompleteRegi
{ {
RuleFor(x => x.PassportNumber).NotNull().WithMessage("The PassportNumber is required!"); 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.Age).GreaterThan(0).WithMessage("The Age must be greater than 0!");
RuleFor(x => x.PassengerType).Must(p => p.GetType().IsEnum && RuleFor(x => x.PassengerType)
p == Enums.PassengerType.Baby || .Must(p =>
p == Enums.PassengerType.Female || p.GetType().IsEnum && p == Enums.PassengerType.Baby
p == Enums.PassengerType.Male || || p == Enums.PassengerType.Female
p == Enums.PassengerType.Unknown) || p == Enums.PassengerType.Male
|| p == Enums.PassengerType.Unknown
)
.WithMessage("PassengerType must be Male, Female, Baby or Unknown"); .WithMessage("PassengerType must be Male, Female, Baby or Unknown");
} }
} }
internal class CompleteRegisterPassengerCommandHandler : ICommandHandler<CompleteRegisterPassenger, internal class CompleteRegisterPassengerCommandHandler
CompleteRegisterPassengerResult> : ICommandHandler<CompleteRegisterPassenger, CompleteRegisterPassengerResult>
{ {
private readonly IMapper _mapper; private readonly IMapper _mapper;
private readonly PassengerDbContext _passengerDbContext; private readonly PassengerDbContext _passengerDbContext;
@ -92,21 +107,30 @@ internal class CompleteRegisterPassengerCommandHandler : ICommandHandler<Complet
_passengerDbContext = passengerDbContext; _passengerDbContext = passengerDbContext;
} }
public async Task<CompleteRegisterPassengerResult> Handle(CompleteRegisterPassenger request, public async Task<CompleteRegisterPassengerResult> Handle(
CancellationToken cancellationToken) CompleteRegisterPassenger request,
CancellationToken cancellationToken
)
{ {
Guard.Against.Null(request, nameof(request)); Guard.Against.Null(request, nameof(request));
var passenger = await _passengerDbContext.Passengers.SingleOrDefaultAsync( var passenger = await _passengerDbContext.Passengers.SingleOrDefaultAsync(
x => x.PassportNumber.Value == request.PassportNumber, cancellationToken); x => x.PassportNumber.Value == request.PassportNumber,
cancellationToken
);
if (passenger is null) if (passenger is null)
{ {
throw new PassengerNotExist(); throw new PassengerNotExist();
} }
passenger.CompleteRegistrationPassenger(passenger.Id, passenger.Name, passenger.CompleteRegistrationPassenger(
passenger.PassportNumber, request.PassengerType, Age.Of(request.Age)); passenger.Id,
passenger.Name,
passenger.PassportNumber,
request.PassengerType,
Age.Of(request.Age)
);
var updatePassenger = _passengerDbContext.Passengers.Update(passenger).Entity; var updatePassenger = _passengerDbContext.Passengers.Update(passenger).Entity;

View File

@ -1,4 +1,5 @@
namespace Passenger.Passengers.Models; namespace Passenger.Passengers.Models;
public class PassengerReadModel public class PassengerReadModel
{ {
public required Guid Id { get; init; } public required Guid Id { get; init; }

View File

@ -4,13 +4,12 @@ using Passenger.Passengers.Enums;
namespace Integration.Test.Fakes; namespace Integration.Test.Fakes;
using global::Passenger.Passengers.Features.CompletingRegisterPassenger.V1; using global::Passenger.Passengers.Features.CompletingRegisterPassenger.V1;
using MassTransit;
public sealed class FakeCompleteRegisterPassengerCommand : AutoFaker<CompleteRegisterPassenger> public sealed class FakeCompleteRegisterPassengerCommand : AutoFaker<CompleteRegisterPassenger>
{ {
public FakeCompleteRegisterPassengerCommand(string passportNumber) public FakeCompleteRegisterPassengerCommand(string passportNumber, Guid passengerId)
{ {
RuleFor(r => r.Id, _ => NewId.NextGuid()); RuleFor(r => r.Id, _ => passengerId);
RuleFor(r => r.PassportNumber, _ => passportNumber); RuleFor(r => r.PassportNumber, _ => passportNumber);
RuleFor(r => r.PassengerType, _ => PassengerType.Male); RuleFor(r => r.PassengerType, _ => PassengerType.Male);
RuleFor(r => r.Age, _ => 30); RuleFor(r => r.Age, _ => 30);

View File

@ -1,16 +0,0 @@
using AutoBogus;
using BuildingBlocks.Contracts.EventBus.Messages;
namespace Integration.Test.Fakes;
using MassTransit;
public class FakeUserCreated : AutoFaker<UserCreated>
{
public FakeUserCreated()
{
RuleFor(r => r.Id, _ => NewId.NextGuid());
RuleFor(r => r.Name, _ => "Sam");
RuleFor(r => r.PassportNumber, _ => "123456789");
}
}

View File

@ -7,9 +7,9 @@
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.12.0" /> <PackageReference Include="Microsoft.NET.Test.Sdk" Version="18.0.1" />
<PackageReference Include="xunit" Version="2.9.2" /> <PackageReference Include="xunit" Version="2.9.3" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.8.2"> <PackageReference Include="xunit.runner.visualstudio" Version="3.1.5">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets> <PrivateAssets>all</PrivateAssets>
</PackageReference> </PackageReference>

View File

@ -1,39 +1,39 @@
using System.Threading.Tasks;
using BuildingBlocks.Contracts.EventBus.Messages;
using BuildingBlocks.TestBase; using BuildingBlocks.TestBase;
using FluentAssertions; using FluentAssertions;
using Integration.Test.Fakes; using Integration.Test.Fakes;
using Passenger.Api;
using Passenger.Data; using Passenger.Data;
using Passenger.Passengers.ValueObjects;
using Xunit; using Xunit;
namespace Integration.Test.Passenger.Features; namespace Integration.Test.Passenger.Features;
public class CompleteRegisterPassengerTests : PassengerIntegrationTestBase public class CompleteRegisterPassengerTests : PassengerIntegrationTestBase
{ {
public CompleteRegisterPassengerTests( public CompleteRegisterPassengerTests(
TestFixture<Program, PassengerDbContext, PassengerReadDbContext> integrationTestFactory) : base(integrationTestFactory) TestFixture<Program, PassengerDbContext, PassengerReadDbContext> integrationTestFactory
{ )
} : base(integrationTestFactory) { }
[Fact] [Fact]
public async Task should_complete_register_passenger_and_update_to_db() public async Task should_complete_register_passenger_and_update_to_db()
{ {
// Arrange // Arrange
var userCreated = new FakeUserCreated().Generate(); var passenger = global::Passenger.Passengers.Models.Passenger.Create(
PassengerId.Of(Guid.CreateVersion7()),
Name.Of("Sam"),
PassportNumber.Of("123456789")
);
await Fixture.Publish(userCreated); await Fixture.InsertAsync(passenger);
(await Fixture.WaitForPublishing<UserCreated>()).Should().Be(true);
(await Fixture.WaitForConsuming<UserCreated>()).Should().Be(true);
var command = new FakeCompleteRegisterPassengerCommand(userCreated.PassportNumber).Generate(); var command = new FakeCompleteRegisterPassengerCommand(passenger.PassportNumber, passenger.Id).Generate();
// Act // Act
var response = await Fixture.SendAsync(command); var response = await Fixture.SendAsync(command);
// Assert // Assert
response.Should().NotBeNull(); response.Should().NotBeNull();
response?.PassengerDto?.Name.Should().Be(userCreated.Name); response?.PassengerDto?.Name.Should().Be(passenger.Name);
response?.PassengerDto?.PassportNumber.Should().Be(command.PassportNumber); response?.PassengerDto?.PassportNumber.Should().Be(command.PassportNumber);
response?.PassengerDto?.PassengerType.ToString().Should().Be(command.PassengerType.ToString()); response?.PassengerDto?.PassengerType.ToString().Should().Be(command.PassengerType.ToString());
response?.PassengerDto?.Age.Should().Be(command.Age); response?.PassengerDto?.Age.Should().Be(command.Age);