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,
"tools": {
"dotnet-outdated-tool": {
"version": "4.6.4",
"version": "4.6.9",
"commands": [
"dotnet-outdated"
],
"rollForward": false
]
},
"dotnet-ef": {
"version": "9.0.0",
"version": "10.0.3",
"commands": [
"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
# CA1032: Implement standard exception constructors
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/

View File

@ -34,7 +34,7 @@ runs:
- name: Setup .NET
uses: actions/setup-dotnet@v4
with:
dotnet-version: '9.x.x'
dotnet-version: '10.x.x'
# https://learn.microsoft.com/en-us/dotnet/core/tools/global-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"?>
<Project>
<PropertyGroup>
<TargetFramework>net9.0</TargetFramework>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<PropertyGroup>
<RepositoryRoot>$(MSBuildThisFileDirectory)</RepositoryRoot>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="StyleCop.Analyzers" PrivateAssets="all" Version="1.1.118">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</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>
</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>
</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>
</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>
</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>
</PackageReference>
<PackageReference Include="AsyncAwaitBestPractices" PrivateAssets="all" Version="9.0.0">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="SerilogAnalyzer" PrivateAssets="all" Version="0.15.0">
<PackageReference Include="AsyncAwaitBestPractices" PrivateAssets="all" Version="10.0.0">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="CSharpGuidelinesAnalyzer" PrivateAssets="all" Version="3.8.5">

View File

@ -6,15 +6,12 @@
</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:
- [Booking with Modular Monolith Architecture](https://github.com/meysamhadeli/booking-modular-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>
<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>
@ -77,10 +74,10 @@
## 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.
- ✔️ **[`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.
- ✔️ **[`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.
@ -217,13 +214,13 @@ dotnet dev-certs https --trust
### 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
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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,45 +1,38 @@
FROM mcr.microsoft.com/dotnet/sdk:9.0 AS builder
WORKDIR /
# ---------- Build Stage ----------
FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build
WORKDIR /src
COPY ./.editorconfig ./
COPY ./global.json ./
COPY ./Directory.Build.props ./
# Copy solution-level files
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/
# Copy project files first (better Docker caching)
COPY src/BuildingBlocks/BuildingBlocks.csproj src/BuildingBlocks/
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
RUN dotnet restore ./src/ApiGateway/src/ApiGateway.csproj
# Copy the rest of the source code
COPY src ./src
# Copy project files
COPY ./src/BuildingBlocks ./src/BuildingBlocks/
COPY ./src/ApiGateway/src ./src/ApiGateway/src/
# Publish (build included)
RUN dotnet publish src/ApiGateway/src/ApiGateway.csproj \
-c Release \
-o /app/publish \
--no-restore
# Build project with Release configuration
# and no restore, as we did it already
# ---------- Runtime Stage ----------
FROM mcr.microsoft.com/dotnet/aspnet:10.0 AS runtime
WORKDIR /app
RUN ls
RUN dotnet build -c Release --no-restore ./src/ApiGateway/src/ApiGateway.csproj
COPY --from=build /app/publish .
WORKDIR /src/ApiGateway/src
# Publish project to output folder
# and no build, as we did it already
RUN dotnet publish -c Release --no-build -o out
FROM mcr.microsoft.com/dotnet/aspnet: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
ENV ASPNETCORE_URLS=http://+:80
ENV ASPNETCORE_ENVIRONMENT=docker
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 Figgle;
using Figgle.Fonts;
var builder = WebApplication.CreateBuilder(args);
var env = builder.Environment;
@ -7,7 +8,6 @@ var appOptions = builder.Services.GetOptions<AppOptions>("AppOptions");
Console.WriteLine(FiggleFonts.Standard.Render(appOptions.Name));
builder.Services.AddControllers();
builder.Services.AddHttpContextAccessor();

View File

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

View File

@ -2,39 +2,82 @@ using System.Net.Sockets;
var builder = DistributedApplication.CreateBuilder(args);
// 1. Database Services
var username = builder.AddParameter("username", "postgres", secret: true);
var password = builder.AddParameter("password", "postgres", secret: true);
builder.AddDockerComposeEnvironment("docker-compose");
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")
.WithEndpoint(port: 5432, targetPort: 5432, name: "postgres")
.WithEndpoint(
"tcp",
e =>
{
e.Port = 5432;
e.TargetPort = 5432;
e.IsProxied = true;
e.IsExternal = false;
})
.WithArgs(
"-c", "wal_level=logical",
"-c", "max_prepared_transactions=10"
)
.WithDataVolume("postgres-data")
.WithLifetime(ContainerLifetime.Persistent);
"-c",
"wal_level=logical",
"-c",
"max_prepared_transactions=10");
if (builder.ExecutionContext.IsPublishMode)
{
postgres.WithDataVolume("postgres-data")
.WithLifetime(ContainerLifetime.Persistent);
}
var flightDb = postgres.AddDatabase("flight");
var passengerDb = postgres.AddDatabase("passenger");
var identityDb = postgres.AddDatabase("identity");
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 mongo = builder.AddMongoDB("mongo", userName: mongoUsername, password: mongoPassword)
.WithImage("mongo:latest")
.WithEndpoint(port: 27017, targetPort: 27017, name: "mongo")
.WithDataVolume("mongo-data")
.WithLifetime(ContainerLifetime.Persistent);
.WithImage("mongo")
.WithImageTag("latest")
.WithEndpoint(
"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")
.WithImage("redis:latest")
.WithEndpoint(port: 6379, targetPort: 6379, name: "redis")
.WithDataVolume("redis-data")
.WithLifetime(ContainerLifetime.Persistent);
.WithEndpoint(
"tcp",
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")
.WithImage("eventstore/eventstore")
@ -43,41 +86,103 @@ var eventstore = builder.AddEventStore("eventstore")
.WithEnvironment("EVENTSTORE_START_STANDARD_PROJECTIONS", "True")
.WithEnvironment("EVENTSTORE_INSECURE", "True")
.WithEnvironment("EVENTSTORE_ENABLE_ATOM_PUB_OVER_HTTP", "True")
.WithHttpEndpoint(port: 2113, targetPort: 2113, name: "eventstore-http")
.WithDataVolume("eventstore-data")
.WithLifetime(ContainerLifetime.Persistent);
.WithEndpoint(
"http",
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
var rabbitmq = builder.AddRabbitMQ("rabbitmq")
.WithImage("rabbitmq:management")
.WithEndpoint(port: 5672, targetPort: 5672, name: "rabbitmq-amqp")
.WithEndpoint(port: 15672, targetPort: 15672, name: "rabbitmq-management")
.WithLifetime(ContainerLifetime.Persistent);
var rabbitmqUsername = builder.AddParameter("rabbitmq-username", "guest", secret: true);
var rabbitmqPassword = builder.AddParameter("rabbitmq-password", "guest", secret: true);
// 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")
.WithEndpoint(port: 6831, targetPort: 6831, name: "jaeger-udp", protocol: ProtocolType.Udp)
.WithEndpoint(port: 16686, targetPort: 16686, name: "jaeger-ui")
.WithEndpoint(port: 14268, targetPort: 14268, name: "jaeger-api")
.WithEndpoint(port: 14317, targetPort: 4317, name: "jaeger-otlp-grpc")
.WithEndpoint(port: 14318, targetPort: 4318, name: "jaeger-otlp-http")
.WithLifetime(ContainerLifetime.Persistent);
.WithEndpoint(
port: 6831,
targetPort: 6831,
name: "agent",
protocol: ProtocolType.Udp,
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")
.WithEndpoint(port: 9411, targetPort: 9411, name: "zipkin-api")
.WithLifetime(ContainerLifetime.Persistent);
.WithEndpoint(port: 9411, targetPort: 9411, name: "http", isProxied: true, isExternal: true);
if (builder.ExecutionContext.IsPublishMode)
{
zipkin.WithLifetime(ContainerLifetime.Persistent);
}
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")
.WithEndpoint(port: 11888, targetPort: 1888, name: "otel-pprof")
.WithEndpoint(port: 8888, targetPort: 8888, name: "otel-metrics")
.WithEndpoint(port: 8889, targetPort: 8889, name: "otel-exporter-metrics")
.WithEndpoint(port: 13133, targetPort: 13133, name: "otel-health")
.WithEndpoint(port: 4317, targetPort: 4317, name: "otel-grpc")
.WithEndpoint(port: 4318, targetPort: 4318, name: "otel-http")
.WithEndpoint(port: 55679, targetPort: 55679, name: "otel-zpages")
.WithLifetime(ContainerLifetime.Persistent);
.WithEndpoint(port: 11888, targetPort: 1888, name: "otel-pprof", isProxied: true, isExternal: true)
.WithEndpoint(port: 8888, targetPort: 8888, name: "otel-metrics", isProxied: true, isExternal: true)
.WithEndpoint(port: 8889, targetPort: 8889, name: "otel-exporter-metrics", isProxied: true, isExternal: true)
.WithEndpoint(port: 13133, targetPort: 13133, name: "otel-health", isProxied: true, isExternal: true)
.WithEndpoint(port: 4317, targetPort: 4317, name: "otel-grpc", isProxied: true, isExternal: true)
.WithEndpoint(port: 4318, targetPort: 4318, name: "otel-http", isProxied: true, isExternal: true)
.WithEndpoint(port: 55679, targetPort: 55679, name: "otel-zpages", isProxied: true, isExternal: true);
if (builder.ExecutionContext.IsPublishMode)
{
otelCollector.WithLifetime(ContainerLifetime.Persistent);
}
var prometheus = builder.AddContainer("prometheus", "prom/prometheus")
.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.templates=/usr/share/prometheus/consoles",
"--web.enable-remote-write-receiver")
.WithEndpoint(port: 9090, targetPort: 9090, name: "prometheus-web")
.WithLifetime(ContainerLifetime.Persistent);
.WithEndpoint(port: 9090, targetPort: 9090, name: "http", isProxied: true, isExternal: true);
if (builder.ExecutionContext.IsPublishMode)
{
prometheus.WithLifetime(ContainerLifetime.Persistent);
}
var grafana = builder.AddContainer("grafana", "grafana/grafana")
.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")
.WithBindMount("../../../../deployments/configs/grafana/provisioning", "/etc/grafana/provisioning")
.WithBindMount("../../../../deployments/configs/grafana/dashboards", "/var/lib/grafana/dashboards")
.WithEndpoint(port: 3000, targetPort: 3000, name: "grafana-web")
.WithLifetime(ContainerLifetime.Persistent);
.WithEndpoint(port: 3000, targetPort: 3000, name: "http", isProxied: true, isExternal: true);
if (builder.ExecutionContext.IsPublishMode)
{
grafana.WithLifetime(ContainerLifetime.Persistent);
}
var nodeExporter = builder.AddContainer("node-exporter", "prom/node-exporter")
.WithBindMount("/proc", "/host/proc", isReadOnly: true)
@ -108,22 +221,36 @@ var nodeExporter = builder.AddContainer("node-exporter", "prom/node-exporter")
"--path.procfs=/host/proc",
"--path.rootfs=/rootfs",
"--path.sysfs=/host/sys")
.WithEndpoint(port: 9101, targetPort: 9100, name: "node-exporter")
.WithLifetime(ContainerLifetime.Persistent);
.WithEndpoint(port: 9101, targetPort: 9100, name: "http", isProxied: true, isExternal: true);
if (builder.ExecutionContext.IsPublishMode)
{
nodeExporter.WithLifetime(ContainerLifetime.Persistent);
}
var tempo = builder.AddContainer("tempo", "grafana/tempo")
.WithBindMount("../../../../deployments/configs/tempo.yaml", "/etc/tempo.yaml", isReadOnly: true)
.WithArgs("--config.file=/etc/tempo.yaml")
.WithEndpoint(port: 3200, targetPort: 3200, name: "tempo")
.WithEndpoint(port: 24317, targetPort: 4317, name: "tempo-otlp-grpc")
.WithEndpoint(port: 24318, targetPort: 4318, name: "tempo-otlp-http")
.WithLifetime(ContainerLifetime.Persistent);
.WithEndpoint(port: 3200, targetPort: 3200, name: "http", isProxied: true, isExternal: false)
.WithEndpoint(port: 9095, targetPort: 9095, name: "grpc", isProxied: true, isExternal: false)
.WithEndpoint(port: 4317, targetPort: 4317, name: "otlp-grpc", isProxied: true, isExternal: false)
.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")
.WithBindMount("../../../../deployments/configs/loki-config.yaml", "/etc/loki/local-config.yaml", isReadOnly: true)
.WithArgs("-config.file=/etc/loki/local-config.yaml")
.WithEndpoint(port: 3100, targetPort: 3100, name: "loki")
.WithLifetime(ContainerLifetime.Persistent);
.WithEndpoint(port: 3100, targetPort: 3100, name: "http", isProxied: true, isExternal: false)
.WithEndpoint(port: 9096, targetPort: 9096, name: "grpc", isProxied: true, isExternal: false);
if (builder.ExecutionContext.IsPublishMode)
{
loki.WithLifetime(ContainerLifetime.Persistent);
}
var elasticsearch = builder.AddElasticsearch("elasticsearch")
.WithImage("docker.elastic.co/elasticsearch/elasticsearch:8.17.0")
@ -139,18 +266,41 @@ var elasticsearch = builder.AddElasticsearch("elasticsearch")
.WithEnvironment("transport.host", "localhost")
.WithEnvironment("bootstrap.memory_lock", "true")
.WithEnvironment("cluster.routing.allocation.disk.threshold_enabled", "false")
.WithEndpoint(port: 9200, targetPort: 9200, name: "elasticsearch-http")
.WithEndpoint(port: 9300, targetPort: 9300, name: "elasticsearch-transport")
.WithDataVolume("elastic-data")
.WithLifetime(ContainerLifetime.Persistent);
.WithEndpoint(
"http",
e =>
{
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")
.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)
.WaitFor(elasticsearch)
.WithLifetime(ContainerLifetime.Persistent);
.WaitFor(elasticsearch);
if (builder.ExecutionContext.IsPublishMode)
{
kibana.WithLifetime(ContainerLifetime.Persistent);
}
// 5. Application Services
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>
<PackageReference Include="Ardalis.GuardClauses" Version="5.0.0" />
<PackageReference Include="Asp.Versioning.Abstractions" Version="8.1.0" />
<PackageReference Include="Asp.Versioning.Http" Version="8.1.0" />
<PackageReference Include="Asp.Versioning.Mvc" Version="8.1.0" />
<PackageReference Include="Asp.Versioning.Mvc.ApiExplorer" Version="8.1.0" />
<PackageReference Include="Asp.Versioning.Http" Version="8.1.1" />
<PackageReference Include="Asp.Versioning.Mvc" Version="8.1.1" />
<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="EasyCaching.Core" Version="1.9.2" />
<PackageReference Include="EasyCaching.InMemory" Version="1.9.2" />
<PackageReference Include="EasyNetQ.Management.Client" Version="3.0.0" />
<PackageReference Include="EFCore.NamingConventions" Version="9.0.0" />
<PackageReference Include="Figgle" Version="0.5.1" />
<PackageReference Include="FluentValidation" Version="11.11.0" />
<PackageReference Include="FluentValidation.AspNetCore" Version="11.3.0" />
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="9.0.0" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="9.0.0" />
<PackageReference Include="Microsoft.Extensions.DependencyModel" Version="9.0.0" />
<PackageReference Include="Microsoft.Extensions.Http.Resilience" Version="9.0.0" />
<PackageReference Include="Microsoft.Extensions.ServiceDiscovery" Version="9.3.1" />
<PackageReference Include="Npgsql" Version="9.0.1" />
<PackageReference Include="EasyNetQ.Management.Client" Version="3.0.1" />
<PackageReference Include="EFCore.NamingConventions" Version="10.0.1" />
<PackageReference Include="Figgle" Version="0.6.5" />
<PackageReference Include="FluentValidation" Version="12.1.1" />
<PackageReference Include="FluentValidation.AspNetCore" Version="11.3.1" />
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="10.0.3" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="10.0.3" />
<PackageReference Include="Microsoft.Extensions.DependencyModel" Version="10.0.3" />
<PackageReference Include="Microsoft.Extensions.Http.Resilience" Version="10.3.0" />
<PackageReference Include="Microsoft.Extensions.ServiceDiscovery" Version="10.3.0" />
<PackageReference Include="Npgsql" Version="10.0.1" />
<PackageReference Include="NSubstitute" Version="5.3.0" />
<PackageReference Include="Polly" Version="8.5.0" />
<PackageReference Include="Humanizer.Core" Version="2.14.1" />
<PackageReference Include="Polly" Version="8.6.5" />
<PackageReference Include="Humanizer.Core" Version="3.0.1" />
<PackageReference Include="IdGen" Version="3.0.7" />
<PackageReference Include="Mapster" Version="7.4.0" />
<PackageReference Include="Mapster.DependencyInjection" Version="1.0.1" />
<PackageReference Include="MediatR" Version="12.4.1" />
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="9.0.0" />
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="9.0.0" />
<PackageReference Include="Microsoft.EntityFrameworkCore.InMemory" Version="9.0.0" />
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="9.0.0" />
<PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="9.0.0" />
<PackageReference Include="Microsoft.Extensions.Configuration.Binder" Version="9.0.0" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="9.0.0" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="9.0.0" />
<PackageReference Include="Microsoft.Extensions.Http.Polly" Version="9.0.0" />
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="9.0.0" />
<PackageReference Include="Microsoft.Extensions.Hosting" Version="9.0.0" />
<PackageReference Include="MongoDB.Driver" Version="3.1.0" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
<PackageReference Include="Scalar.AspNetCore" Version="1.2.64" />
<PackageReference Include="Scrutor" Version="5.0.2" />
<PackageReference Include="MediatR" Version="14.0.0" />
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="10.0.3" />
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="10.0.3" />
<PackageReference Include="Microsoft.EntityFrameworkCore.InMemory" Version="10.0.3" />
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="10.0.3" />
<PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="10.0.3" />
<PackageReference Include="Microsoft.Extensions.Configuration.Binder" Version="10.0.3" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="10.0.3" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="10.0.3" />
<PackageReference Include="Microsoft.Extensions.Http.Polly" Version="10.0.3" />
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="10.0.3" />
<PackageReference Include="Microsoft.Extensions.Hosting" Version="10.0.3" />
<PackageReference Include="MongoDB.Driver" Version="3.6.0" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.4" />
<PackageReference Include="Scalar.AspNetCore" Version="2.12.38" />
<PackageReference Include="Scrutor" Version="7.0.0" />
<PackageReference Include="Sieve" Version="2.5.5" />
<PackageReference Include="Swashbuckle.AspNetCore" Version="7.1.0" />
<PackageReference Include="Swashbuckle.AspNetCore.SwaggerGen" Version="7.1.0" />
<PackageReference Include="Swashbuckle.AspNetCore.SwaggerUI" Version="7.1.0" />
<PackageReference Include="MassTransit" Version="8.3.6" />
<PackageReference Include="MassTransit.RabbitMQ" Version="8.3.6" />
<PackageReference Include="Duende.IdentityServer" Version="7.0.8" />
<PackageReference Include="Duende.IdentityServer.AspNetIdentity" Version="7.0.8" />
<PackageReference Include="Duende.IdentityServer.EntityFramework" Version="7.0.8" />
<PackageReference Include="Duende.IdentityServer.EntityFramework.Storage" Version="7.0.8" />
<PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="9.0.0" />
<PackageReference Include="System.Linq.Async" Version="6.0.1" />
<PackageReference Include="System.Linq.Async.Queryable" Version="6.0.1" />
<PackageReference Include="Testcontainers" Version="4.0.0" />
<PackageReference Include="Testcontainers.EventStoreDb" Version="4.0.0" />
<PackageReference Include="Testcontainers.MongoDb" Version="4.0.0" />
<PackageReference Include="Testcontainers.PostgreSql" Version="4.0.0" />
<PackageReference Include="Testcontainers.RabbitMq" Version="4.0.0" />
<PackageReference Include="Unchase.Swashbuckle.AspNetCore.Extensions" Version="2.7.1" />
<PackageReference Include="Swashbuckle.AspNetCore" Version="10.1.2" />
<PackageReference Include="Swashbuckle.AspNetCore.SwaggerGen" Version="10.1.2" />
<PackageReference Include="Swashbuckle.AspNetCore.SwaggerUI" Version="10.1.2" />
<PackageReference Include="MassTransit" Version="8.5.8" />
<PackageReference Include="MassTransit.RabbitMQ" Version="8.5.8" />
<PackageReference Include="Duende.IdentityServer" Version="7.4.5" />
<PackageReference Include="Duende.IdentityServer.AspNetIdentity" Version="7.4.5" />
<PackageReference Include="Duende.IdentityServer.EntityFramework" Version="7.4.5" />
<PackageReference Include="Duende.IdentityServer.EntityFramework.Storage" Version="7.4.5" />
<PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="10.0.3" />
<PackageReference Include="System.Linq.Async" Version="7.0.0" />
<PackageReference Include="System.Linq.Async.Queryable" Version="7.0.0" />
<PackageReference Include="Testcontainers" Version="4.9.0" />
<PackageReference Include="Testcontainers.EventStoreDb" Version="4.9.0" />
<PackageReference Include="Testcontainers.MongoDb" Version="4.9.0" />
<PackageReference Include="Testcontainers.PostgreSql" Version="4.9.0" />
<PackageReference Include="Testcontainers.RabbitMq" Version="4.9.0" />
<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="Yarp.ReverseProxy" Version="2.2.0" />
<PackageReference Include="Microsoft.Extensions.Diagnostics.HealthChecks" Version="9.0.0" />
<PackageReference Include="Yarp.ReverseProxy" Version="2.3.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.Client" 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.MongoDb" Version="9.0.0" />
<PackageReference Include="Npgsql.OpenTelemetry" Version="9.0.1" />
<PackageReference Include="OpenTelemetry.Exporter.OpenTelemetryProtocol" Version="1.11.1" />
<PackageReference Include="OpenTelemetry.Extensions.Hosting" Version="1.11.1"/>
<PackageReference Include="OpenTelemetry.Instrumentation.AspNetCore" Version="1.11.0"/>
<PackageReference Include="OpenTelemetry.Instrumentation.GrpcNetClient" Version="1.11.0-beta.1"/>
<PackageReference Include="OpenTelemetry.Instrumentation.Http" Version="1.11.0"/>
<PackageReference Include="OpenTelemetry.Instrumentation.Process" Version="1.11.0-beta.1"/>
<PackageReference Include="OpenTelemetry.Instrumentation.Runtime" Version="1.11.0"/>
<PackageReference Include="OpenTelemetry.Exporter.Prometheus.AspNetCore" Version="1.11.0-beta.1"/>
<PackageReference Include="Grafana.OpenTelemetry" Version="1.2.0"/>
<PackageReference Include="OpenTelemetry.Exporter.Console" Version="1.11.1"/>
<PackageReference Include="OpenTelemetry.Exporter.Zipkin" Version="1.11.1"/>
<PackageReference Include="Npgsql.OpenTelemetry" Version="10.0.1" />
<PackageReference Include="OpenTelemetry.Exporter.OpenTelemetryProtocol" Version="1.15.0" />
<PackageReference Include="OpenTelemetry.Extensions.Hosting" Version="1.15.0" />
<PackageReference Include="OpenTelemetry.Instrumentation.AspNetCore" Version="1.15.0" />
<PackageReference Include="OpenTelemetry.Instrumentation.GrpcNetClient" Version="1.15.0-beta.1" />
<PackageReference Include="OpenTelemetry.Instrumentation.Http" Version="1.15.0" />
<PackageReference Include="OpenTelemetry.Instrumentation.Process" Version="1.15.0-beta.1" />
<PackageReference Include="OpenTelemetry.Instrumentation.Runtime" Version="1.15.0" />
<PackageReference Include="OpenTelemetry.Exporter.Prometheus.AspNetCore" Version="1.15.0-beta.1" />
<PackageReference Include="Grafana.OpenTelemetry" Version="1.5.2" />
<PackageReference Include="OpenTelemetry.Exporter.Console" Version="1.15.0" />
<PackageReference Include="OpenTelemetry.Exporter.Zipkin" Version="1.15.0" />
<PackageReference Include="EventStore.Client.Grpc.Streams" Version="23.3.7" />
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="9.0.1" />
<PackageReference Include="EventStore.Client.Grpc.Streams" Version="23.3.9" />
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="10.0.0" />
<PackageReference Include="AutoBogus" Version="2.13.1" />
<PackageReference Include="Bogus" Version="35.6.1" />
<PackageReference Include="FluentAssertions" Version="7.0.0" />
<PackageReference Include="Respawn" Version="6.2.1" />
<PackageReference Include="Microsoft.AspNetCore.TestHost" Version="9.0.0" />
<PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" Version="9.0.0" />
<PackageReference Include="WebMotions.Fake.Authentication.JwtBearer" Version="8.0.1" />
<PackageReference Include="Bogus" Version="35.6.5" />
<PackageReference Include="FluentAssertions" Version="8.8.0" />
<PackageReference Include="Respawn" Version="7.0.0" />
<PackageReference Include="Microsoft.AspNetCore.TestHost" Version="10.0.3" />
<PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" Version="10.0.3" />
<PackageReference Include="WebMotions.Fake.Authentication.JwtBearer" Version="10.0.0" />
<PackageReference Include="Google.Protobuf" Version="3.29.1" />
<PackageReference Include="Grpc.Net.ClientFactory" Version="2.67.0" />
<PackageReference Include="Google.Protobuf" Version="3.33.5" />
<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>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>

View File

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

View File

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

View File

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

View File

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

View File

@ -1,47 +1,39 @@
FROM mcr.microsoft.com/dotnet/sdk:9.0 AS builder
WORKDIR /
# ---------- Build Stage ----------
FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build
WORKDIR /src
COPY ./.editorconfig ./
COPY ./global.json ./
COPY ./Directory.Build.props ./
# Copy solution-level files
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/
# Copy project files first (for Docker layer caching)
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/
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
RUN dotnet restore ./src/Services/Booking/src/Booking.Api/Booking.Api.csproj
# Copy the rest of the source
COPY src ./src
# 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/
# Publish (build included)
RUN dotnet publish src/Services/Booking/src/Booking.Api/Booking.Api.csproj \
-c Release \
-o /app/publish \
--no-restore
# Build project with Release configuration
# and no restore, as we did it already
# ---------- Runtime Stage ----------
FROM mcr.microsoft.com/dotnet/aspnet:10.0 AS runtime
WORKDIR /app
RUN ls
RUN dotnet build -c Release --no-restore ./src/Services/Booking/src/Booking.Api/Booking.Api.csproj
COPY --from=build /app/publish .
WORKDIR /src/Services/Booking/src/Booking.Api
# Publish project to output folder
# and no build, as we did it already
RUN dotnet publish -c Release --no-build -o out
FROM mcr.microsoft.com/dotnet/aspnet: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
ENV ASPNETCORE_URLS=http://+:80
ENV ASPNETCORE_ENVIRONMENT=docker
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">
<ItemGroup>
<PackageReference Include="Grpc.Tools" Version="2.68.1">
<PackageReference Include="Grpc.Tools" Version="2.78.0">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
@ -14,6 +14,7 @@
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\..\..\Aspire\src\ServiceDefaults\ServiceDefaults.csproj" />
<ProjectReference Include="..\..\..\..\BuildingBlocks\BuildingBlocks.csproj" />
</ItemGroup>

View File

@ -13,9 +13,12 @@ public static class GrpcClientExtensions
public static IServiceCollection AddGrpcClients(this IServiceCollection services)
{
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);
})
@ -25,54 +28,37 @@ public static class GrpcClientExtensions
{
var timeSpan = TimeSpan.FromMinutes(1);
options.AddRetry(
new HttpRetryStrategyOptions
{
MaxRetryAttempts = 3,
});
options.AddRetry(new HttpRetryStrategyOptions { MaxRetryAttempts = 3 });
options.AddCircuitBreaker(
new HttpCircuitBreakerStrategyOptions
{
SamplingDuration = timeSpan * 2,
});
new HttpCircuitBreakerStrategyOptions { SamplingDuration = timeSpan * 2 }
);
options.AddTimeout(
new HttpTimeoutStrategyOptions
{
Timeout = timeSpan * 3,
});
});
options.AddTimeout(new HttpTimeoutStrategyOptions { Timeout = timeSpan * 3 });
}
);
services.AddGrpcClient<PassengerGrpcService.PassengerGrpcServiceClient>(o =>
{
o.Address = new Uri(grpcOptions.PassengerAddress);
})
.AddResilienceHandler(
"grpc-passenger-resilience",
options =>
services
.AddGrpcClient<PassengerGrpcService.PassengerGrpcServiceClient>(o =>
{
var timeSpan = TimeSpan.FromMinutes(1);
o.Address = new Uri(grpcOptions.PassengerAddress);
})
.AddResilienceHandler(
"grpc-passenger-resilience",
options =>
{
var timeSpan = TimeSpan.FromMinutes(1);
options.AddRetry(
new HttpRetryStrategyOptions
{
MaxRetryAttempts = 3,
});
options.AddRetry(new HttpRetryStrategyOptions { MaxRetryAttempts = 3 });
options.AddCircuitBreaker(
new HttpCircuitBreakerStrategyOptions
{
SamplingDuration = timeSpan * 2,
});
options.AddCircuitBreaker(
new HttpCircuitBreakerStrategyOptions { SamplingDuration = timeSpan * 2 }
);
options.AddTimeout(
new HttpTimeoutStrategyOptions
{
Timeout = timeSpan * 3,
});
});
options.AddTimeout(new HttpTimeoutStrategyOptions { Timeout = timeSpan * 3 });
}
);
return services;
}
}
}

