Compare commits

..

No commits in common. "main" and "v2.2.0" have entirely different histories.
main ... v2.2.0

68 changed files with 1247 additions and 1065 deletions

View File

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

View File

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

View File

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

View File

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

1
.gitpod.Dockerfile vendored Normal file
View File

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

37
.gitpod.yml Normal file
View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,41 +0,0 @@
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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -13,12 +13,9 @@ public static class GrpcClientExtensions
public static IServiceCollection AddGrpcClients(this IServiceCollection services) public static IServiceCollection AddGrpcClients(this IServiceCollection services)
{ {
var grpcOptions = services.GetOptions<GrpcOptions>("Grpc"); var grpcOptions = services.GetOptions<GrpcOptions>("Grpc");
var resilienceOptions = services.GetOptions<HttpStandardResilienceOptions>( var resilienceOptions = services.GetOptions<HttpStandardResilienceOptions>(nameof(HttpStandardResilienceOptions));
nameof(HttpStandardResilienceOptions)
);
services services.AddGrpcClient<FlightGrpcService.FlightGrpcServiceClient>(o =>
.AddGrpcClient<FlightGrpcService.FlightGrpcServiceClient>(o =>
{ {
o.Address = new Uri(grpcOptions.FlightAddress); o.Address = new Uri(grpcOptions.FlightAddress);
}) })
@ -28,37 +25,54 @@ public static class GrpcClientExtensions
{ {
var timeSpan = TimeSpan.FromMinutes(1); var timeSpan = TimeSpan.FromMinutes(1);
options.AddRetry(new HttpRetryStrategyOptions { MaxRetryAttempts = 3 }); options.AddRetry(
new HttpRetryStrategyOptions
{
MaxRetryAttempts = 3,
});
options.AddCircuitBreaker( 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 services.AddGrpcClient<PassengerGrpcService.PassengerGrpcServiceClient>(o =>
.AddGrpcClient<PassengerGrpcService.PassengerGrpcServiceClient>(o => {
o.Address = new Uri(grpcOptions.PassengerAddress);
})
.AddResilienceHandler(
"grpc-passenger-resilience",
options =>
{ {
o.Address = new Uri(grpcOptions.PassengerAddress); var timeSpan = TimeSpan.FromMinutes(1);
})
.AddResilienceHandler(
"grpc-passenger-resilience",
options =>
{
var timeSpan = TimeSpan.FromMinutes(1);
options.AddRetry(new HttpRetryStrategyOptions { MaxRetryAttempts = 3 }); options.AddRetry(
new HttpRetryStrategyOptions
{
MaxRetryAttempts = 3,
});
options.AddCircuitBreaker( 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,
});
});
return services; return services;
} }
} }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,51 @@
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,7 +12,6 @@ using BuildingBlocks.PersistMessageProcessor;
using BuildingBlocks.ProblemDetails; using BuildingBlocks.ProblemDetails;
using BuildingBlocks.Web; using BuildingBlocks.Web;
using Figgle; using Figgle;
using Figgle.Fonts;
using Flight.Data; using Flight.Data;
using Flight.Data.Seed; using Flight.Data.Seed;
using Flight.GrpcServer.Services; using Flight.GrpcServer.Services;
@ -22,7 +21,6 @@ using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Hosting;
using ServiceDefaults;
namespace Flight.Extensions.Infrastructure; namespace Flight.Extensions.Infrastructure;
@ -34,7 +32,25 @@ public static class InfrastructureExtensions
var configuration = builder.Configuration; var configuration = builder.Configuration;
var env = builder.Environment; var env = builder.Environment;
builder.AddServiceDefaults(); 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.Services.AddScoped<ICurrentUserProvider, CurrentUserProvider>(); builder.Services.AddScoped<ICurrentUserProvider, CurrentUserProvider>();
builder.Services.AddScoped<IEventMapper, FlightEventMapper>(); builder.Services.AddScoped<IEventMapper, FlightEventMapper>();
@ -84,7 +100,8 @@ public static class InfrastructureExtensions
app.UseAuthentication(); app.UseAuthentication();
app.UseAuthorization(); app.UseAuthorization();
app.UseServiceDefaults(); app.UseCustomHealthCheck();
app.UseCustomObservability();
app.UseCustomProblemDetails(); app.UseCustomProblemDetails();
app.UseCorrelationId(); app.UseCorrelationId();

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,50 @@
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,13 +1,14 @@
using BuildingBlocks.Core; using BuildingBlocks.Core;
using BuildingBlocks.EFCore; using BuildingBlocks.EFCore;
using BuildingBlocks.HealthCheck;
using BuildingBlocks.Mapster; using BuildingBlocks.Mapster;
using BuildingBlocks.MassTransit; using BuildingBlocks.MassTransit;
using BuildingBlocks.OpenApi; using BuildingBlocks.OpenApi;
using BuildingBlocks.OpenTelemetryCollector;
using BuildingBlocks.PersistMessageProcessor; using BuildingBlocks.PersistMessageProcessor;
using BuildingBlocks.ProblemDetails; using BuildingBlocks.ProblemDetails;
using BuildingBlocks.Web; using BuildingBlocks.Web;
using Figgle; using Figgle;
using Figgle.Fonts;
using FluentValidation; using FluentValidation;
using Identity.Data; using Identity.Data;
using Identity.Data.Seed; using Identity.Data.Seed;
@ -17,7 +18,6 @@ using Microsoft.AspNetCore.HttpOverrides;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Hosting;
using ServiceDefaults;
namespace Identity.Extensions.Infrastructure; namespace Identity.Extensions.Infrastructure;
@ -28,7 +28,25 @@ public static class InfrastructureExtensions
var configuration = builder.Configuration; var configuration = builder.Configuration;
var env = builder.Environment; var env = builder.Environment;
builder.AddServiceDefaults(); 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.Services.AddScoped<ICurrentUserProvider, CurrentUserProvider>(); builder.Services.AddScoped<ICurrentUserProvider, CurrentUserProvider>();
builder.Services.AddScoped<IEventMapper, IdentityEventMapper>(); builder.Services.AddScoped<IEventMapper, IdentityEventMapper>();
@ -76,7 +94,8 @@ public static class InfrastructureExtensions
app.UseAuthentication(); app.UseAuthentication();
app.UseAuthorization(); app.UseAuthorization();
app.UseServiceDefaults(); app.UseCustomHealthCheck();
app.UseCustomObservability();
app.UseForwardedHeaders(); app.UseForwardedHeaders();

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,50 @@
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,7 +1,6 @@
using SmartCharging.Infrastructure.Exceptions; using SmartCharging.Infrastructure.Exceptions;
namespace Passenger.Exceptions; namespace Passenger.Exceptions;
using System; using System;
public class InvalidPassengerIdException : DomainException public class InvalidPassengerIdException : DomainException

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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