View File

@ -12,12 +12,14 @@ using BuildingBlocks.PersistMessageProcessor;
using BuildingBlocks.ProblemDetails;
using BuildingBlocks.Web;
using Figgle;
using Figgle.Fonts;
using FluentValidation;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using ServiceDefaults;
namespace Booking.Extensions.Infrastructure;
@ -28,25 +30,7 @@ public static class InfrastructureExtensions
var configuration = builder.Configuration;
var env = builder.Environment;
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();
});
builder.AddServiceDefaults();
builder.Services.AddScoped<ICurrentUserProvider, CurrentUserProvider>();
builder.Services.AddScoped<IEventMapper, BookingEventMapper>();
@ -94,8 +78,7 @@ public static class InfrastructureExtensions
app.UseAuthentication();
app.UseAuthorization();
app.UseCustomHealthCheck();
app.UseCustomObservability();
app.UseServiceDefaults();
app.UseCustomProblemDetails();
app.UseCorrelationId();

View File

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

View File

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

View File

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

View File

@ -1,48 +1,39 @@
FROM mcr.microsoft.com/dotnet/sdk:9.0 AS builder
WORKDIR /
# ---------- Build Stage ----------
FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build
WORKDIR /src
COPY ./.editorconfig ./
COPY ./global.json ./
COPY ./Directory.Build.props ./
# Copy solution-level files
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/
# Copy project files first (better layer caching)
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/
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
RUN dotnet restore ./src/Services/Flight/src/Flight.Api/Flight.Api.csproj
# Copy remaining source code
COPY src ./src
# 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/
# Publish (build included)
RUN dotnet publish src/Services/Flight/src/Flight.Api/Flight.Api.csproj \
-c Release \
-o /app/publish \
--no-restore
# Build project with Release configuration
# and no restore, as we did it already
# ---------- Runtime Stage ----------
FROM mcr.microsoft.com/dotnet/aspnet:10.0 AS runtime
WORKDIR /app
RUN ls
RUN dotnet build -c Release --no-restore ./src/Services/Flight/src/Flight.Api/Flight.Api.csproj
COPY --from=build /app/publish .
WORKDIR /src/Services/Flight/src/Flight.Api
# Publish project to output folder
# and no build, as we did it already
RUN dotnet publish -c Release --no-build -o out
FROM mcr.microsoft.com/dotnet/aspnet: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
ENV ASPNETCORE_URLS=http://+:80
ENV ASPNETCORE_ENVIRONMENT=docker
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.Web;
using Figgle;
using Figgle.Fonts;
using Flight.Data;
using Flight.Data.Seed;
using Flight.GrpcServer.Services;
@ -21,6 +22,7 @@ using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using ServiceDefaults;
namespace Flight.Extensions.Infrastructure;
@ -32,25 +34,7 @@ public static class InfrastructureExtensions
var configuration = builder.Configuration;
var env = builder.Environment;
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();
});
builder.AddServiceDefaults();
builder.Services.AddScoped<ICurrentUserProvider, CurrentUserProvider>();
builder.Services.AddScoped<IEventMapper, FlightEventMapper>();
@ -100,8 +84,7 @@ public static class InfrastructureExtensions
app.UseAuthentication();
app.UseAuthorization();
app.UseCustomHealthCheck();
app.UseCustomObservability();
app.UseServiceDefaults();
app.UseCustomProblemDetails();
app.UseCorrelationId();

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -38,6 +38,6 @@ public class GetAvailableSeatsTests : FlightIntegrationTestBase
// Assert
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;
namespace Integration.Test.Seat.Features;
public class ReserveSeatTests : FlightIntegrationTestBase
{
public ReserveSeatTests(

View File

@ -7,9 +7,9 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.12.0" />
<PackageReference Include="xunit" Version="2.9.2" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.8.2">
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="18.0.1" />
<PackageReference Include="xunit" Version="2.9.3" />
<PackageReference Include="xunit.runner.visualstudio" Version="3.1.5">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</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
WORKDIR /
# Copy solution-level files
COPY .editorconfig .
COPY global.json .
COPY Directory.Build.props .
COPY ./.editorconfig ./
COPY ./global.json ./
COPY ./Directory.Build.props ./
# Copy project files first (for better Docker layer caching)
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/
COPY src/Aspire/src/ServiceDefaults/ServiceDefaults.csproj src/Aspire/src/ServiceDefaults/
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 dependencies
RUN dotnet restore src/Services/Identity/src/Identity.Api/Identity.Api.csproj
# Restore nuget packages
RUN dotnet restore ./src/Services/Identity/src/Identity.Api/Identity.Api.csproj
# Copy remaining source code
COPY src ./src
# 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/
# Publish (build included, no separate build step needed)
RUN dotnet publish src/Services/Identity/src/Identity.Api/Identity.Api.csproj \
-c Release \
-o /app/publish \
--no-restore
# Build project with Release configuration
# and no restore, as we did it already
# ---------- Runtime Stage ----------
FROM mcr.microsoft.com/dotnet/aspnet:10.0 AS runtime
WORKDIR /app
RUN ls
RUN dotnet build -c Release --no-restore ./src/Services/Identity/src/Identity.Api/Identity.Api.csproj
COPY --from=build /app/publish .
WORKDIR /src/Services/Identity/src/Identity.Api
# Publish project to output folder
# and no build, as we did it already
RUN dotnet publish -c Release --no-build -o out
FROM mcr.microsoft.com/dotnet/aspnet: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
ENV ASPNETCORE_URLS=http://+:80
ENV ASPNETCORE_ENVIRONMENT=docker
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.EFCore;
using BuildingBlocks.HealthCheck;
using BuildingBlocks.Mapster;
using BuildingBlocks.MassTransit;
using BuildingBlocks.OpenApi;
using BuildingBlocks.OpenTelemetryCollector;
using BuildingBlocks.PersistMessageProcessor;
using BuildingBlocks.ProblemDetails;
using BuildingBlocks.Web;
using Figgle;
using Figgle.Fonts;
using FluentValidation;
using Identity.Data;
using Identity.Data.Seed;
@ -18,6 +17,7 @@ using Microsoft.AspNetCore.HttpOverrides;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using ServiceDefaults;
namespace Identity.Extensions.Infrastructure;
@ -28,25 +28,7 @@ public static class InfrastructureExtensions
var configuration = builder.Configuration;
var env = builder.Environment;
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();
});
builder.AddServiceDefaults();
builder.Services.AddScoped<ICurrentUserProvider, CurrentUserProvider>();
builder.Services.AddScoped<IEventMapper, IdentityEventMapper>();
@ -94,8 +76,7 @@ public static class InfrastructureExtensions
app.UseAuthentication();
app.UseAuthorization();
app.UseCustomHealthCheck();
app.UseCustomObservability();
app.UseServiceDefaults();
app.UseForwardedHeaders();

View File

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

View File

@ -10,10 +10,8 @@ namespace Integration.Test.Identity.Features;
public class RegisterNewUserTests : IdentityIntegrationTestBase
{
public RegisterNewUserTests(
TestWriteFixture<Program, IdentityContext> integrationTestFactory) : base(integrationTestFactory)
{
}
public RegisterNewUserTests(TestWriteFixture<Program, IdentityContext> integrationTestFactory)
: base(integrationTestFactory) { }
[Fact]
public async Task should_create_new_user_to_db_and_publish_message_to_broker()
@ -30,4 +28,4 @@ public class RegisterNewUserTests : IdentityIntegrationTestBase
(await Fixture.WaitForPublishing<UserCreated>()).Should().Be(true);
}
}
}

View File

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

View File

@ -1,47 +1,39 @@
FROM mcr.microsoft.com/dotnet/sdk:9.0 AS builder
WORKDIR /
# ---------- Build Stage ----------
FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build
WORKDIR /src
COPY ./.editorconfig ./
COPY ./global.json ./
COPY ./Directory.Build.props ./
# Copy solution-level files
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/
# Copy project files first (better Docker layer caching)
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/
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
RUN dotnet restore ./src/Services/Passenger/src/Passenger.Api/Passenger.Api.csproj
# Copy remaining source code
COPY src ./src
# 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/
# Publish (build included)
RUN dotnet publish src/Services/Passenger/src/Passenger.Api/Passenger.Api.csproj \
-c Release \
-o /app/publish \
--no-restore
# Build project with Release configuration
# and no restore, as we did it already
# ---------- Runtime Stage ----------
FROM mcr.microsoft.com/dotnet/aspnet:10.0 AS runtime
WORKDIR /app
RUN ls
RUN dotnet build -c Release --no-restore ./src/Services/Passenger/src/Passenger.Api/Passenger.Api.csproj
COPY --from=build /app/publish .
WORKDIR /src/Services/Passenger/src/Passenger.Api
# Publish project to output folder
# and no build, as we did it already
RUN dotnet publish -c Release --no-build -o out
FROM mcr.microsoft.com/dotnet/aspnet: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
ENV ASPNETCORE_URLS=http://+:80
ENV ASPNETCORE_ENVIRONMENT=docker
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;
namespace Passenger.Exceptions;
using System;
public class InvalidPassengerIdException : DomainException

View File

@ -12,6 +12,7 @@ using BuildingBlocks.PersistMessageProcessor;
using BuildingBlocks.ProblemDetails;
using BuildingBlocks.Web;
using Figgle;
using Figgle.Fonts;
using FluentValidation;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http;
@ -20,6 +21,7 @@ using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Passenger.Data;
using Passenger.GrpcServer.Services;
using ServiceDefaults;
namespace Passenger.Extensions.Infrastructure;
@ -30,26 +32,7 @@ public static class InfrastructureExtensions
var configuration = builder.Configuration;
var env = builder.Environment;
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();
});
builder.AddServiceDefaults();
builder.Services.AddScoped<ICurrentUserProvider, CurrentUserProvider>();
builder.Services.AddScoped<IEventMapper, PassengerEventMapper>();
@ -97,8 +80,7 @@ public static class InfrastructureExtensions
app.UseAuthentication();
app.UseAuthorization();
app.UseCustomHealthCheck();
app.UseCustomObservability();
app.UseServiceDefaults();
app.UseCustomProblemDetails();

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -4,15 +4,14 @@ using Passenger.Passengers.Enums;
namespace Integration.Test.Fakes;
using global::Passenger.Passengers.Features.CompletingRegisterPassenger.V1;
using MassTransit;
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.PassengerType, _ => PassengerType.Male);
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>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.12.0" />
<PackageReference Include="xunit" Version="2.9.2" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.8.2">
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="18.0.1" />
<PackageReference Include="xunit" Version="2.9.3" />
<PackageReference Include="xunit.runner.visualstudio" Version="3.1.5">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>

View File

@ -1,41 +1,41 @@
using System.Threading.Tasks;
using BuildingBlocks.Contracts.EventBus.Messages;
using BuildingBlocks.TestBase;
using FluentAssertions;
using Integration.Test.Fakes;
using Passenger.Api;
using Passenger.Data;
using Passenger.Passengers.ValueObjects;
using Xunit;
namespace Integration.Test.Passenger.Features;
public class CompleteRegisterPassengerTests : PassengerIntegrationTestBase
{
public CompleteRegisterPassengerTests(
TestFixture<Program, PassengerDbContext, PassengerReadDbContext> integrationTestFactory) : base(integrationTestFactory)
{
}
TestFixture<Program, PassengerDbContext, PassengerReadDbContext> integrationTestFactory
)
: base(integrationTestFactory) { }
[Fact]
public async Task should_complete_register_passenger_and_update_to_db()
{
// 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.WaitForPublishing<UserCreated>()).Should().Be(true);
(await Fixture.WaitForConsuming<UserCreated>()).Should().Be(true);
await Fixture.InsertAsync(passenger);
var command = new FakeCompleteRegisterPassengerCommand(userCreated.PassportNumber).Generate();
var command = new FakeCompleteRegisterPassengerCommand(passenger.PassportNumber, passenger.Id).Generate();
// Act
var response = await Fixture.SendAsync(command);
// Assert
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?.PassengerType.ToString().Should().Be(command.PassengerType.ToString());
response?.PassengerDto?.Age.Should().Be(command.Age);
}
}
}